diff options
| author | 2020-02-27 18:09:39 +0000 | |
|---|---|---|
| committer | 2020-02-27 18:09:39 +0000 | |
| commit | d050812a30d2b04f6f027989e1b0227f27426238 (patch) | |
| tree | 27e3469cbb2f70094c50d9b7ac799fbe1ce58b13 | |
| parent | 626b24958f3e5a8a078c477a258338174730395b (diff) | |
| parent | 032ad16316815100e252e737aa8f2e14ae0a464e (diff) | |
Merge changes I9f0a440d,I0b734cc7,I510ee307,Ia257021d into rvc-dev
* changes:
Pretty-print parcelables when logging
Add sysdump to SoundTriggerMiddlewareService
SoundTriggerMiddlewareService logging decorator
Refactor SoundTriggerMiddlewareService
6 files changed, 1591 insertions, 657 deletions
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/Dumpable.java b/services/core/java/com/android/server/soundtrigger_middleware/Dumpable.java new file mode 100644 index 000000000000..f9aa009c6e2a --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/Dumpable.java @@ -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); +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ObjectPrinter.java b/services/core/java/com/android/server/soundtrigger_middleware/ObjectPrinter.java new file mode 100644 index 000000000000..7f047f882122 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ObjectPrinter.java @@ -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); + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java new file mode 100644 index 000000000000..fa78cb0931c2 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java @@ -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); + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java index 1ed97becb776..0d8fc76e1bd2 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java @@ -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,637 +62,129 @@ 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). - - 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); - } - } - } - - /** - * 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); - } - - /** - * 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 - } - - /** 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); - } + public void setExternalCaptureState(boolean active) throws RemoteException { + mDelegate.setExternalCaptureState(active); } - /** - * Entry-point to this module: exposes the module as a {@link SystemService}. - */ - public static final class Lifecycle extends SystemService { - public Lifecycle(Context context) { - super(context); - } - - @Override - public void onStart() { - HalFactory[] factories = new HalFactory[]{() -> { - try { - Log.d(TAG, "Connecting to default ISoundTriggerHw"); - return ISoundTriggerHw.getService(true); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - }}; - - publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, - new SoundTriggerMiddlewareService( - new SoundTriggerMiddlewareImpl(factories, - new AudioSessionProviderImpl()), - getContext())); + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + if (mDelegate instanceof Dumpable) { + ((Dumpable) mDelegate).dump(fout); } } - /** - * 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(); - } - } + private final static class ModuleService extends ISoundTriggerModule.Stub { + private final ISoundTriggerModule mDelegate; - void attach(@NonNull ISoundTriggerModule delegate) { + private ModuleService(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); - } - } + public int loadModel(SoundModel model) throws RemoteException { + return mDelegate.loadModel(model); } @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); - } - } + public int loadPhraseModel(PhraseSoundModel model) throws RemoteException { + return mDelegate.loadPhraseModel(model); } @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); - } - } + public void unloadModel(int modelHandle) throws RemoteException { + mDelegate.unloadModel(modelHandle); + ; } @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); - } - } + public void startRecognition(int modelHandle, RecognitionConfig config) + throws RemoteException { + mDelegate.startRecognition(modelHandle, config); } @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); - } - } + public void stopRecognition(int modelHandle) throws RemoteException { + mDelegate.stopRecognition(modelHandle); } @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); - } - } + public void forceRecognitionEvent(int modelHandle) throws RemoteException { + mDelegate.forceRecognitionEvent(modelHandle); } @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); - } - } + public void setModelParameter(int modelHandle, int modelParam, int value) + throws RemoteException { + mDelegate.setModelParameter(modelHandle, modelParam, value); } @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); - } - } + public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { + return mDelegate.getModelParameter(modelHandle, modelParam); } @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); - } - } + public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) + throws RemoteException { + return mDelegate.queryModelParameterSupport(modelHandle, modelParam); } @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); - } - } + public void detach() throws RemoteException { + mDelegate.detach(); } + } - @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); - } - } + /** + * Entry-point to this module: exposes the module as a {@link SystemService}. + */ + public static final class Lifecycle extends SystemService { + public Lifecycle(Context context) { + super(context); } @Override - public void onModuleDied() { - synchronized (this) { + public void onStart() { + HalFactory[] factories = new HalFactory[]{() -> { try { - mCallback.onModuleDied(); + Log.d(TAG, "Connecting to default ISoundTriggerHw"); + return ISoundTriggerHw.getService(true); } 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); + throw e.rethrowAsRuntimeException(); } - } - } + }}; - @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); - } - } + publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, + new SoundTriggerMiddlewareService( + new SoundTriggerMiddlewareLogging( + new SoundTriggerMiddlewareValidation( + new SoundTriggerMiddlewareImpl(factories, + new AudioSessionProviderImpl()), + getContext())))); } } } diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java new file mode 100644 index 000000000000..c45f37dfdbd8 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java @@ -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); + } + } + } + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index aa1558ebfc70..d6390184e3bd 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -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); |