summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYtai Ben-Tsvi <ytai@google.com>2020-04-29 15:36:36 -0700
committerYtai Ben-Tsvi <ytai@google.com>2020-09-10 13:27:07 -0700
commit1a619f03895189efdaa4db944b1cbd14d41b94c9 (patch)
tree999a61d4698cc031b500fb5b8a1d752645b318dd
parent4f6f70cdd10dc7ddea58f0e6b2c3b4fd2334e467 (diff)
Associate an originator identity to sessions
This change formalizes the permission enforcement patterns in the sound trigger middleware layer. Every sound trigger session is associated with an originator identity, established during attachment. This identity is the used to authorize any operations performed on this session, including data deliver via callbacks. Temporarily, for b/w compatibility the existing SoundTrigger.java API is preserved. Follow up changes will use the newer API, which requires an identity to be provided. Logging / dumpsys aspects have been modified to include the originator identity associated with every call / session. Change-Id: If83b151bd182af5a0dd98ff23dce252018de936b Bug: 163865561
-rw-r--r--core/java/android/hardware/soundtrigger/SoundTrigger.java184
-rw-r--r--core/java/android/hardware/soundtrigger/SoundTriggerModule.java34
-rw-r--r--core/res/AndroidManifest.xml9
-rw-r--r--media/Android.bp24
-rw-r--r--media/java/android/media/permission/ClearCallingIdentityContext.java58
-rw-r--r--media/java/android/media/permission/CompositeSafeCloseable.java40
-rw-r--r--media/java/android/media/permission/Identity.aidl32
-rw-r--r--media/java/android/media/permission/IdentityContext.java100
-rw-r--r--media/java/android/media/permission/PermissionUtil.java250
-rw-r--r--media/java/android/media/permission/SafeCloseable.java27
-rw-r--r--media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl53
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java25
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java216
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java223
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java103
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java211
16 files changed, 1288 insertions, 301 deletions
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 80f35a0a2e32..eb138fcb8f1d 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,17 +2003,81 @@ 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<ModuleProperties> 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<ModuleProperties> 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<ModuleProperties> 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);
@@ -2023,6 +2085,22 @@ public class SoundTrigger {
}
/**
+ * 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<ModuleProperties> 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.
* @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory.
@@ -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,
+ 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);
+ 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 a2a15b30d578..05823bf14b63 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 560e3c11d2f4..4082b309d861 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3972,6 +3972,15 @@
<permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD"
android:protectionLevel="signature|privileged" />
+ <!-- Puts an application in the chain of trust for sound trigger
+ operations. Being in the chain of trust allows an application to
+ delegate an identity of a separate entity to the sound trigger system
+ and vouch for the authenticity of this identity.
+ <p>Not for use by third-party applications.</p>
+ @hide -->
+ <permission android:name="android.permission.SOUNDTRIGGER_DELEGATE_IDENTITY"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi Allows an application to modify audio routing and override policy decisions.
<p>Not for use by third-party applications.</p>
@hide -->
diff --git a/media/Android.bp b/media/Android.bp
index 0ed10472561d..8895b3a9a2ba 100644
--- a/media/Android.bp
+++ b/media/Android.bp
@@ -23,6 +23,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,
local_include_dir: "java",
@@ -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 000000000000..364a2e800afe
--- /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.
+ *
+ * <p>
+ * Intended usage:
+ * <pre>
+ * 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.
+ * </pre>
+ *
+ * @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 000000000000..08990ebd5057
--- /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 000000000000..361497d59ea9
--- /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 000000000000..d10654fbe684
--- /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.
+ * <p>
+ * Intended usage:
+ * <pre>
+ * 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();
+ * ...
+ * }
+ * </pre>
+ *
+ * @hide
+ */
+public class IdentityContext implements SafeCloseable {
+ private static ThreadLocal<Identity> 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 000000000000..458f11243f68
--- /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.
+ * <p>
+ * <h1>Intended usage:</h1>
+ * 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.
+ * <p>
+ * 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:
+ * <ul>
+ * <li>The passed middlemanIdentity argument will have its uid/pid fields overridden with
+ * those provided by binder.
+ * <li>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.
+ * </ul>
+ * Example usage:
+ * <pre>
+ * 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.
+ * </pre>
+ * <p>
+ * <b>Important note:</b> 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:
+ * <ul>
+ * <li>The passed originatorIdentity argument will have its uid/pid fields overridden with
+ * those provided by binder.
+ * <li>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.
+ * </ul>
+ * Example usage:
+ * <pre>
+ * 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.
+ * </pre>
+ * <p>
+ * <b>Important note:</b> 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 000000000000..8ec15b18bd37
--- /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 06c39071cdf5..d1126b9006e0 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 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 attach(int handle, ISoundTriggerCallback callback);
-} \ No newline at end of file
+ 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 5def7621c148..a90053a23dea 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 8b6ed1ff5081..2ef0759719fc 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,94 +120,33 @@ 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);
+ logVoidReturnWithObject(this, IdentityContext.get(), 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 class ModuleLogging implements ISoundTriggerModule {
+ private ISoundTriggerModule mDelegate;
+ private final @NonNull CallbackLogging mCallbackWrapper;
+ private final @NonNull Identity mOriginatorIdentity;
- private void logVoidReturn(String methodName, Object... args) {
- logVoidReturnWithObject(this, methodName, args);
+ ModuleLogging(@NonNull ISoundTriggerCallback callback) {
+ mCallbackWrapper = new CallbackLogging(callback);
+ mOriginatorIdentity = IdentityContext.getNonNull();
}
- @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();
+ void attach(@NonNull ISoundTriggerModule delegate) {
+ mDelegate = delegate;
}
- }
-
- private class ModuleLogging implements ISoundTriggerModule {
- private final ISoundTriggerModule mDelegate;
- private ModuleLogging(ISoundTriggerModule delegate) {
- mDelegate = delegate;
+ ISoundTriggerCallback getCallbackWrapper() {
+ return mCallbackWrapper;
}
@Override
@@ -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 f50c06118d12..7b6c6561ccf1 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.
* <p>
- * 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:
* <code><pre>
* @Override public T method(S arg) {
* // Permission check.
- * checkPermissions();
+ * enforcePermissions*(...);
* return mDelegate.method(arg);
* }
* </pre></code>
@@ -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.
+ * originator temporarily doesn't have the right permissions to use this service.
+ */
+ 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 permission The permission to check.
+ * @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 void enforcePermission(String permission) {
- final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
+ 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 Identity originatorIdentity,
+ @NonNull ISoundTriggerCallback callback) {
+ mOriginatorIdentity = originatorIdentity;
+ mCallbackWrapper = new CallbackWrapper(callback);
+ }
- ModuleWrapper(@NonNull ISoundTriggerModule delegate) {
+ 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;
- }
-
- @Override
- public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException {
- mDelegate.onRecognition(modelHandle, event);
- }
-
- @Override
- public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
- throws RemoteException {
- mDelegate.onPhraseRecognition(modelHandle, event);
- }
- @Override
- public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
- mDelegate.onRecognitionAvailabilityChange(available);
- }
-
- @Override
- public void onModuleDied() throws RemoteException {
- mDelegate.onModuleDied();
+ private void enforcePermissions() {
+ enforcePermissionsForPreflight(mOriginatorIdentity);
}
- @Override
- public IBinder asBinder() {
- return mDelegate.asBinder();
- }
-
- // Override toString() in order to have the delegate's ID in it.
- @Override
- public String toString() {
- return Objects.toString(mDelegate);
+ private class CallbackWrapper implements ISoundTriggerCallback {
+ private final ISoundTriggerCallback mDelegate;
+
+ private CallbackWrapper(ISoundTriggerCallback delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void onRecognition(int modelHandle, RecognitionEvent event)
+ throws RemoteException {
+ enforcePermissions("Sound trigger recognition.");
+ mDelegate.onRecognition(modelHandle, event);
+ }
+
+ @Override
+ public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
+ throws RemoteException {
+ enforcePermissions("Sound trigger phrase recognition.");
+ mDelegate.onPhraseRecognition(modelHandle, event);
+ }
+
+ @Override
+ public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
+ mDelegate.onRecognitionAvailabilityChange(available);
+ }
+
+ @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 eb3c907ec545..db7a575b08e2 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 12ec36a1fe72..51519cb56a11 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<ModuleService> sessions = new HashSet<>();
+ Set<Session> 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<Integer, ModelState> 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<Integer, ModelState> 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,41 +782,53 @@ 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 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<Integer, ModelState> entry :
- mLoadedModels.entrySet()) {
- // Idempotent call, no harm in calling even for models that are already
- // stopped.
- mDelegate.stopRecognition(entry.getKey());
- mDelegate.unloadModel(entry.getKey());
+ @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().getActivityState()
+ == ModelState.Activity.ACTIVE) {
+ mDelegate.stopRecognition(entry.getKey());
+ }
+ mDelegate.unloadModel(entry.getKey());
+ }
+ // Detach.
+ detachInternal();
+ } catch (Exception e) {
+ throw handleException(e);
}
- // 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);
+ }
}
}
}