diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 80f35a0a2e320..eb138fcb8f1d2 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -16,6 +16,9 @@ package android.hardware.soundtrigger; +import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; +import static android.Manifest.permission.RECORD_AUDIO; +import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY; import static android.system.OsConstants.EINVAL; import static android.system.OsConstants.ENODEV; import static android.system.OsConstants.ENOSYS; @@ -27,6 +30,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; @@ -34,9 +38,13 @@ import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.media.AudioFormat; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.Identity; +import android.media.permission.SafeCloseable; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; import android.media.soundtrigger_middleware.Status; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -1943,16 +1951,6 @@ public class SoundTrigger { public static final int SERVICE_STATE_DISABLED = 1; private static Object mServiceLock = new Object(); private static ISoundTriggerMiddlewareService mService; - /** - * @return returns current package name. - */ - static String getCurrentOpPackageName() { - String packageName = ActivityThread.currentOpPackageName(); - if (packageName == null) { - return ""; - } - return packageName; - } /** * Translate an exception thrown from interaction with the underlying service to an error code. @@ -2005,23 +2003,103 @@ public class SoundTrigger { * - {@link #STATUS_BAD_VALUE} if modules is null * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails * + * @deprecated Please use {@link #listModulesAsOriginator(ArrayList, Identity)} or + * {@link #listModulesAsMiddleman(ArrayList, Identity, Identity)}, based on whether the + * client is acting on behalf of its own identity or a separate identity. * @hide */ @UnsupportedAppUsage public static int listModules(@NonNull ArrayList modules) { + // TODO(ytai): This is a temporary hack to retain prior behavior, which makes + // assumptions about process affinity and Binder context, namely that the binder calling ID + // reliably reflects the originator identity. + Identity middlemanIdentity = new Identity(); + middlemanIdentity.packageName = ActivityThread.currentOpPackageName(); + + Identity originatorIdentity = new Identity(); + originatorIdentity.pid = Binder.getCallingPid(); + originatorIdentity.uid = Binder.getCallingUid(); + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return listModulesAsMiddleman(modules, middlemanIdentity, originatorIdentity); + } + } + + /** + * Returns a list of descriptors for all hardware modules loaded. + * This variant is intended for use when the caller itself is the originator of the operation. + * @param modules A ModuleProperties array where the list will be returned. + * @param originatorIdentity The identity of the originator, which will be used for permission + * purposes. + * @return - {@link #STATUS_OK} in case of success + * - {@link #STATUS_ERROR} in case of unspecified error + * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission + * - {@link #STATUS_NO_INIT} if the native service cannot be reached + * - {@link #STATUS_BAD_VALUE} if modules is null + * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails + * + * @hide + */ + @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + public static int listModulesAsOriginator(@NonNull ArrayList modules, + @NonNull Identity originatorIdentity) { try { - SoundTriggerModuleDescriptor[] descs = getService().listModules(); - modules.clear(); - modules.ensureCapacity(descs.length); - for (SoundTriggerModuleDescriptor desc : descs) { - modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc)); - } + SoundTriggerModuleDescriptor[] descs = getService().listModulesAsOriginator( + originatorIdentity); + convertDescriptorsToModuleProperties(descs, modules); return STATUS_OK; } catch (Exception e) { return handleException(e); } } + /** + * Returns a list of descriptors for all hardware modules loaded. + * This variant is intended for use when the caller is acting on behalf of a different identity + * for permission purposes. + * @param modules A ModuleProperties array where the list will be returned. + * @param middlemanIdentity The identity of the caller, acting as middleman. + * @param originatorIdentity The identity of the originator, which will be used for permission + * purposes. + * @return - {@link #STATUS_OK} in case of success + * - {@link #STATUS_ERROR} in case of unspecified error + * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission + * - {@link #STATUS_NO_INIT} if the native service cannot be reached + * - {@link #STATUS_BAD_VALUE} if modules is null + * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails + * + * @hide + */ + @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY) + public static int listModulesAsMiddleman(@NonNull ArrayList modules, + @NonNull Identity middlemanIdentity, + @NonNull Identity originatorIdentity) { + try { + SoundTriggerModuleDescriptor[] descs = getService().listModulesAsMiddleman( + middlemanIdentity, originatorIdentity); + convertDescriptorsToModuleProperties(descs, modules); + return STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } + + /** + * Converts an array of SoundTriggerModuleDescriptor into an (existing) ArrayList of + * ModuleProperties. + * @param descsIn The input descriptors. + * @param modulesOut The output list. + */ + private static void convertDescriptorsToModuleProperties( + @NonNull SoundTriggerModuleDescriptor[] descsIn, + @NonNull ArrayList modulesOut) { + modulesOut.clear(); + modulesOut.ensureCapacity(descsIn.length); + for (SoundTriggerModuleDescriptor desc : descsIn) { + modulesOut.add(ConversionUtil.aidl2apiModuleDescriptor(desc)); + } + } + /** * Get an interface on a hardware module to control sound models and recognition on * this module. @@ -2031,15 +2109,85 @@ public class SoundTrigger { * is OK. * @return a valid sound module in case of success or null in case of error. * + * @deprecated Please use + * {@link #attachModuleAsOriginator(int, StatusListener, Handler, Identity)} or + * {@link #attachModuleAsMiddleman(int, StatusListener, Handler, Identity, Identity)}, based + * on whether the client is acting on behalf of its own identity or a separate identity. * @hide */ @UnsupportedAppUsage - public static @NonNull SoundTriggerModule attachModule(int moduleId, + public static SoundTriggerModule attachModule(int moduleId, @NonNull StatusListener listener, @Nullable Handler handler) { + // TODO(ytai): This is a temporary hack to retain prior behavior, which makes + // assumptions about process affinity and Binder context, namely that the binder calling ID + // reliably reflects the originator identity. + Identity middlemanIdentity = new Identity(); + middlemanIdentity.packageName = ActivityThread.currentOpPackageName(); + + Identity originatorIdentity = new Identity(); + originatorIdentity.pid = Binder.getCallingPid(); + originatorIdentity.uid = Binder.getCallingUid(); + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return attachModuleAsMiddleman(moduleId, listener, handler, middlemanIdentity, + originatorIdentity); + } + } + + /** + * Get an interface on a hardware module to control sound models and recognition on + * this module. + * This variant is intended for use when the caller is acting on behalf of a different identity + * for permission purposes. + * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory. + * @param listener {@link StatusListener} interface. Mandatory. + * @param handler the Handler that will receive the callabcks. Can be null if default handler + * is OK. + * @param middlemanIdentity The identity of the caller, acting as middleman. + * @param originatorIdentity The identity of the originator, which will be used for permission + * purposes. + * @return a valid sound module in case of success or null in case of error. + * + * @hide + */ + @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY) + public static SoundTriggerModule attachModuleAsMiddleman(int moduleId, + @NonNull SoundTrigger.StatusListener listener, + @Nullable Handler handler, Identity middlemanIdentity, + Identity originatorIdentity) { Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); try { - return new SoundTriggerModule(getService(), moduleId, listener, looper); + return new SoundTriggerModule(getService(), moduleId, listener, looper, + middlemanIdentity, originatorIdentity); + } catch (Exception e) { + Log.e(TAG, "", e); + return null; + } + } + + /** + * Get an interface on a hardware module to control sound models and recognition on + * this module. + * This variant is intended for use when the caller itself is the originator of the operation. + * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory. + * @param listener {@link StatusListener} interface. Mandatory. + * @param handler the Handler that will receive the callabcks. Can be null if default handler + * is OK. + * @param originatorIdentity The identity of the originator, which will be used for permission + * purposes. + * @return a valid sound module in case of success or null in case of error. + * + * @hide + */ + @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + public static SoundTriggerModule attachModuleAsOriginator(int moduleId, + @NonNull SoundTrigger.StatusListener listener, + @Nullable Handler handler, @NonNull Identity originatorIdentity) { + Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); + try { + return new SoundTriggerModule(getService(), moduleId, listener, looper, + originatorIdentity); } catch (Exception e) { Log.e(TAG, "", e); return null; diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java index a2a15b30d5787..05823bf14b636 100644 --- a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java +++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java @@ -19,6 +19,9 @@ package android.hardware.soundtrigger; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.Identity; +import android.media.permission.SafeCloseable; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; @@ -50,12 +53,39 @@ public class SoundTriggerModule { private EventHandlerDelegate mEventHandlerDelegate; private ISoundTriggerModule mService; + /** + * This variant is intended for use when the caller is acting an originator, rather than on + * behalf of a different entity, as far as authorization goes. + */ SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service, - int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper) + int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper, + @NonNull Identity originatorIdentity) throws RemoteException { mId = moduleId; mEventHandlerDelegate = new EventHandlerDelegate(listener, looper); - mService = service.attach(moduleId, mEventHandlerDelegate); + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mService = service.attachAsOriginator(moduleId, originatorIdentity, + mEventHandlerDelegate); + } + mService.asBinder().linkToDeath(mEventHandlerDelegate, 0); + } + + /** + * This variant is intended for use when the caller is acting as a middleman, i.e. on behalf of + * a different entity, as far as authorization goes. + */ + SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service, + int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper, + @NonNull Identity middlemanIdentity, @NonNull Identity originatorIdentity) + throws RemoteException { + mId = moduleId; + mEventHandlerDelegate = new EventHandlerDelegate(listener, looper); + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mService = service.attachAsMiddleman(moduleId, middlemanIdentity, originatorIdentity, + mEventHandlerDelegate); + } mService.asBinder().linkToDeath(mEventHandlerDelegate, 0); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 560e3c11d2f45..4082b309d8618 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3972,6 +3972,15 @@ + + + diff --git a/media/Android.bp b/media/Android.bp index 0ed10472561d4..8895b3a9a2ba7 100644 --- a/media/Android.bp +++ b/media/Android.bp @@ -22,6 +22,25 @@ aidl_interface { }, } +aidl_interface { + name: "media_permission-aidl", + unstable: true, + local_include_dir: "java", + srcs: [ + "java/android/media/permission/Identity.aidl", + ], + backend: + { + cpp: { + enabled: true, + }, + java: { + // Already generated as part of the entire media java library. + enabled: false, + }, + }, +} + aidl_interface { name: "soundtrigger_middleware-aidl", unstable: true, @@ -61,5 +80,8 @@ aidl_interface { enabled: false, }, }, - imports: [ "audio_common-aidl" ], + imports: [ + "audio_common-aidl", + "media_permission-aidl", + ], } diff --git a/media/java/android/media/permission/ClearCallingIdentityContext.java b/media/java/android/media/permission/ClearCallingIdentityContext.java new file mode 100644 index 0000000000000..364a2e800afe4 --- /dev/null +++ b/media/java/android/media/permission/ClearCallingIdentityContext.java @@ -0,0 +1,58 @@ +/* + * 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 android.media.permission; + +import android.annotation.NonNull; +import android.os.Binder; + +/** + * An RAII-style object, used to establish a scope in which the binder calling identity is cleared. + * + *

+ * Intended usage: + *

+ * void caller() {
+ *   try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ *       // Within this scope the binder calling identity is cleared.
+ *       ...
+ *   }
+ *   // Outside the scope the calling identity is restored to its prior state.
+ * 
+ * + * @hide + */ +public class ClearCallingIdentityContext implements SafeCloseable { + private final long mRestoreKey; + + /** + * Creates a new instance. + * @return A {@link SafeCloseable}, intended to be used in a try-with-resource block. + */ + public static @NonNull + SafeCloseable create() { + return new ClearCallingIdentityContext(); + } + + private ClearCallingIdentityContext() { + mRestoreKey = Binder.clearCallingIdentity(); + } + + @Override + public void close() { + Binder.restoreCallingIdentity(mRestoreKey); + } +} diff --git a/media/java/android/media/permission/CompositeSafeCloseable.java b/media/java/android/media/permission/CompositeSafeCloseable.java new file mode 100644 index 0000000000000..08990ebd5057c --- /dev/null +++ b/media/java/android/media/permission/CompositeSafeCloseable.java @@ -0,0 +1,40 @@ +/* + * 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 android.media.permission; + +import android.annotation.NonNull; + +/** + * A composite {@link SafeCloseable}. Will close its children in reverse order. + * + * @hide + */ +class CompositeSafeCloseable implements SafeCloseable { + private final @NonNull SafeCloseable[] mChildren; + + CompositeSafeCloseable(@NonNull SafeCloseable... children) { + mChildren = children; + } + + @Override + public void close() { + // Close in reverse order. + for (int i = mChildren.length - 1; i >= 0; --i) { + mChildren[i].close(); + } + } +} diff --git a/media/java/android/media/permission/Identity.aidl b/media/java/android/media/permission/Identity.aidl new file mode 100644 index 0000000000000..361497d59ea94 --- /dev/null +++ b/media/java/android/media/permission/Identity.aidl @@ -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 android.media.permission; + +/** + * A collection of identity-related information, required for permission enforcement. + * + * {@hide} + */ +parcelable Identity { + /** Linux user ID. */ + int uid; + /** Linux process ID. */ + int pid; + /** Package name. If null, the first package owned by the given uid will be assumed. */ + @nullable String packageName; + /** Attribution tag. Mostly used for diagnostic purposes. */ + @nullable String attributionTag; +} diff --git a/media/java/android/media/permission/IdentityContext.java b/media/java/android/media/permission/IdentityContext.java new file mode 100644 index 0000000000000..d10654fbe6844 --- /dev/null +++ b/media/java/android/media/permission/IdentityContext.java @@ -0,0 +1,100 @@ +/* + * 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 android.media.permission; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +/** + * An RAII-style object, used to establish a scope in which a single identity is part of the + * context. This is used in order to avoid having to explicitly pass identity information through + * deep call-stacks. + *

+ * Intended usage: + *

+ * void caller() {
+ *   Identity originator = ...;
+ *   try (SafeCloseable ignored = IdentityContext.create(originator)) {
+ *       // Within this scope the context is established.
+ *       callee();
+ *   }
+ *   // Outside the scope the context is restored to its prior state.
+ *
+ * void callee() {
+ *     // Here we can access the identity without having to explicitly take it as an argument.
+ *     // This is true even if this were a deeply nested call.
+ *     Identity originator = IdentityContext.getNonNull();
+ *     ...
+ * }
+ * 
+ * + * @hide + */ +public class IdentityContext implements SafeCloseable { + private static ThreadLocal sThreadLocalIdentity = new ThreadLocal<>(); + private @Nullable Identity mPrior = get(); + + /** + * Create a scoped identity context. + * + * @param identity The identity to establish with the scope. + * @return A {@link SafeCloseable}, to be used in a try-with-resources block to establish a + * scope. + */ + public static @NonNull + SafeCloseable create(@Nullable Identity identity) { + return new IdentityContext(identity); + } + + /** + * Get the current identity context. + * + * @return The identity, or null if it has not been established. + */ + public static @Nullable + Identity get() { + return sThreadLocalIdentity.get(); + } + + /** + * Get the current identity context. Throws a {@link NullPointerException} if it has not been + * established. + * + * @return The identity. + */ + public static @NonNull + Identity getNonNull() { + Identity result = get(); + if (result == null) { + throw new NullPointerException("Identity context is not set"); + } + return result; + } + + private IdentityContext(@Nullable Identity identity) { + set(identity); + } + + @Override + public void close() { + set(mPrior); + } + + private static void set(@Nullable Identity identity) { + sThreadLocalIdentity.set(identity); + } +} diff --git a/media/java/android/media/permission/PermissionUtil.java b/media/java/android/media/permission/PermissionUtil.java new file mode 100644 index 0000000000000..458f11243f68b --- /dev/null +++ b/media/java/android/media/permission/PermissionUtil.java @@ -0,0 +1,250 @@ +/* + * 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 android.media.permission; + +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.PermissionChecker; +import android.content.pm.PackageManager; +import android.os.Binder; + +import java.util.Objects; + +/** + * This module provides some utility methods for facilitating our permission enforcement patterns. + *

+ *

Intended usage:

+ * Close to the client-facing edge of the server, first authenticate the client, using {@link + * #establishIdentityDirect(Identity)}, or {@link #establishIdentityIndirect(Context, String, + * Identity, Identity)}, depending on whether the client is trying to authenticate as the + * originator or a middleman. Those methods will establish a scope with the originator in the + * {@link android.media.permission.IdentityContext} and a cleared binder calling identity. + * Typically there would be two distinct API methods for the two different options, and typically + * those API methods would be used to establish a client session which is associated with the + * originator for the lifetime of the session. + *

+ * When performing an operation that requires permissions, use {@link + * #checkPermissionForPreflight(Context, Identity, String)} or {@link + * #checkPermissionForDataDelivery(Context, Identity, String, String)} on the originator + * identity. Note that this won't typically be the identity pulled from the {@link + * android.media.permission.IdentityContext}, since we are working with a session-based approach, + * the originator identity will be established once upon creation of a session, and then all + * interactions with this session will using the identity attached to the session. This also covers + * performing checks prior to invoking client callbacks for data delivery. + * + * @hide + */ +public class PermissionUtil { + /** + * Authenticate an originator, where the binder call is coming from a middleman. + * + * The middleman is expected to hold a special permission to act as such, or else a + * {@link SecurityException} will be thrown. If the call succeeds: + *

    + *
  • The passed middlemanIdentity argument will have its uid/pid fields overridden with + * those provided by binder. + *
  • An {@link SafeCloseable} is returned, used to established a scope in which the + * originator identity is available via {@link android.media.permission.IdentityContext} + * and in which the binder + * calling ID is cleared. + *
+ * Example usage: + *
+     *     try (SafeCloseable ignored = PermissionUtil.establishIdentityIndirect(...)) {
+     *         // Within this scope we have the identity context established, and the binder calling
+     *         // identity cleared.
+     *         ...
+     *         Identity originator = IdentityContext.getNonNull();
+     *         ...
+     *     }
+     *     // outside the scope, everything is back to the prior state.
+     * 
+ *

+ * Important note: The binder calling ID will be used to securely establish the identity + * of the middleman. However, if the middleman is on the same process as the server, + * the middleman must remember to clear the binder calling identity, or else the binder calling + * ID will reflect the process calling into the middleman, not the middleman process itself. If + * the middleman itself is using this API, this is typically not an issue, since this method + * will take care of that. + * + * @param context A {@link Context}, used for permission checks. + * @param middlemanPermission The permission that will be checked in order to authorize the + * middleman to act as such (i.e. be trusted to convey the + * originator + * identity reliably). + * @param middlemanIdentity The identity of the middleman. + * @param originatorIdentity The identity of the originator. + * @return A {@link SafeCloseable}, used to establish a scope, as mentioned above. + */ + public static @NonNull + SafeCloseable establishIdentityIndirect( + @NonNull Context context, + @NonNull String middlemanPermission, + @NonNull Identity middlemanIdentity, + @NonNull Identity originatorIdentity) { + Objects.requireNonNull(context); + Objects.requireNonNull(middlemanPermission); + Objects.requireNonNull(middlemanIdentity); + Objects.requireNonNull(originatorIdentity); + + // Override uid/pid with the secure values provided by binder. + middlemanIdentity.pid = Binder.getCallingPid(); + middlemanIdentity.uid = Binder.getCallingUid(); + + // Authorize middleman to delegate identity. + context.enforcePermission(middlemanPermission, middlemanIdentity.pid, + middlemanIdentity.uid, + String.format("Middleman must have the %s permision.", middlemanPermission)); + return new CompositeSafeCloseable(IdentityContext.create(originatorIdentity), + ClearCallingIdentityContext.create()); + } + + /** + * Authenticate an originator, where the binder call is coming directly from the originator. + * + * If the call succeeds: + *

    + *
  • The passed originatorIdentity argument will have its uid/pid fields overridden with + * those provided by binder. + *
  • A {@link SafeCloseable} is returned, used to established a scope in which the + * originator identity is available via {@link IdentityContext} and in which the binder + * calling ID is cleared. + *
+ * Example usage: + *
+     *     try (AutoClosable ignored = PermissionUtil.establishIdentityDirect(...)) {
+     *         // Within this scope we have the identity context established, and the binder calling
+     *         // identity cleared.
+     *         ...
+     *         Identity originator = IdentityContext.getNonNull();
+     *         ...
+     *     }
+     *     // outside the scope, everything is back to the prior state.
+     * 
+ *

+ * Important note: The binder calling ID will be used to securely establish the identity + * of the client. However, if the client is on the same process as the server, and is itself a + * binder server, it must remember to clear the binder calling identity, or else the binder + * calling ID will reflect the process calling into the client, not the client process itself. + * If the client itself is using this API, this is typically not an issue, since this method + * will take care of that. + * + * @param originatorIdentity The identity of the originator. + * @return A {@link SafeCloseable}, used to establish a scope, as mentioned above. + */ + public static @NonNull + SafeCloseable establishIdentityDirect(@NonNull Identity originatorIdentity) { + Objects.requireNonNull(originatorIdentity); + + originatorIdentity.uid = Binder.getCallingUid(); + originatorIdentity.pid = Binder.getCallingPid(); + return new CompositeSafeCloseable( + IdentityContext.create(originatorIdentity), + ClearCallingIdentityContext.create()); + } + + /** + * Checks whether the given identity has the given permission to receive data. + * + * @param context A {@link Context}, used for permission checks. + * @param identity The identity to check. + * @param permission The identifier of the permission we want to check. + * @param reason The reason why we're requesting the permission, for auditing purposes. + * @return The permission check result which is either + * {@link PermissionChecker#PERMISSION_GRANTED} + * or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or + * {@link PermissionChecker#PERMISSION_HARD_DENIED}. + */ + public static int checkPermissionForDataDelivery(@NonNull Context context, + @NonNull Identity identity, + @NonNull String permission, + @NonNull String reason) { + return PermissionChecker.checkPermissionForDataDelivery(context, permission, + identity.pid, identity.uid, identity.packageName, identity.attributionTag, + reason); + } + + /** + * Checks whether the given identity has the given permission to receive data. + * + * This variant ignores the proc-state for the sake of the check, i.e. overrides any + * restrictions that apply specifically to apps running in the background. + * + * TODO(ytai): This is a temporary hack until we have permissions that are specifically intended + * for background microphone access. + * + * @param context A {@link Context}, used for permission checks. + * @param identity The identity to check. + * @param permission The identifier of the permission we want to check. + * @param reason The reason why we're requesting the permission, for auditing purposes. + * @return The permission check result which is either + * {@link PermissionChecker#PERMISSION_GRANTED} + * or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or + * {@link PermissionChecker#PERMISSION_HARD_DENIED}. + */ + public static int checkPermissionForDataDeliveryIgnoreProcState(@NonNull Context context, + @NonNull Identity identity, + @NonNull String permission, + @NonNull String reason) { + if (context.checkPermission(permission, identity.pid, identity.uid) + != PackageManager.PERMISSION_GRANTED) { + return PermissionChecker.PERMISSION_HARD_DENIED; + } + + String appOpOfPermission = AppOpsManager.permissionToOp(permission); + if (appOpOfPermission == null) { + // not platform defined + return PermissionChecker.PERMISSION_GRANTED; + } + + String packageName = identity.packageName; + if (packageName == null) { + String[] packageNames = context.getPackageManager().getPackagesForUid(identity.uid); + if (packageNames != null && packageNames.length > 0) { + packageName = packageNames[0]; + } + } + + final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); + + int appOpMode = appOpsManager.unsafeCheckOpRawNoThrow(appOpOfPermission, identity.uid, + packageName); + if (appOpMode == AppOpsManager.MODE_ALLOWED) { + return PermissionChecker.PERMISSION_GRANTED; + } + return PermissionChecker.PERMISSION_SOFT_DENIED; + } + + /** + * Checks whether the given identity has the given permission. + * + * @param context A {@link Context}, used for permission checks. + * @param identity The identity to check. + * @param permission The identifier of the permission we want to check. + * @return The permission check result which is either + * {@link PermissionChecker#PERMISSION_GRANTED} + * or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or + * {@link PermissionChecker#PERMISSION_HARD_DENIED}. + */ + public static int checkPermissionForPreflight(@NonNull Context context, + @NonNull Identity identity, + @NonNull String permission) { + return PermissionChecker.checkPermissionForPreflight(context, permission, + identity.pid, identity.uid, identity.packageName); + } +} diff --git a/media/java/android/media/permission/SafeCloseable.java b/media/java/android/media/permission/SafeCloseable.java new file mode 100644 index 0000000000000..8ec15b18bd37d --- /dev/null +++ b/media/java/android/media/permission/SafeCloseable.java @@ -0,0 +1,27 @@ +/* + * 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 android.media.permission; + +/** + * An {@link AutoCloseable} that doesn't throw on {@link #close()}. + * + * @hide + */ +public interface SafeCloseable extends AutoCloseable { + @Override + void close(); +} diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl index 06c39071cdf50..d1126b9006e02 100644 --- a/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl @@ -15,6 +15,7 @@ */ package android.media.soundtrigger_middleware; +import android.media.permission.Identity; import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; @@ -30,13 +31,59 @@ import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; interface ISoundTriggerMiddlewareService { /** * Query the available modules and their capabilities. + * + * This variant is intended for use by the originator of the operations for permission + * enforcement purposes. The provided identity's uid/pid fields will be ignored and overridden + * by the ones provided by Binder.getCallingUid() / Binder.getCallingPid(). */ - SoundTriggerModuleDescriptor[] listModules(); + SoundTriggerModuleDescriptor[] listModulesAsOriginator(in Identity identity); + + /** + * Query the available modules and their capabilities. + * + * This variant is intended for use by a trusted "middleman", acting on behalf of some identity + * other than itself. The caller must provide: + * - Its own identity, which will be used to establish trust via the + * SOUNDTRIGGER_DELEGATE_IDENTITY permission. This identity's uid/pid fields will be ignored + * and overridden by the ones provided by Binder.getCallingUid() / Binder.getCallingPid(). + * This implies that the caller must clear its caller identity to protect from the case where + * it resides in the same process as the callee. + * - The identity of the entity on behalf of which module operations are to be performed. + */ + SoundTriggerModuleDescriptor[] listModulesAsMiddleman(in Identity middlemanIdentity, + in Identity originatorIdentity); /** * Attach to one of the available modules. + * + * This variant is intended for use by the originator of the operations for permission + * enforcement purposes. The provided identity's uid/pid fields will be ignored and overridden + * by the ones provided by Binder.getCallingUid() / Binder.getCallingPid(). + * * listModules() must be called prior to calling this method and the provided handle must be * one of the handles from the returned list. */ - ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback); -} \ No newline at end of file + ISoundTriggerModule attachAsOriginator(int handle, + in Identity identity, + ISoundTriggerCallback callback); + + /** + * Attach to one of the available modules. + * + * This variant is intended for use by a trusted "middleman", acting on behalf of some identity + * other than itself. The caller must provide: + * - Its own identity, which will be used to establish trust via the + * SOUNDTRIGGER_DELEGATE_IDENTITY permission. This identity's uid/pid fields will be ignored + * and overridden by the ones provided by Binder.getCallingUid() / Binder.getCallingPid(). + * This implies that the caller must clear its caller identity to protect from the case where + * it resides in the same process as the callee. + * - The identity of the entity on behalf of which module operations are to be performed. + * + * listModules() must be called prior to calling this method and the provided handle must be + * one of the handles from the returned list. + */ + ISoundTriggerModule attachAsMiddleman(int handle, + in Identity middlemanIdentity, + in Identity originatorIdentity, + ISoundTriggerCallback callback); +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java index 5def7621c1480..a90053a23deaa 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java @@ -17,11 +17,28 @@ package com.android.server.soundtrigger_middleware; import android.media.ICaptureStateListener; -import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; /** - * This interface unifies ISoundTriggerMiddlewareService with ICaptureStateListener. + * This interface unifies methods from ISoundTriggerMiddlewareService and ICaptureStateListener. + * + * The ISoundTriggerMiddlewareService have been modified to exclude identity information and the + * RemoteException signature, both of which are only relevant at the service boundary layer. */ -public interface ISoundTriggerMiddlewareInternal extends ISoundTriggerMiddlewareService, - ICaptureStateListener { +public interface ISoundTriggerMiddlewareInternal extends ICaptureStateListener { + /** + * Query the available modules and their capabilities. + */ + public SoundTriggerModuleDescriptor[] listModules(); + + /** + * Attach to one of the available modules. + * + * listModules() must be called prior to calling this method and the provided handle must be + * one of the handles from the returned list. + */ + public ISoundTriggerModule attach(int handle, + ISoundTriggerCallback callback); } diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java index 8b6ed1ff50813..2ef0759719fc7 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java @@ -18,8 +18,9 @@ package com.android.server.soundtrigger_middleware; import android.annotation.NonNull; import android.annotation.Nullable; +import android.media.permission.Identity; +import android.media.permission.IdentityContext; 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; @@ -28,17 +29,15 @@ 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; +import java.util.Objects; /** * An ISoundTriggerMiddlewareService decorator, which adds logging of all API calls (and @@ -71,7 +70,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt } @Override - public @NonNull SoundTriggerModuleDescriptor[] listModules() throws RemoteException { + public @NonNull + SoundTriggerModuleDescriptor[] listModules() { try { SoundTriggerModuleDescriptor[] result = mDelegate.listModules(); logReturn("listModules", result); @@ -83,12 +83,13 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt } @Override - public @NonNull ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback) - throws RemoteException { + public @NonNull + ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback) { try { - ISoundTriggerModule result = mDelegate.attach(handle, new CallbackLogging(callback)); + ModuleLogging result = new ModuleLogging(callback); + result.attach(mDelegate.attach(handle, result.getCallbackWrapper())); logReturn("attach", result, handle, callback); - return new ModuleLogging(result); + return result; } catch (Exception e) { logException("attach", e, handle, callback); throw e; @@ -106,7 +107,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt } } - @Override public IBinder asBinder() { + @Override + public IBinder asBinder() { throw new UnsupportedOperationException( "This implementation is not inteded to be used directly with Binder."); } @@ -118,96 +120,35 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt } private void logException(String methodName, Exception ex, Object... args) { - logExceptionWithObject(this, methodName, ex, args); + logExceptionWithObject(this, IdentityContext.get(), methodName, ex, args); } private void logReturn(String methodName, Object retVal, Object... args) { - logReturnWithObject(this, methodName, retVal, args); + logReturnWithObject(this, IdentityContext.get(), 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(); - } + logVoidReturnWithObject(this, IdentityContext.get(), methodName, args); } private class ModuleLogging implements ISoundTriggerModule { - private final ISoundTriggerModule mDelegate; + private ISoundTriggerModule mDelegate; + private final @NonNull CallbackLogging mCallbackWrapper; + private final @NonNull Identity mOriginatorIdentity; - private ModuleLogging(ISoundTriggerModule delegate) { + ModuleLogging(@NonNull ISoundTriggerCallback callback) { + mCallbackWrapper = new CallbackLogging(callback); + mOriginatorIdentity = IdentityContext.getNonNull(); + } + + void attach(@NonNull ISoundTriggerModule delegate) { mDelegate = delegate; } + ISoundTriggerCallback getCallbackWrapper() { + return mCallbackWrapper; + } + @Override public int loadModel(SoundModel model) throws RemoteException { try { @@ -334,19 +275,92 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt // Override toString() in order to have the delegate's ID in it. @Override public String toString() { - return mDelegate.toString(); + return Objects.toString(mDelegate); } private void logException(String methodName, Exception ex, Object... args) { - logExceptionWithObject(this, methodName, ex, args); + logExceptionWithObject(this, mOriginatorIdentity, methodName, ex, args); } private void logReturn(String methodName, Object retVal, Object... args) { - logReturnWithObject(this, methodName, retVal, args); + logReturnWithObject(this, mOriginatorIdentity, methodName, retVal, args); } private void logVoidReturn(String methodName, Object... args) { - logVoidReturnWithObject(this, methodName, args); + logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args); + } + + private class CallbackLogging implements ISoundTriggerCallback { + private final ISoundTriggerCallback mCallbackDelegate; + + private CallbackLogging(ISoundTriggerCallback delegate) { + mCallbackDelegate = delegate; + } + + @Override + public void onRecognition(int modelHandle, RecognitionEvent event) + throws RemoteException { + try { + mCallbackDelegate.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 { + mCallbackDelegate.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 { + mCallbackDelegate.onRecognitionAvailabilityChange(available); + logVoidReturn("onRecognitionAvailabilityChange", available); + } catch (Exception e) { + logException("onRecognitionAvailabilityChange", e, available); + throw e; + } + } + + @Override + public void onModuleDied() throws RemoteException { + try { + mCallbackDelegate.onModuleDied(); + logVoidReturn("onModuleDied"); + } catch (Exception e) { + logException("onModuleDied", e); + throw e; + } + } + + private void logException(String methodName, Exception ex, Object... args) { + logExceptionWithObject(this, mOriginatorIdentity, methodName, ex, args); + } + + private void logVoidReturn(String methodName, Object... args) { + logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args); + } + + @Override + public IBinder asBinder() { + return mCallbackDelegate.asBinder(); + } + + // Override toString() in order to have the delegate's ID in it. + @Override + public String toString() { + return Objects.toString(mCallbackDelegate); + } } } @@ -386,34 +400,37 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt return builder.toString(); } - private void logReturnWithObject(@NonNull Object object, String methodName, + private void logReturnWithObject(@NonNull Object object, @Nullable Identity originatorIdentity, + String methodName, @Nullable Object retVal, @NonNull Object[] args) { - final String message = String.format("%s[this=%s, caller=%d/%d](%s) -> %s", methodName, + final String message = String.format("%s[this=%s, client=%s](%s) -> %s", methodName, object, - Binder.getCallingUid(), Binder.getCallingPid(), + printObject(originatorIdentity), printArgs(args), printObject(retVal)); Log.i(TAG, message); appendMessage(message); } - private void logVoidReturnWithObject(@NonNull Object object, @NonNull String methodName, + private void logVoidReturnWithObject(@NonNull Object object, + @Nullable Identity originatorIdentity, @NonNull String methodName, @NonNull Object[] args) { - final String message = String.format("%s[this=%s, caller=%d/%d](%s)", methodName, + final String message = String.format("%s[this=%s, client=%s](%s)", methodName, object, - Binder.getCallingUid(), Binder.getCallingPid(), + printObject(originatorIdentity), printArgs(args)); Log.i(TAG, message); appendMessage(message); } - private void logExceptionWithObject(@NonNull Object object, @NonNull String methodName, + private void logExceptionWithObject(@NonNull Object object, + @Nullable Identity originatorIdentity, @NonNull String methodName, @NonNull Exception ex, Object[] args) { - final String message = String.format("%s[this=%s, caller=%d/%d](%s) threw", methodName, + final String message = String.format("%s[this=%s, client=%s](%s) threw", methodName, object, - Binder.getCallingUid(), Binder.getCallingPid(), + printObject(originatorIdentity), printArgs(args)); Log.e(TAG, message, ex); appendMessage(message + " " + ex.toString()); @@ -429,7 +446,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt } } - @Override public void dump(PrintWriter pw) { + @Override + public void dump(PrintWriter pw) { pw.println(); pw.println("========================================="); pw.println("Last events"); diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java index f50c06118d12d..7b6c6561ccf15 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java @@ -16,11 +16,16 @@ package com.android.server.soundtrigger_middleware; -import android.Manifest; +import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; +import static android.Manifest.permission.RECORD_AUDIO; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.PermissionChecker; +import android.media.permission.Identity; +import android.media.permission.IdentityContext; +import android.media.permission.PermissionUtil; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; @@ -42,12 +47,12 @@ import java.util.Objects; /** * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions. *

- * Every public method in this class, overriding an interface method, must follow the following + * Every public method in this class, overriding an interface method, must follow a similar * pattern: *

  * @Override public T method(S arg) {
  *     // Permission check.
- *     checkPermissions();
+ *     enforcePermissions*(...);
  *     return mDelegate.method(arg);
  * }
  * 
@@ -68,18 +73,20 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware @Override public @NonNull - SoundTriggerModuleDescriptor[] listModules() throws RemoteException { - checkPermissions(); + SoundTriggerModuleDescriptor[] listModules() { + Identity identity = getIdentity(); + enforcePermissionsForPreflight(identity); return mDelegate.listModules(); } @Override public @NonNull ISoundTriggerModule attach(int handle, - @NonNull ISoundTriggerCallback callback) throws RemoteException { - checkPermissions(); - return new ModuleWrapper( - mDelegate.attach(handle, new CallbackWrapper(callback))); + @NonNull ISoundTriggerCallback callback) { + Identity identity = getIdentity(); + enforcePermissionsForPreflight(identity); + ModuleWrapper wrapper = new ModuleWrapper(identity, callback); + return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper())); } @Override @@ -91,7 +98,7 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware // Override toString() in order to have the delegate's ID in it. @Override public String toString() { - return mDelegate.toString(); + return Objects.toString(mDelegate); } @Override @@ -101,39 +108,89 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware } /** - * 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. + * Get the identity context, or throws an InternalServerError if it has not been established. + * + * @return The identity. */ - private void checkPermissions() { - enforcePermission(Manifest.permission.RECORD_AUDIO); - enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD); + private static @NonNull + Identity getIdentity() { + return IdentityContext.getNonNull(); } /** - * Throws a {@link SecurityException} if caller permanently doesn't have the given permission, + * Throws a {@link SecurityException} if originator 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. + * originator temporarily doesn't have the right permissions to use this service. */ - private void enforcePermission(String permission) { - final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext, + private void enforcePermissionsForPreflight(@NonNull Identity identity) { + enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO); + enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD); + } + + /** + * Throws a {@link SecurityException} iff the originator has permission to receive data. + */ + void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) { + enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO, reason); + enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD, + reason); + } + + /** + * Throws a {@link SecurityException} iff the given identity has given permission to receive + * data. + * + * @param context A {@link Context}, used for permission checks. + * @param identity The identity to check. + * @param permission The identifier of the permission we want to check. + * @param reason The reason why we're requesting the permission, for auditing purposes. + */ + private static void enforcePermissionForDataDelivery(@NonNull Context context, + @NonNull Identity identity, + @NonNull String permission, @NonNull String reason) { + // TODO(ytai): We're temporarily ignoring proc state until we have a proper permission that + // represents being able to use the microphone in the background. Otherwise, some of our + // existing use-cases would break. + final int status = PermissionUtil.checkPermissionForDataDeliveryIgnoreProcState(context, + identity, permission, reason); + if (status != PermissionChecker.PERMISSION_GRANTED) { + throw new SecurityException( + String.format("Failed to obtain permission %s for identity %s", permission, + ObjectPrinter.print(identity, true, 16))); + } + } + + /** + * Throws a {@link SecurityException} if originator permanently doesn't have the given + * permission, or a {@link ServiceSpecificException} with a {@link + * Status#TEMPORARY_PERMISSION_DENIED} if caller originator doesn't have the given permission. + * + * @param context A {@link Context}, used for permission checks. + * @param identity The identity to check. + * @param permission The identifier of the permission we want to check. + */ + private static void enforcePermissionForPreflight(@NonNull Context context, + @NonNull Identity identity, @NonNull String permission) { + final int status = PermissionUtil.checkPermissionForPreflight(context, identity, 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)); + String.format("Failed to obtain permission %s for identity %s", permission, + ObjectPrinter.print(identity, true, 16))); case PermissionChecker.PERMISSION_SOFT_DENIED: throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED, - String.format("Caller must have the %s permission.", permission)); + String.format("Failed to obtain permission %s for identity %s", permission, + ObjectPrinter.print(identity, true, 16))); default: throw new RuntimeException("Unexpected perimission check result."); } } + @Override public void dump(PrintWriter pw) { if (mDelegate instanceof Dumpable) { @@ -146,27 +203,40 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions. */ private class ModuleWrapper extends ISoundTriggerModule.Stub { - private final ISoundTriggerModule mDelegate; + private ISoundTriggerModule mDelegate; + private final @NonNull Identity mOriginatorIdentity; + private final @NonNull CallbackWrapper mCallbackWrapper; - ModuleWrapper(@NonNull ISoundTriggerModule delegate) { + ModuleWrapper(@NonNull Identity originatorIdentity, + @NonNull ISoundTriggerCallback callback) { + mOriginatorIdentity = originatorIdentity; + mCallbackWrapper = new CallbackWrapper(callback); + } + + ModuleWrapper attach(@NonNull ISoundTriggerModule delegate) { mDelegate = delegate; + return this; + } + + ISoundTriggerCallback getCallbackWrapper() { + return mCallbackWrapper; } @Override public int loadModel(@NonNull SoundModel model) throws RemoteException { - checkPermissions(); + enforcePermissions(); return mDelegate.loadModel(model); } @Override public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException { - checkPermissions(); + enforcePermissions(); return mDelegate.loadPhraseModel(model); } @Override public void unloadModel(int modelHandle) throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.unloadModel(modelHandle); } @@ -174,32 +244,32 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware @Override public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.startRecognition(modelHandle, config); } @Override public void stopRecognition(int modelHandle) throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.stopRecognition(modelHandle); } @Override public void forceRecognitionEvent(int modelHandle) throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.forceRecognitionEvent(modelHandle); } @Override public void setModelParameter(int modelHandle, int modelParam, int value) throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.setModelParameter(modelHandle, modelParam, value); } @Override public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { - checkPermissions(); + enforcePermissions(); return mDelegate.getModelParameter(modelHandle, modelParam); } @@ -207,14 +277,14 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware @Nullable public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) throws RemoteException { - checkPermissions(); + enforcePermissions(); return mDelegate.queryModelParameterSupport(modelHandle, modelParam); } @Override public void detach() throws RemoteException { - checkPermissions(); + enforcePermissions(); mDelegate.detach(); } @@ -223,45 +293,56 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware public String toString() { return Objects.toString(mDelegate); } - } - private class CallbackWrapper implements ISoundTriggerCallback { - private final ISoundTriggerCallback mDelegate; - - private CallbackWrapper(ISoundTriggerCallback delegate) { - mDelegate = delegate; + private void enforcePermissions() { + enforcePermissionsForPreflight(mOriginatorIdentity); } - @Override - public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException { - mDelegate.onRecognition(modelHandle, event); - } + private class CallbackWrapper implements ISoundTriggerCallback { + private final ISoundTriggerCallback mDelegate; - @Override - public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event) - throws RemoteException { - mDelegate.onPhraseRecognition(modelHandle, event); - } + private CallbackWrapper(ISoundTriggerCallback delegate) { + mDelegate = delegate; + } - @Override - public void onRecognitionAvailabilityChange(boolean available) throws RemoteException { - mDelegate.onRecognitionAvailabilityChange(available); - } + @Override + public void onRecognition(int modelHandle, RecognitionEvent event) + throws RemoteException { + enforcePermissions("Sound trigger recognition."); + mDelegate.onRecognition(modelHandle, event); + } - @Override - public void onModuleDied() throws RemoteException { - mDelegate.onModuleDied(); - } + @Override + public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event) + throws RemoteException { + enforcePermissions("Sound trigger phrase recognition."); + mDelegate.onPhraseRecognition(modelHandle, event); + } - @Override - public IBinder asBinder() { - return mDelegate.asBinder(); - } + @Override + public void onRecognitionAvailabilityChange(boolean available) throws RemoteException { + mDelegate.onRecognitionAvailabilityChange(available); + } - // Override toString() in order to have the delegate's ID in it. - @Override - public String toString() { - return Objects.toString(mDelegate); + @Override + public void onModuleDied() throws RemoteException { + mDelegate.onModuleDied(); + } + + @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 enforcePermissions(String reason) { + enforcePermissionsForDataDelivery(mOriginatorIdentity, reason); + } } } } 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 eb3c907ec5455..db7a575b08e2f 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java @@ -16,9 +16,15 @@ package com.android.server.soundtrigger_middleware; +import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY; + import android.annotation.NonNull; import android.content.Context; import android.hardware.soundtrigger.V2_0.ISoundTriggerHw; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.Identity; +import android.media.permission.PermissionUtil; +import android.media.permission.SafeCloseable; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; @@ -62,15 +68,17 @@ import java.util.Objects; public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub { static private final String TAG = "SoundTriggerMiddlewareService"; - @NonNull - private final ISoundTriggerMiddlewareInternal mDelegate; + private final @NonNull ISoundTriggerMiddlewareInternal mDelegate; + private final @NonNull Context mContext; /** * 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 ISoundTriggerMiddlewareInternal delegate) { + private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareInternal delegate, + @NonNull Context context) { mDelegate = Objects.requireNonNull(delegate); + mContext = context; new ExternalCaptureStateTracker(active -> { try { mDelegate.setCaptureState(active); @@ -81,16 +89,37 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic } @Override - public @NonNull - SoundTriggerModuleDescriptor[] listModules() throws RemoteException { - return mDelegate.listModules(); + public SoundTriggerModuleDescriptor[] listModulesAsOriginator(Identity identity) { + try (SafeCloseable ignored = establishIdentityDirect(identity)) { + return mDelegate.listModules(); + } } @Override - public @NonNull - ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) - throws RemoteException { - return new ModuleService(mDelegate.attach(handle, callback)); + public SoundTriggerModuleDescriptor[] listModulesAsMiddleman(Identity middlemanIdentity, + Identity originatorIdentity) { + try (SafeCloseable ignored = establishIdentityIndirect(middlemanIdentity, + originatorIdentity)) { + return mDelegate.listModules(); + } + } + + @Override + public ISoundTriggerModule attachAsOriginator(int handle, Identity identity, + ISoundTriggerCallback callback) { + try (SafeCloseable ignored = establishIdentityDirect(Objects.requireNonNull(identity))) { + return new ModuleService(mDelegate.attach(handle, callback)); + } + } + + @Override + public ISoundTriggerModule attachAsMiddleman(int handle, Identity middlemanIdentity, + Identity originatorIdentity, ISoundTriggerCallback callback) { + try (SafeCloseable ignored = establishIdentityIndirect( + Objects.requireNonNull(middlemanIdentity), + Objects.requireNonNull(originatorIdentity))) { + return new ModuleService(mDelegate.attach(handle, callback)); + } } @Override @@ -100,6 +129,18 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic } } + private @NonNull + SafeCloseable establishIdentityIndirect(Identity middlemanIdentity, + Identity originatorIdentity) { + return PermissionUtil.establishIdentityIndirect(mContext, SOUNDTRIGGER_DELEGATE_IDENTITY, + middlemanIdentity, originatorIdentity); + } + + private @NonNull + SafeCloseable establishIdentityDirect(Identity originatorIdentity) { + return PermissionUtil.establishIdentityDirect(originatorIdentity); + } + private final static class ModuleService extends ISoundTriggerModule.Stub { private final ISoundTriggerModule mDelegate; @@ -109,55 +150,75 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic @Override public int loadModel(SoundModel model) throws RemoteException { - return mDelegate.loadModel(model); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return mDelegate.loadModel(model); + } } @Override public int loadPhraseModel(PhraseSoundModel model) throws RemoteException { - return mDelegate.loadPhraseModel(model); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return mDelegate.loadPhraseModel(model); + } } @Override public void unloadModel(int modelHandle) throws RemoteException { - mDelegate.unloadModel(modelHandle); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.unloadModel(modelHandle); + } } @Override public void startRecognition(int modelHandle, RecognitionConfig config) throws RemoteException { - mDelegate.startRecognition(modelHandle, config); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.startRecognition(modelHandle, config); + } } @Override public void stopRecognition(int modelHandle) throws RemoteException { - mDelegate.stopRecognition(modelHandle); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.stopRecognition(modelHandle); + } } @Override public void forceRecognitionEvent(int modelHandle) throws RemoteException { - mDelegate.forceRecognitionEvent(modelHandle); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.forceRecognitionEvent(modelHandle); + } } @Override public void setModelParameter(int modelHandle, int modelParam, int value) throws RemoteException { - mDelegate.setModelParameter(modelHandle, modelParam, value); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.setModelParameter(modelHandle, modelParam, value); + } } @Override public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { - return mDelegate.getModelParameter(modelHandle, modelParam); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return mDelegate.getModelParameter(modelHandle, modelParam); + } } @Override public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) throws RemoteException { - return mDelegate.queryModelParameterSupport(modelHandle, modelParam); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + return mDelegate.queryModelParameterSupport(modelHandle, modelParam); + } } @Override public void detach() throws RemoteException { - mDelegate.detach(); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mDelegate.detach(); + } } } @@ -187,7 +248,7 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic new SoundTriggerMiddlewareValidation( new SoundTriggerMiddlewareImpl(factories, new AudioSessionProviderImpl())), - getContext())))); + getContext())), 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 index 12ec36a1fe72a..51519cb56a112 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java @@ -16,11 +16,10 @@ 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.permission.Identity; +import android.media.permission.IdentityContext; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; @@ -113,11 +112,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware ALIVE, DETACHED, DEAD - }; + } private class ModuleState { final @NonNull SoundTriggerModuleProperties properties; - Set sessions = new HashSet<>(); + Set sessions = new HashSet<>(); private ModuleState(@NonNull SoundTriggerModuleProperties properties) { this.properties = properties; @@ -202,10 +201,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // 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; + Session session = new Session(handle, callback); + session.attach(mDelegate.attach(handle, session.getCallbackWrapper())); + return session; } catch (Exception e) { throw handleException(e); } @@ -254,7 +252,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware pw.printf("Module %d\n%s\n", handle, ObjectPrinter.print(module.properties, true, 16)); pw.println("========================================="); - for (ModuleService session : module.sessions) { + for (Session session : module.sessions) { session.dump(pw); } } @@ -284,7 +282,14 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware /** Model is loaded, recognition is inactive. */ LOADED, /** Model is loaded, recognition is active. */ - ACTIVE + ACTIVE, + /** + * Model is active as far as the client is concerned, but loaded as far as the + * layers are concerned. This condition occurs when a recognition event that indicates + * the recognition for this model arrived from the underlying layer, but had not been + * delivered to the caller (most commonly, for permission reasons). + */ + INTERCEPTED, } /** Activity state. */ @@ -357,9 +362,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware * 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 class Session extends ISoundTriggerModule.Stub { private ISoundTriggerModule mDelegate; // While generally all the fields of this class must be changed under a lock, an exception // is made for the specific case of changing a model state from ACTIVE to LOADED, which @@ -370,15 +373,17 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware ConcurrentMap mLoadedModels = new ConcurrentHashMap<>(); private final int mHandle; private ModuleStatus mState = ModuleStatus.ALIVE; + private final CallbackWrapper mCallbackWrapper; + private final Identity mOriginatorIdentity; - ModuleService(int handle, @NonNull ISoundTriggerCallback callback) { - mCallback = callback; + Session(int handle, @NonNull ISoundTriggerCallback callback) { + mCallbackWrapper = new CallbackWrapper(callback); mHandle = handle; - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } + mOriginatorIdentity = IdentityContext.get(); + } + + ISoundTriggerCallback getCallbackWrapper() { + return mCallbackWrapper; } void attach(@NonNull ISoundTriggerModule delegate) { @@ -446,7 +451,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware } if (modelState.getActivityState() != ModelState.Activity.LOADED) { throw new IllegalStateException("Model with handle: " + modelHandle - + " has invalid state for unloading: " + modelState.getActivityState()); + + " has invalid state for unloading"); } // From here on, every exception isn't client's fault. @@ -476,8 +481,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware } if (modelState.getActivityState() != ModelState.Activity.LOADED) { throw new IllegalStateException("Model with handle: " + modelHandle - + " has invalid state for starting recognition: " - + modelState.getActivityState()); + + " has invalid state for starting recognition"); } // From here on, every exception isn't client's fault. @@ -512,7 +516,12 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // From here on, every exception isn't client's fault. try { - mDelegate.stopRecognition(modelHandle); + // If the activity state is LOADED or INTERCEPTED, we skip delegating the + // command, but still consider the call valid. In either case, the resulting + // state is LOADED. + if (modelState.getActivityState() == ModelState.Activity.ACTIVE) { + mDelegate.stopRecognition(modelHandle); + } modelState.setActivityState(ModelState.Activity.LOADED); } catch (Exception e) { throw handleException(e); @@ -538,7 +547,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // From here on, every exception isn't client's fault. try { - mDelegate.forceRecognitionEvent(modelHandle); + // If the activity state is LOADED or INTERCEPTED, we skip delegating the + // command, but still consider the call valid. + if (modelState.getActivityState() == ModelState.Activity.ACTIVE) { + mDelegate.forceRecognitionEvent(modelHandle); + } } catch (Exception e) { throw handleException(e); } @@ -658,7 +671,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware try { mDelegate.detach(); mState = ModuleStatus.DETACHED; - mCallback.asBinder().unlinkToDeath(this, 0); + mCallbackWrapper.detached(); mModules.get(mHandle).sessions.remove(this); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); @@ -667,7 +680,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware void dump(PrintWriter pw) { if (mState == ModuleStatus.ALIVE) { - pw.printf("Loaded models for session %s (handle, active)", toString()); + pw.printf("Session %s, client: %s\n", toString(), + ObjectPrinter.print(mOriginatorIdentity, true, 16)); + pw.printf("Loaded models (handle, active, description):", toString()); pw.println(); pw.println("-------------------------------"); for (Map.Entry entry : mLoadedModels.entrySet()) { @@ -684,10 +699,24 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware } } - //////////////////////////////////////////////////////////////////////////////////////////// - // Callbacks + class CallbackWrapper implements ISoundTriggerCallback, + IBinder.DeathRecipient { + private final ISoundTriggerCallback mCallback; - @Override + CallbackWrapper(ISoundTriggerCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + void detached() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) { // We cannot obtain a lock on SoundTriggerMiddlewareValidation.this, since this call // might be coming from the audio server (via setCaptureState()) while it is holding @@ -696,18 +725,21 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // To avoid this problem, we use an atomic model activity state. There is a risk of the // model not being in the mLoadedModels map here, since it might have been stopped / // unloaded while the event was in flight. - if (event.status != RecognitionStatus.FORCED) { - ModelState modelState = mLoadedModels.get(modelHandle); - if (modelState != null) { + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState != null) { + if (event.status != RecognitionStatus.FORCED) { modelState.setActivityState(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); + try { + mCallback.onRecognition(modelHandle, event); + } catch (Exception 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); + if (event.status != RecognitionStatus.FORCED) { + modelState.setActivityState(ModelState.Activity.INTERCEPTED); + } + } } } @@ -720,18 +752,21 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware // To avoid this problem, we use an atomic model activity state. There is a risk of the // model not being in the mLoadedModels map here, since it might have been stopped / // unloaded while the event was in flight. - if (event.common.status != RecognitionStatus.FORCED) { - ModelState modelState = mLoadedModels.get(modelHandle); - if (modelState != null) { - modelState.setActivityState(ModelState.Activity.LOADED); + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState != null) { + if (event.common.status != RecognitionStatus.FORCED) { + modelState.setActivityState(ModelState.Activity.LOADED); + } + try { + mCallback.onPhraseRecognition(modelHandle, event); + } catch (Exception 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); + if (event.common.status != RecognitionStatus.FORCED) { + modelState.setActivityState(ModelState.Activity.INTERCEPTED); + } } - } - 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); } } @@ -747,40 +782,52 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware } } - @Override - public void onModuleDied() { - synchronized (SoundTriggerMiddlewareValidation.this) { - mState = ModuleStatus.DEAD; - } - // Trigger the callback outside of the lock to avoid deadlocks. - 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 entry : - mLoadedModels.entrySet()) { - // Idempotent call, no harm in calling even for models that are already - // stopped. - mDelegate.stopRecognition(entry.getKey()); - mDelegate.unloadModel(entry.getKey()); - } - // Detach. - detachInternal(); - } catch (Exception e) { - throw handleException(e); + @Override + public void onModuleDied() { + synchronized (SoundTriggerMiddlewareValidation.this) { + mState = ModuleStatus.DEAD; } + // Trigger the callback outside of the lock to avoid deadlocks. + 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 entry : + mLoadedModels.entrySet()) { + if (entry.getValue().getActivityState() + == ModelState.Activity.ACTIVE) { + mDelegate.stopRecognition(entry.getKey()); + } + mDelegate.unloadModel(entry.getKey()); + } + // Detach. + detachInternal(); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public IBinder asBinder() { + return mCallback.asBinder(); + } + + // Override toString() in order to have the delegate's ID in it. + @Override + public String toString() { + return Objects.toString(mDelegate); } } }