Merge changes I9f0a440d,I0b734cc7,I510ee307,Ia257021d into rvc-dev

* changes:
  Pretty-print parcelables when logging
  Add sysdump to SoundTriggerMiddlewareService
  SoundTriggerMiddlewareService logging decorator
  Refactor SoundTriggerMiddlewareService
This commit is contained in:
Ytai Ben-tsvi
2020-02-27 18:09:39 +00:00
committed by Android (Google) Code Review
6 changed files with 1602 additions and 668 deletions

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.soundtrigger_middleware;
import android.annotation.NonNull;
import java.io.PrintWriter;
/**
* An interface of an object that can generate a dump.
*/
interface Dumpable {
/**
* Generate a human-readable dump into the given writer.
* @param pw The writer.
*/
void dump(@NonNull PrintWriter pw);
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.soundtrigger_middleware;
import android.annotation.NonNull;
import android.annotation.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
/**
* A collection of pretty-print utilities for data objects.
*/
class ObjectPrinter {
/** Default maximum elements to print in a collection. */
static public final int kDefaultMaxCollectionLength = 16;
/**
* Simple version of {@link #print(Object, boolean, int)} that prints an object, without
* recursing into sub-objects.
*
* @param obj The object to print.
* @return A string representing the object.
*/
static String print(@Nullable Object obj) {
return print(obj, false, kDefaultMaxCollectionLength);
}
/**
* Pretty-prints an object.
*
* @param obj The object to print.
* @param deep Whether to pretty-print sub-objects (if false, just prints them
* with {@link Object#toString()}).
* @param maxCollectionLength Whenever encountering collections, maximum number of elements to
* print.
* @return A string representing the object.
*/
static String print(@Nullable Object obj, boolean deep, int maxCollectionLength) {
StringBuilder builder = new StringBuilder();
print(builder, obj, deep, maxCollectionLength);
return builder.toString();
}
/**
* This version is suitable for use inside a toString() override of an object, e.g.:
* <pre><code>
* class MyObject {
* ...
* @Override
* String toString() {
* return ObjectPrinter.printPublicFields(this, ...);
* }
* }
* </code></pre>
*
* @param obj The object to print.
* @param deep Whether to pretty-print sub-objects (if false, just prints them
* with {@link Object#toString()}).
* @param maxCollectionLength Whenever encountering collections, maximum number of elements to
* print.
*/
static String printPublicFields(@Nullable Object obj, boolean deep, int maxCollectionLength) {
StringBuilder builder = new StringBuilder();
printPublicFields(builder, obj, deep, maxCollectionLength);
return builder.toString();
}
/**
* A version of {@link #print(Object, boolean, int)} that uses a {@link StringBuilder}.
*
* @param builder StringBuilder to print into.
* @param obj The object to print.
* @param deep Whether to pretty-print sub-objects (if false, just prints them
* with {@link Object#toString()}).
* @param maxCollectionLength Whenever encountering collections, maximum number of elements to
* print.
*/
static void print(@NonNull StringBuilder builder, @Nullable Object obj, boolean deep,
int maxCollectionLength) {
try {
if (obj == null) {
builder.append("null");
return;
}
if (obj instanceof Boolean) {
builder.append(obj.toString());
return;
}
if (obj instanceof Number) {
builder.append(obj.toString());
return;
}
if (obj instanceof Character) {
builder.append('\'');
builder.append(obj.toString());
builder.append('\'');
return;
}
if (obj instanceof String) {
builder.append('"');
builder.append(obj.toString());
builder.append('"');
return;
}
Class cls = obj.getClass();
if (Collection.class.isAssignableFrom(cls)) {
Collection collection = (Collection) obj;
builder.append("[ ");
int length = collection.size();
boolean isLong = false;
int i = 0;
for (Object child : collection) {
if (i > 0) {
builder.append(", ");
}
if (i >= maxCollectionLength) {
isLong = true;
break;
}
print(builder, child, deep, maxCollectionLength);
++i;
}
if (isLong) {
builder.append("... (+");
builder.append(length - maxCollectionLength);
builder.append(" entries)");
}
builder.append(" ]");
return;
}
if (Map.class.isAssignableFrom(cls)) {
Map<?, ?> map = (Map<?, ?>) obj;
builder.append("< ");
int length = map.size();
boolean isLong = false;
int i = 0;
for (Map.Entry<?, ?> child : map.entrySet()) {
if (i > 0) {
builder.append(", ");
}
if (i >= maxCollectionLength) {
isLong = true;
break;
}
print(builder, child.getKey(), deep, maxCollectionLength);
builder.append(": ");
print(builder, child.getValue(), deep, maxCollectionLength);
++i;
}
if (isLong) {
builder.append("... (+");
builder.append(length - maxCollectionLength);
builder.append(" entries)");
}
builder.append(" >");
return;
}
if (cls.isArray()) {
builder.append("[ ");
int length = Array.getLength(obj);
boolean isLong = false;
for (int i = 0; i < length; ++i) {
if (i > 0) {
builder.append(", ");
}
if (i >= maxCollectionLength) {
isLong = true;
break;
}
print(builder, Array.get(obj, i), deep, maxCollectionLength);
}
if (isLong) {
builder.append("... (+");
builder.append(length - maxCollectionLength);
builder.append(" entries)");
}
builder.append(" ]");
return;
}
if (!deep) {
builder.append(obj.toString());
return;
}
printPublicFields(builder, obj, deep, maxCollectionLength);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* A version of {@link #printPublicFields(Object, boolean, int)} that uses a {@link
* StringBuilder}.
*
* @param obj The object to print.
* @param deep Whether to pretty-print sub-objects (if false, just prints them
* with {@link Object#toString()}).
* @param maxCollectionLength Whenever encountering collections, maximum number of elements to
* print.
*/
static void printPublicFields(@NonNull StringBuilder builder, @Nullable Object obj,
boolean deep,
int maxCollectionLength) {
try {
Class cls = obj.getClass();
builder.append("{ ");
boolean first = true;
for (Field fld : cls.getDeclaredFields()) {
int mod = fld.getModifiers();
if ((mod & Modifier.PUBLIC) != 0 && (mod & Modifier.STATIC) == 0) {
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(fld.getName());
builder.append(": ");
print(builder, fld.get(obj), deep, maxCollectionLength);
}
}
builder.append(" }");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,454 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.soundtrigger_middleware;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ModelParameterRange;
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
import android.media.soundtrigger_middleware.PhraseSoundModel;
import android.media.soundtrigger_middleware.RecognitionConfig;
import android.media.soundtrigger_middleware.RecognitionEvent;
import android.media.soundtrigger_middleware.SoundModel;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
/**
* An ISoundTriggerMiddlewareService decorator, which adds logging of all API calls (and
* callbacks).
*
* All API methods should follow this structure:
* <pre><code>
* @Override
* public @NonNull ReturnType someMethod(ArgType1 arg1, ArgType2 arg2) throws ExceptionType {
* try {
* ReturnType result = mDelegate.someMethod(arg1, arg2);
* logReturn("someMethod", result, arg1, arg2);
* return result;
* } catch (Exception e) {
* logException("someMethod", e, arg1, arg2);
* throw e;
* }
* }
* </code></pre>
* The actual handling of these events is then done inside of {@link #logReturnWithObject(Object,
* String, Object, Object[])}, {@link #logVoidReturnWithObject(Object, String, Object[])} and {@link
* #logExceptionWithObject(Object, String, Exception, Object[])}.
*/
public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareService, Dumpable {
private static final String TAG = "SoundTriggerMiddlewareLogging";
private final @NonNull ISoundTriggerMiddlewareService mDelegate;
public SoundTriggerMiddlewareLogging(@NonNull ISoundTriggerMiddlewareService delegate) {
mDelegate = delegate;
}
@Override
public @NonNull SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
try {
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
logReturn("listModules", result);
return result;
} catch (Exception e) {
logException("listModules", e);
throw e;
}
}
@Override
public @NonNull ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback)
throws RemoteException {
try {
ISoundTriggerModule result = mDelegate.attach(handle, new CallbackLogging(callback));
logReturn("attach", result, handle, callback);
return new ModuleLogging(result);
} catch (Exception e) {
logException("attach", e, handle, callback);
throw e;
}
}
@Override
public void setExternalCaptureState(boolean active) throws RemoteException {
try {
mDelegate.setExternalCaptureState(active);
logVoidReturn("setExternalCaptureState", active);
} catch (Exception e) {
logException("setExternalCaptureState", e, active);
throw e;
}
}
@Override public IBinder asBinder() {
throw new UnsupportedOperationException(
"This implementation is not inteded to be used directly with Binder.");
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
}
private void logReturn(String methodName, Object retVal, Object... args) {
logReturnWithObject(this, methodName, retVal, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
}
private class CallbackLogging implements ISoundTriggerCallback {
private final ISoundTriggerCallback mDelegate;
private CallbackLogging(ISoundTriggerCallback delegate) {
mDelegate = delegate;
}
@Override
public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException {
try {
mDelegate.onRecognition(modelHandle, event);
logVoidReturn("onRecognition", modelHandle, event);
} catch (Exception e) {
logException("onRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
throws RemoteException {
try {
mDelegate.onPhraseRecognition(modelHandle, event);
logVoidReturn("onPhraseRecognition", modelHandle, event);
} catch (Exception e) {
logException("onPhraseRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
try {
mDelegate.onRecognitionAvailabilityChange(available);
logVoidReturn("onRecognitionAvailabilityChange", available);
} catch (Exception e) {
logException("onRecognitionAvailabilityChange", e, available);
throw e;
}
}
@Override
public void onModuleDied() throws RemoteException {
try {
mDelegate.onModuleDied();
logVoidReturn("onModuleDied");
} catch (Exception e) {
logException("onModuleDied", e);
throw e;
}
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
}
@Override
public IBinder asBinder() {
return mDelegate.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
}
private class ModuleLogging implements ISoundTriggerModule {
private final ISoundTriggerModule mDelegate;
private ModuleLogging(ISoundTriggerModule delegate) {
mDelegate = delegate;
}
@Override
public int loadModel(SoundModel model) throws RemoteException {
try {
int result = mDelegate.loadModel(model);
logReturn("loadModel", result, model);
return result;
} catch (Exception e) {
logException("loadModel", e, model);
throw e;
}
}
@Override
public int loadPhraseModel(PhraseSoundModel model) throws RemoteException {
try {
int result = mDelegate.loadPhraseModel(model);
logReturn("loadPhraseModel", result, model);
return result;
} catch (Exception e) {
logException("loadPhraseModel", e, model);
throw e;
}
}
@Override
public void unloadModel(int modelHandle) throws RemoteException {
try {
mDelegate.unloadModel(modelHandle);
logVoidReturn("unloadModel", modelHandle);
} catch (Exception e) {
logException("unloadModel", e, modelHandle);
throw e;
}
}
@Override
public void startRecognition(int modelHandle, RecognitionConfig config)
throws RemoteException {
try {
mDelegate.startRecognition(modelHandle, config);
logVoidReturn("startRecognition", modelHandle, config);
} catch (Exception e) {
logException("startRecognition", e, modelHandle, config);
throw e;
}
}
@Override
public void stopRecognition(int modelHandle) throws RemoteException {
try {
mDelegate.stopRecognition(modelHandle);
logVoidReturn("stopRecognition", modelHandle);
} catch (Exception e) {
logException("stopRecognition", e, modelHandle);
throw e;
}
}
@Override
public void forceRecognitionEvent(int modelHandle) throws RemoteException {
try {
mDelegate.forceRecognitionEvent(modelHandle);
logVoidReturn("forceRecognitionEvent", modelHandle);
} catch (Exception e) {
logException("forceRecognitionEvent", e, modelHandle);
throw e;
}
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value)
throws RemoteException {
try {
mDelegate.setModelParameter(modelHandle, modelParam, value);
logVoidReturn("setModelParameter", modelHandle, modelParam, value);
} catch (Exception e) {
logException("setModelParameter", e, modelHandle, modelParam, value);
throw e;
}
}
@Override
public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
try {
int result = mDelegate.getModelParameter(modelHandle, modelParam);
logReturn("getModelParameter", result, modelHandle, modelParam);
return result;
} catch (Exception e) {
logException("getModelParameter", e, modelHandle, modelParam);
throw e;
}
}
@Override
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
throws RemoteException {
try {
ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
modelParam);
logReturn("queryModelParameterSupport", result, modelHandle, modelParam);
return result;
} catch (Exception e) {
logException("queryModelParameterSupport", e, modelHandle, modelParam);
throw e;
}
}
@Override
public void detach() throws RemoteException {
try {
mDelegate.detach();
logVoidReturn("detach");
} catch (Exception e) {
logException("detach", e);
throw e;
}
}
@Override
public IBinder asBinder() {
return mDelegate.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
}
private void logReturn(String methodName, Object retVal, Object... args) {
logReturnWithObject(this, methodName, retVal, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Actual logging logic below.
private static final int NUM_EVENTS_TO_DUMP = 64;
private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
private final @NonNull LinkedList<Event> mLastEvents = new LinkedList<>();
static private class Event {
public final long timestamp = System.currentTimeMillis();
public final String message;
private Event(String message) {
this.message = message;
}
}
private static String printArgs(@NonNull Object[] args) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < args.length; ++i) {
if (i > 0) {
result.append(", ");
}
printObject(result, args[i]);
}
return result.toString();
}
private static void printObject(@NonNull StringBuilder builder, @Nullable Object obj) {
if (obj instanceof Parcelable) {
ObjectPrinter.print(builder, obj, true, 16);
} else {
builder.append(obj.toString());
}
}
private static String printObject(@Nullable Object obj) {
StringBuilder builder = new StringBuilder();
printObject(builder, obj);
return builder.toString();
}
private void logReturnWithObject(@NonNull Object object, String methodName,
@Nullable Object retVal,
@NonNull Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s) -> %s", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printArgs(args),
printObject(retVal));
Log.i(TAG, message);
appendMessage(message);
}
private void logVoidReturnWithObject(@NonNull Object object, @NonNull String methodName,
@NonNull Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s)", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printArgs(args));
Log.i(TAG, message);
appendMessage(message);
}
private void logExceptionWithObject(@NonNull Object object, @NonNull String methodName,
@NonNull Exception ex,
Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s) threw", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printArgs(args));
Log.e(TAG, message, ex);
appendMessage(message + " " + ex.toString());
}
private void appendMessage(@NonNull String message) {
Event event = new Event(message);
synchronized (mLastEvents) {
if (mLastEvents.size() > NUM_EVENTS_TO_DUMP) {
mLastEvents.remove();
}
mLastEvents.add(event);
}
}
@Override public void dump(PrintWriter pw) {
pw.println();
pw.println("=========================================");
pw.println("Last events");
pw.println("=========================================");
synchronized (mLastEvents) {
for (Event event : mLastEvents) {
pw.print(DATE_FORMAT.format(new Date(event.timestamp)));
pw.print('\t');
pw.println(event.message);
}
}
pw.println();
if (mDelegate instanceof Dumpable) {
((Dumpable) mDelegate).dump(pw);
}
}
}

View File

@@ -16,91 +16,40 @@
package com.android.server.soundtrigger_middleware;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ModelParameterRange;
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
import android.media.soundtrigger_middleware.PhraseSoundModel;
import android.media.soundtrigger_middleware.RecognitionConfig;
import android.media.soundtrigger_middleware.RecognitionEvent;
import android.media.soundtrigger_middleware.RecognitionStatus;
import android.media.soundtrigger_middleware.SoundModel;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.media.soundtrigger_middleware.Status;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Objects;
import java.util.Set;
/**
* This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes
* it as a Binder service and enforces permissions and correct usage by the client, as well as makes
* sure that exceptions representing a server malfunction do not get sent to the client.
* it as a Binder service.
* <p>
* This is intended to extract the non-business logic out of the underlying implementation and thus
* make it easier to maintain each one of those separate aspects. A design trade-off is being made
* here, in that this class would need to essentially eavesdrop on all the client-server
* communication and retain all state known to the client, while the client doesn't necessarily care
* about all of it, and while the server has its own representation of this information. However,
* in this case, this is a small amount of data, and the benefits in code elegance seem worth it.
* There is also some additional cost in employing a simplistic locking mechanism here, but
* following the same line of reasoning, the benefits in code simplicity outweigh it.
* This is intended to facilitate a pattern of decorating the core implementation (business logic)
* of the interface with every decorator implementing a different aspect of the service, such as
* validation and logging. This class acts as the top-level decorator, which also adds the binder-
* related functionality (hence, it extends ISoundTriggerMiddlewareService.Stub as rather than
* implements ISoundTriggerMiddlewareService), and does the same thing for child interfaces
* returned.
* <p>
* Every public method in this class, overriding an interface method, must follow the following
* pattern:
* <code><pre>
* @Override public T method(S arg) {
* // Permission check.
* checkPermissions();
* // Input validation.
* ValidationUtil.validateS(arg);
* synchronized (this) {
* // State validation.
* if (...state is not valid for this call...) {
* throw new IllegalStateException("State is invalid because...");
* }
* // From here on, every exception isn't client's fault.
* try {
* T result = mDelegate.method(arg);
* // Update state.;
* ...
* return result;
* } catch (Exception e) {
* throw handleException(e);
* }
* }
* }
* </pre></code>
* Following this patterns ensures a consistent and rigorous handling of all aspects associated
* with client-server separation.
* <p>
* <b>Exception handling approach:</b><br>
* We make sure all client faults (permissions, argument and state validation) happen first, and
* would throw {@link SecurityException}, {@link IllegalArgumentException}/
* {@link NullPointerException} or {@link
* IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
* will get sent back to the client.<br>
* Once this is done, any subsequent fault is considered a server fault. Only {@link
* RecoverableException}s thrown by the implementation are special-cased: they would get sent back
* to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other
* exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type
* that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level
* exception handler on the server side, typically resulting in rebooting the server.
* The inner class {@link Lifecycle} acts as both a factory, composing the various aspect-decorators
* to create a full-featured implementation, as well as as an entry-point for presenting this
* implementation as a system service.
* <p>
* <b>Exposing this service as a System Service:</b><br>
* Insert this line into {@link com.android.server.SystemServer}:
@@ -113,224 +62,100 @@ import java.util.Set;
public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub {
static private final String TAG = "SoundTriggerMiddlewareService";
@NonNull
private final ISoundTriggerMiddlewareService mDelegate;
private final Context mContext;
private Set<Integer> mModuleHandles;
/**
* Constructor for internal use only. Could be exposed for testing purposes in the future.
* Users should access this class via {@link Lifecycle}.
*/
private SoundTriggerMiddlewareService(
@NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) {
mDelegate = delegate;
mContext = context;
}
/**
* Generic exception handling for exceptions thrown by the underlying implementation.
*
* Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed
* by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError}
* (<b>not</b> passed by Binder to the caller).
* <p>
* Typical usage:
* <code><pre>
* try {
* ... Do server operations ...
* } catch (Exception e) {
* throw handleException(e);
* }
* </pre></code>
*/
private static @NonNull
RuntimeException handleException(@NonNull Exception e) {
if (e instanceof RecoverableException) {
throw new ServiceSpecificException(((RecoverableException) e).errorCode,
e.getMessage());
}
throw new InternalServerError(e);
private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareService delegate) {
mDelegate = Objects.requireNonNull(delegate);
}
@Override
public @NonNull
SoundTriggerModuleDescriptor[] listModules() {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation (always valid).
// From here on, every exception isn't client's fault.
try {
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
mModuleHandles = new HashSet<>(result.length);
for (SoundTriggerModuleDescriptor desc : result) {
mModuleHandles.add(desc.handle);
}
return result;
} catch (Exception e) {
throw handleException(e);
}
}
SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
return mDelegate.listModules();
}
@Override
public @NonNull
ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) {
// Permission check.
checkPermissions();
// Input validation.
Objects.requireNonNull(callback);
Objects.requireNonNull(callback.asBinder());
synchronized (this) {
// State validation.
if (mModuleHandles == null) {
throw new IllegalStateException(
"Client must call listModules() prior to attaching.");
}
if (!mModuleHandles.contains(handle)) {
throw new IllegalArgumentException("Invalid handle: " + handle);
}
// From here on, every exception isn't client's fault.
try {
ModuleService moduleService = new ModuleService(callback);
moduleService.attach(mDelegate.attach(handle, moduleService));
return moduleService;
} catch (Exception e) {
throw handleException(e);
}
}
ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback)
throws RemoteException {
return new ModuleService(mDelegate.attach(handle, callback));
}
@Override
public void setExternalCaptureState(boolean active) {
// Permission check.
checkPreemptPermissions();
// Input validation (always valid).
public void setExternalCaptureState(boolean active) throws RemoteException {
mDelegate.setExternalCaptureState(active);
}
synchronized (this) {
// State validation (always valid).
// From here on, every exception isn't client's fault.
try {
mDelegate.setExternalCaptureState(active);
} catch (Exception e) {
throw handleException(e);
}
@Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
if (mDelegate instanceof Dumpable) {
((Dumpable) mDelegate).dump(fout);
}
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the right permissions to use this service.
*/
private void checkPermissions() {
enforcePermission(Manifest.permission.RECORD_AUDIO);
enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
}
private final static class ModuleService extends ISoundTriggerModule.Stub {
private final ISoundTriggerModule mDelegate;
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the right permissions to preempt active sound trigger
* sessions.
*/
private void checkPreemptPermissions() {
enforcePermission(Manifest.permission.PREEMPT_SOUND_TRIGGER);
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the given permission.
*
* @param permission The permission to check.
*/
private void enforcePermission(String permission) {
final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
permission);
switch (status) {
case PermissionChecker.PERMISSION_GRANTED:
return;
case PermissionChecker.PERMISSION_HARD_DENIED:
throw new SecurityException(
String.format("Caller must have the %s permission.", permission));
case PermissionChecker.PERMISSION_SOFT_DENIED:
throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
String.format("Caller must have the %s permission.", permission));
default:
throw new InternalServerError(
new RuntimeException("Unexpected perimission check result."));
}
}
/** State of a sound model. */
static class ModelState {
/** Activity state of a sound model. */
enum Activity {
/** Model is loaded, recognition is inactive. */
LOADED,
/** Model is loaded, recognition is active. */
ACTIVE
private ModuleService(ISoundTriggerModule delegate) {
mDelegate = delegate;
}
/** Activity state. */
Activity activityState = Activity.LOADED;
/**
* A map of known parameter support. A missing key means we don't know yet whether the
* parameter is supported. A null value means it is known to not be supported. A non-null
* value indicates the valid value range.
*/
private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
/**
* Check that the given parameter is known to be supported for this model.
*
* @param modelParam The parameter key.
*/
void checkSupported(int modelParam) {
if (!parameterSupport.containsKey(modelParam)) {
throw new IllegalStateException("Parameter has not been checked for support.");
}
ModelParameterRange range = parameterSupport.get(modelParam);
if (range == null) {
throw new IllegalArgumentException("Paramater is not supported.");
}
@Override
public int loadModel(SoundModel model) throws RemoteException {
return mDelegate.loadModel(model);
}
/**
* Check that the given parameter is known to be supported for this model and that the given
* value is a valid value for it.
*
* @param modelParam The parameter key.
* @param value The value.
*/
void checkSupported(int modelParam, int value) {
if (!parameterSupport.containsKey(modelParam)) {
throw new IllegalStateException("Parameter has not been checked for support.");
}
ModelParameterRange range = parameterSupport.get(modelParam);
if (range == null) {
throw new IllegalArgumentException("Paramater is not supported.");
}
Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive,
"value");
@Override
public int loadPhraseModel(PhraseSoundModel model) throws RemoteException {
return mDelegate.loadPhraseModel(model);
}
/**
* Update support state for the given parameter for this model.
*
* @param modelParam The parameter key.
* @param range The parameter value range, or null if not supported.
*/
void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) {
parameterSupport.put(modelParam, range);
@Override
public void unloadModel(int modelHandle) throws RemoteException {
mDelegate.unloadModel(modelHandle);
;
}
@Override
public void startRecognition(int modelHandle, RecognitionConfig config)
throws RemoteException {
mDelegate.startRecognition(modelHandle, config);
}
@Override
public void stopRecognition(int modelHandle) throws RemoteException {
mDelegate.stopRecognition(modelHandle);
}
@Override
public void forceRecognitionEvent(int modelHandle) throws RemoteException {
mDelegate.forceRecognitionEvent(modelHandle);
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value)
throws RemoteException {
mDelegate.setModelParameter(modelHandle, modelParam, value);
}
@Override
public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
return mDelegate.getModelParameter(modelHandle, modelParam);
}
@Override
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
throws RemoteException {
return mDelegate.queryModelParameterSupport(modelHandle, modelParam);
}
@Override
public void detach() throws RemoteException {
mDelegate.detach();
}
}
@@ -355,395 +180,11 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
new SoundTriggerMiddlewareService(
new SoundTriggerMiddlewareImpl(factories,
new AudioSessionProviderImpl()),
getContext()));
}
}
/**
* A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
* mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
*/
private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback,
DeathRecipient {
private final ISoundTriggerCallback mCallback;
private ISoundTriggerModule mDelegate;
private @NonNull Map<Integer, ModelState> mLoadedModels = new HashMap<>();
ModuleService(@NonNull ISoundTriggerCallback callback) {
mCallback = callback;
try {
mCallback.asBinder().linkToDeath(this, 0);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
void attach(@NonNull ISoundTriggerModule delegate) {
mDelegate = delegate;
}
@Override
public int loadModel(@NonNull SoundModel model) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateGenericModel(model);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
// From here on, every exception isn't client's fault.
try {
int handle = mDelegate.loadModel(model);
mLoadedModels.put(handle, new ModelState());
return handle;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validatePhraseModel(model);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
// From here on, every exception isn't client's fault.
try {
int handle = mDelegate.loadPhraseModel(model);
mLoadedModels.put(handle, new ModelState());
return handle;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void unloadModel(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
if (modelState.activityState != ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for unloading: " + modelState.activityState);
}
// From here on, every exception isn't client's fault.
try {
mDelegate.unloadModel(modelHandle);
mLoadedModels.remove(modelHandle);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateRecognitionConfig(config);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
if (modelState.activityState != ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for starting recognition: "
+ modelState.activityState);
}
// From here on, every exception isn't client's fault.
try {
mDelegate.startRecognition(modelHandle, config);
modelState.activityState = ModelState.Activity.ACTIVE;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void stopRecognition(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// stopRecognition is idempotent - no need to check model state.
// From here on, every exception isn't client's fault.
try {
mDelegate.stopRecognition(modelHandle);
modelState.activityState = ModelState.Activity.LOADED;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void forceRecognitionEvent(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// forceRecognitionEvent is idempotent - no need to check model state.
// From here on, every exception isn't client's fault.
try {
mDelegate.forceRecognitionEvent(modelHandle);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
modelState.checkSupported(modelParam, value);
// From here on, every exception isn't client's fault.
try {
mDelegate.setModelParameter(modelHandle, modelParam, value);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public int getModelParameter(int modelHandle, int modelParam) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
modelState.checkSupported(modelParam);
// From here on, every exception isn't client's fault.
try {
return mDelegate.getModelParameter(modelHandle, modelParam);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
@Nullable
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// From here on, every exception isn't client's fault.
try {
ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
modelParam);
modelState.updateParameterSupport(modelParam, result);
return result;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void detach() {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has already been detached.");
}
if (!mLoadedModels.isEmpty()) {
throw new IllegalStateException("Cannot detach while models are loaded.");
}
// From here on, every exception isn't client's fault.
try {
detachInternal();
} catch (Exception e) {
throw handleException(e);
}
}
}
private void detachInternal() {
try {
mDelegate.detach();
mDelegate = null;
mCallback.asBinder().unlinkToDeath(this, 0);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
////////////////////////////////////////////////////////////////////////////////////////////
// Callbacks
@Override
public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
synchronized (this) {
if (event.status != RecognitionStatus.FORCED) {
mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
}
try {
mCallback.onRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) {
synchronized (this) {
if (event.common.status != RecognitionStatus.FORCED) {
mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
}
try {
mCallback.onPhraseRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onRecognitionAvailabilityChange(boolean available) {
synchronized (this) {
try {
mCallback.onRecognitionAvailabilityChange(available);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onModuleDied() {
synchronized (this) {
try {
mCallback.onModuleDied();
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void binderDied() {
// This is called whenever our client process dies.
synchronized (this) {
try {
// Gracefully stop all active recognitions and unload the models.
for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
if (entry.getValue().activityState == ModelState.Activity.ACTIVE) {
mDelegate.stopRecognition(entry.getKey());
}
mDelegate.unloadModel(entry.getKey());
}
// Detach.
detachInternal();
} catch (Exception e) {
throw handleException(e);
}
}
new SoundTriggerMiddlewareLogging(
new SoundTriggerMiddlewareValidation(
new SoundTriggerMiddlewareImpl(factories,
new AudioSessionProviderImpl()),
getContext()))));
}
}
}

View File

@@ -0,0 +1,784 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.soundtrigger_middleware;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ModelParameterRange;
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
import android.media.soundtrigger_middleware.PhraseSoundModel;
import android.media.soundtrigger_middleware.RecognitionConfig;
import android.media.soundtrigger_middleware.RecognitionEvent;
import android.media.soundtrigger_middleware.RecognitionStatus;
import android.media.soundtrigger_middleware.SoundModel;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.media.soundtrigger_middleware.Status;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.Log;
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions and
* correct usage by the client, as well as makes sure that exceptions representing a server
* malfunction do not get sent to the client.
* <p>
* This is intended to extract the non-business logic out of the underlying implementation and thus
* make it easier to maintain each one of those separate aspects. A design trade-off is being made
* here, in that this class would need to essentially eavesdrop on all the client-server
* communication and retain all state known to the client, while the client doesn't necessarily care
* about all of it, and while the server has its own representation of this information. However,
* in this case, this is a small amount of data, and the benefits in code elegance seem worth it.
* There is also some additional cost in employing a simplistic locking mechanism here, but
* following the same line of reasoning, the benefits in code simplicity outweigh it.
* <p>
* Every public method in this class, overriding an interface method, must follow the following
* pattern:
* <code><pre>
* @Override public T method(S arg) {
* // Permission check.
* checkPermissions();
* // Input validation.
* ValidationUtil.validateS(arg);
* synchronized (this) {
* // State validation.
* if (...state is not valid for this call...) {
* throw new IllegalStateException("State is invalid because...");
* }
* // From here on, every exception isn't client's fault.
* try {
* T result = mDelegate.method(arg);
* // Update state.;
* ...
* return result;
* } catch (Exception e) {
* throw handleException(e);
* }
* }
* }
* </pre></code>
* Following this patterns ensures a consistent and rigorous handling of all aspects associated
* with client-server separation.
* <p>
* <b>Exception handling approach:</b><br>
* We make sure all client faults (permissions, argument and state validation) happen first, and
* would throw {@link SecurityException}, {@link IllegalArgumentException}/
* {@link NullPointerException} or {@link
* IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
* will get sent back to the client.<br>
* Once this is done, any subsequent fault is considered a server fault. Only {@link
* RecoverableException}s thrown by the implementation are special-cased: they would get sent back
* to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other
* exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type
* that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level
* exception handler on the server side, typically resulting in rebooting the server.
*
* {@hide}
*/
public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddlewareService, Dumpable {
private static final String TAG = "SoundTriggerMiddlewareValidation";
private final @NonNull ISoundTriggerMiddlewareService mDelegate;
private final @NonNull Context mContext;
private Map<Integer, Set<ModuleService>> mModules;
public SoundTriggerMiddlewareValidation(
@NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) {
mDelegate = delegate;
mContext = context;
}
/**
* Generic exception handling for exceptions thrown by the underlying implementation.
*
* Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed
* by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError}
* (<b>not</b> passed by Binder to the caller).
* <p>
* Typical usage:
* <code><pre>
* try {
* ... Do server operations ...
* } catch (Exception e) {
* throw handleException(e);
* }
* </pre></code>
*/
static @NonNull
RuntimeException handleException(@NonNull Exception e) {
if (e instanceof RecoverableException) {
throw new ServiceSpecificException(((RecoverableException) e).errorCode,
e.getMessage());
}
throw new InternalServerError(e);
}
@Override
public @NonNull
SoundTriggerModuleDescriptor[] listModules() {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation (always valid).
// From here on, every exception isn't client's fault.
try {
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
mModules = new HashMap<>(result.length);
for (SoundTriggerModuleDescriptor desc : result) {
mModules.put(desc.handle, new HashSet<>());
}
return result;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public @NonNull ISoundTriggerModule attach(int handle,
@NonNull ISoundTriggerCallback callback) {
// Permission check.
checkPermissions();
// Input validation.
Objects.requireNonNull(callback);
Objects.requireNonNull(callback.asBinder());
synchronized (this) {
// State validation.
if (mModules == null) {
throw new IllegalStateException(
"Client must call listModules() prior to attaching.");
}
if (!mModules.containsKey(handle)) {
throw new IllegalArgumentException("Invalid handle: " + handle);
}
// From here on, every exception isn't client's fault.
try {
ModuleService moduleService =
new ModuleService(handle, callback);
moduleService.attach(mDelegate.attach(handle, moduleService));
return moduleService;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void setExternalCaptureState(boolean active) {
// Permission check.
checkPreemptPermissions();
// Input validation (always valid).
synchronized (this) {
// State validation (always valid).
// From here on, every exception isn't client's fault.
try {
mDelegate.setExternalCaptureState(active);
} catch (Exception e) {
throw handleException(e);
}
}
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the right permissions to use this service.
*/
void checkPermissions() {
enforcePermission(Manifest.permission.RECORD_AUDIO);
enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the right permissions to preempt active sound trigger
* sessions.
*/
void checkPreemptPermissions() {
enforcePermission(Manifest.permission.PREEMPT_SOUND_TRIGGER);
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the given permission.
*
* @param permission The permission to check.
*/
void enforcePermission(String permission) {
final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
permission);
switch (status) {
case PermissionChecker.PERMISSION_GRANTED:
return;
case PermissionChecker.PERMISSION_HARD_DENIED:
throw new SecurityException(
String.format("Caller must have the %s permission.", permission));
case PermissionChecker.PERMISSION_SOFT_DENIED:
throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
String.format("Caller must have the %s permission.", permission));
default:
throw new InternalServerError(
new RuntimeException("Unexpected perimission check result."));
}
}
@Override
public IBinder asBinder() {
throw new UnsupportedOperationException(
"This implementation is not inteded to be used directly with Binder.");
}
@Override public void dump(PrintWriter pw) {
synchronized (this) {
if (mModules != null) {
for (int handle : mModules.keySet()) {
pw.println("=========================================");
pw.printf("Active sessions for module %d", handle);
pw.println();
pw.println("=========================================");
for (ModuleService session : mModules.get(handle)) {
session.dump(pw);
}
}
}
}
pw.println();
if (mDelegate instanceof Dumpable) {
((Dumpable) mDelegate).dump(pw);
}
}
/** State of a sound model. */
static class ModelState {
/** Activity state of a sound model. */
enum Activity {
/** Model is loaded, recognition is inactive. */
LOADED,
/** Model is loaded, recognition is active. */
ACTIVE
}
/** Activity state. */
Activity activityState = Activity.LOADED;
/**
* A map of known parameter support. A missing key means we don't know yet whether the
* parameter is supported. A null value means it is known to not be supported. A non-null
* value indicates the valid value range.
*/
private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
/**
* Check that the given parameter is known to be supported for this model.
*
* @param modelParam The parameter key.
*/
void checkSupported(int modelParam) {
if (!parameterSupport.containsKey(modelParam)) {
throw new IllegalStateException("Parameter has not been checked for support.");
}
ModelParameterRange range = parameterSupport.get(modelParam);
if (range == null) {
throw new IllegalArgumentException("Paramater is not supported.");
}
}
/**
* Check that the given parameter is known to be supported for this model and that the given
* value is a valid value for it.
*
* @param modelParam The parameter key.
* @param value The value.
*/
void checkSupported(int modelParam, int value) {
if (!parameterSupport.containsKey(modelParam)) {
throw new IllegalStateException("Parameter has not been checked for support.");
}
ModelParameterRange range = parameterSupport.get(modelParam);
if (range == null) {
throw new IllegalArgumentException("Paramater is not supported.");
}
Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive,
"value");
}
/**
* Update support state for the given parameter for this model.
*
* @param modelParam The parameter key.
* @param range The parameter value range, or null if not supported.
*/
void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) {
parameterSupport.put(modelParam, range);
}
}
/**
* A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
* mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
*/
private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback,
IBinder.DeathRecipient {
private final ISoundTriggerCallback mCallback;
private ISoundTriggerModule mDelegate;
private @NonNull Map<Integer, ModelState> mLoadedModels = new HashMap<>();
private final int mHandle;
ModuleService(int handle, @NonNull ISoundTriggerCallback callback) {
mCallback = callback;
mHandle = handle;
try {
mCallback.asBinder().linkToDeath(null, 0);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
void attach(@NonNull ISoundTriggerModule delegate) {
mDelegate = delegate;
mModules.get(mHandle).add(this);
}
@Override
public int loadModel(@NonNull SoundModel model) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateGenericModel(model);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
// From here on, every exception isn't client's fault.
try {
int handle = mDelegate.loadModel(model);
mLoadedModels.put(handle, new ModelState());
return handle;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validatePhraseModel(model);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
// From here on, every exception isn't client's fault.
try {
int handle = mDelegate.loadPhraseModel(model);
mLoadedModels.put(handle, new ModelState());
return handle;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void unloadModel(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
if (modelState.activityState
!= ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for unloading: " + modelState.activityState);
}
// From here on, every exception isn't client's fault.
try {
mDelegate.unloadModel(modelHandle);
mLoadedModels.remove(modelHandle);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateRecognitionConfig(config);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
if (modelState.activityState
!= ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for starting recognition: "
+ modelState.activityState);
}
// From here on, every exception isn't client's fault.
try {
mDelegate.startRecognition(modelHandle, config);
modelState.activityState =
ModelState.Activity.ACTIVE;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void stopRecognition(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// stopRecognition is idempotent - no need to check model state.
// From here on, every exception isn't client's fault.
try {
mDelegate.stopRecognition(modelHandle);
modelState.activityState =
ModelState.Activity.LOADED;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void forceRecognitionEvent(int modelHandle) {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// forceRecognitionEvent is idempotent - no need to check model state.
// From here on, every exception isn't client's fault.
try {
mDelegate.forceRecognitionEvent(modelHandle);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
modelState.checkSupported(modelParam, value);
// From here on, every exception isn't client's fault.
try {
mDelegate.setModelParameter(modelHandle, modelParam, value);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public int getModelParameter(int modelHandle, int modelParam) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
modelState.checkSupported(modelParam);
// From here on, every exception isn't client's fault.
try {
return mDelegate.getModelParameter(modelHandle, modelParam);
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
@Nullable
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
// Permission check.
checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has been detached.");
}
ModelState modelState = mLoadedModels.get(
modelHandle);
if (modelState == null) {
throw new IllegalStateException("Invalid handle: " + modelHandle);
}
// From here on, every exception isn't client's fault.
try {
ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
modelParam);
modelState.updateParameterSupport(modelParam, result);
return result;
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public void detach() {
// Permission check.
checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
// State validation.
if (mDelegate == null) {
throw new IllegalStateException("Module has already been detached.");
}
if (!mLoadedModels.isEmpty()) {
throw new IllegalStateException("Cannot detach while models are loaded.");
}
// From here on, every exception isn't client's fault.
try {
detachInternal();
} catch (Exception e) {
throw handleException(e);
}
}
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
private void detachInternal() {
try {
mDelegate.detach();
mDelegate = null;
mCallback.asBinder().unlinkToDeath(null, 0);
mModules.get(mHandle).remove(this);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
void dump(PrintWriter pw) {
pw.printf("Loaded models for session %s (handle, active)", toString());
pw.println();
pw.println("-------------------------------");
for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
pw.print(entry.getKey());
pw.print('\t');
pw.print(entry.getValue().activityState.name());
pw.println();
}
}
////////////////////////////////////////////////////////////////////////////////////////////
// Callbacks
@Override
public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
synchronized (SoundTriggerMiddlewareValidation.this) {
if (event.status != RecognitionStatus.FORCED) {
mLoadedModels.get(modelHandle).activityState =
ModelState.Activity.LOADED;
}
try {
mCallback.onRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) {
synchronized (SoundTriggerMiddlewareValidation.this) {
if (event.common.status != RecognitionStatus.FORCED) {
mLoadedModels.get(modelHandle).activityState =
ModelState.Activity.LOADED;
}
try {
mCallback.onPhraseRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onRecognitionAvailabilityChange(boolean available) {
synchronized (SoundTriggerMiddlewareValidation.this) {
try {
mCallback.onRecognitionAvailabilityChange(available);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void onModuleDied() {
synchronized (SoundTriggerMiddlewareValidation.this) {
try {
mCallback.onModuleDied();
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
}
@Override
public void binderDied() {
// This is called whenever our client process dies.
synchronized (SoundTriggerMiddlewareValidation.this) {
try {
// Gracefully stop all active recognitions and unload the models.
for (Map.Entry<Integer, ModelState> entry :
mLoadedModels.entrySet()) {
if (entry.getValue().activityState
== ModelState.Activity.ACTIVE) {
mDelegate.stopRecognition(entry.getKey());
}
mDelegate.unloadModel(entry.getKey());
}
// Detach.
detachInternal();
} catch (Exception e) {
throw handleException(e);
}
}
}
}
}

View File

@@ -123,7 +123,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
*/
synchronized @NonNull
ISoundTriggerModule attach(@NonNull ISoundTriggerCallback callback) {
Log.d(TAG, "attach()");
Session session = new Session(callback);
mActiveSessions.add(session);
return session;
@@ -149,8 +148,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
* @param active true iff external capture is active.
*/
synchronized void setExternalCaptureState(boolean active) {
Log.d(TAG, String.format("setExternalCaptureState(active=%b)", active));
if (mProperties.concurrentCapture) {
// If we support concurrent capture, we don't care about any of this.
return;
@@ -235,7 +232,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void detach() {
Log.d(TAG, "detach()");
synchronized (SoundTriggerModule.this) {
if (mCallback == null) {
return;
@@ -247,8 +243,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public int loadModel(@NonNull SoundModel model) {
Log.d(TAG, String.format("loadModel(model=%s)", model));
// We must do this outside the lock, to avoid possible deadlocks with the remote process
// that provides the audio sessions, which may also be calling into us.
SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession =
@@ -276,8 +270,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
Log.d(TAG, String.format("loadPhraseModel(model=%s)", model));
// We must do this outside the lock, to avoid possible deadlocks with the remote process
// that provides the audio sessions, which may also be calling into us.
SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession =
@@ -306,10 +298,7 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void unloadModel(int modelHandle) {
Log.d(TAG, String.format("unloadModel(handle=%d)", modelHandle));
int sessionId;
synchronized (SoundTriggerModule.this) {
checkValid();
sessionId = mLoadedModels.get(modelHandle).unload();
@@ -323,8 +312,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
Log.d(TAG,
String.format("startRecognition(handle=%d, config=%s)", modelHandle, config));
synchronized (SoundTriggerModule.this) {
checkValid();
mLoadedModels.get(modelHandle).startRecognition(config);
@@ -333,7 +320,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void stopRecognition(int modelHandle) {
Log.d(TAG, String.format("stopRecognition(handle=%d)", modelHandle));
synchronized (SoundTriggerModule.this) {
mLoadedModels.get(modelHandle).stopRecognition();
}
@@ -341,7 +327,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void forceRecognitionEvent(int modelHandle) {
Log.d(TAG, String.format("forceRecognitionEvent(handle=%d)", modelHandle));
synchronized (SoundTriggerModule.this) {
checkValid();
mLoadedModels.get(modelHandle).forceRecognitionEvent();
@@ -350,9 +335,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void setModelParameter(int modelHandle, int modelParam, int value) {
Log.d(TAG,
String.format("setModelParameter(handle=%d, param=%d, value=%d)", modelHandle,
modelParam, value));
synchronized (SoundTriggerModule.this) {
checkValid();
mLoadedModels.get(modelHandle).setParameter(modelParam, value);
@@ -361,8 +343,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public int getModelParameter(int modelHandle, int modelParam) {
Log.d(TAG, String.format("getModelParameter(handle=%d, param=%d)", modelHandle,
modelParam));
synchronized (SoundTriggerModule.this) {
checkValid();
return mLoadedModels.get(modelHandle).getParameter(modelParam);
@@ -372,8 +352,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
@Nullable
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
Log.d(TAG, String.format("queryModelParameterSupport(handle=%d, param=%d)", modelHandle,
modelParam));
synchronized (SoundTriggerModule.this) {
checkValid();
return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam);
@@ -584,8 +562,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
public void recognitionCallback(
@NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent,
int cookie) {
Log.d(TAG, String.format("recognitionCallback_2_1(event=%s, cookie=%d)",
recognitionEvent, cookie));
synchronized (SoundTriggerModule.this) {
android.media.soundtrigger_middleware.RecognitionEvent aidlEvent =
ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent);
@@ -608,8 +584,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
public void phraseRecognitionCallback(
@NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent,
int cookie) {
Log.d(TAG, String.format("phraseRecognitionCallback_2_1(event=%s, cookie=%d)",
phraseRecognitionEvent, cookie));
synchronized (SoundTriggerModule.this) {
android.media.soundtrigger_middleware.PhraseRecognitionEvent aidlEvent =
ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent);