summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYtai Ben-tsvi <ytai@google.com>2020-09-21 19:07:04 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2020-09-21 19:07:04 +0000
commit189d6050daddc3be6631d6bb48b97393c5793e6e (patch)
tree2a9aff6e2bc5e6c724f608b6c3d38f7438b1816f
parent89008da40e650e7d04bda5419cd28b24d1e137ee (diff)
parentc4d23a482d9e0d65ef146c2d68ff2ab416a5e47d (diff)
Merge changes from topic "new-perm"
* changes: Demote AlwaysOnHotwordDetector to SystemApi Sessionize VoiceInteractionManagerService Sessionize the SoundTriggerService layer. Use standard API in SoundTriggerTestApp Fix bug in dumpsys Require identity information in SoundTrigger.java Associate an originator identity to sessions Extract permission checking as a separate aspect Correctly handle HAL death
-rw-r--r--api/current.txt46
-rw-r--r--api/system-current.txt46
-rw-r--r--core/java/android/hardware/soundtrigger/SoundTrigger.java184
-rw-r--r--core/java/android/hardware/soundtrigger/SoundTriggerModule.java34
-rw-r--r--core/java/android/service/voice/AlwaysOnHotwordDetector.java44
-rw-r--r--core/java/android/service/voice/VoiceInteractionService.java14
-rw-r--r--core/java/com/android/internal/app/ISoundTriggerService.aidl78
-rw-r--r--core/java/com/android/internal/app/ISoundTriggerSession.aidl68
-rw-r--r--core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl78
-rw-r--r--core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl95
-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/SoundTriggerDetector.java12
-rw-r--r--media/java/android/media/soundtrigger/SoundTriggerManager.java53
-rw-r--r--media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl53
-rw-r--r--non-updatable-api/current.txt46
-rw-r--r--non-updatable-api/system-current.txt46
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerMiddlewareInternal.java25
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Enforcer.java7
-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.java348
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java113
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java309
-rw-r--r--services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java5
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java18
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java179
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java1274
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java267
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java22
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java35
36 files changed, 2857 insertions, 1398 deletions
diff --git a/api/current.txt b/api/current.txt
index 0f4cffc9427b..ae22abbf884a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -44287,54 +44287,8 @@ package android.service.textservice {
package android.service.voice {
- public class AlwaysOnHotwordDetector {
- method public android.content.Intent createEnrollIntent();
- method public android.content.Intent createReEnrollIntent();
- method public android.content.Intent createUnEnrollIntent();
- method public int getParameter(int);
- method public int getSupportedAudioCapabilities();
- method public int getSupportedRecognitionModes();
- method @Nullable public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int);
- method public int setParameter(int, int);
- method public boolean startRecognition(int);
- method public boolean stopRecognition();
- field public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 1; // 0x1
- field public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 2; // 0x2
- field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0
- field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
- field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
- field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
- field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
- field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
- field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
- field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
- field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
- field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
- field @Deprecated public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
- }
-
- public abstract static class AlwaysOnHotwordDetector.Callback {
- ctor public AlwaysOnHotwordDetector.Callback();
- method public abstract void onAvailabilityChanged(int);
- method public abstract void onDetected(@NonNull android.service.voice.AlwaysOnHotwordDetector.EventPayload);
- method public abstract void onError();
- method public abstract void onRecognitionPaused();
- method public abstract void onRecognitionResumed();
- }
-
- public static class AlwaysOnHotwordDetector.EventPayload {
- method @Nullable public android.media.AudioFormat getCaptureAudioFormat();
- method @Nullable public byte[] getTriggerAudio();
- }
-
- public static final class AlwaysOnHotwordDetector.ModelParamRange {
- method public int getEnd();
- method public int getStart();
- }
-
public class VoiceInteractionService extends android.app.Service {
ctor public VoiceInteractionService();
- method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
method public int getDisabledShowContext();
method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
method public android.os.IBinder onBind(android.content.Intent);
diff --git a/api/system-current.txt b/api/system-current.txt
index 65fe85bdd727..2bb9f37a926e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10272,7 +10272,53 @@ package android.service.trust {
package android.service.voice {
+ public class AlwaysOnHotwordDetector {
+ method @Nullable public android.content.Intent createEnrollIntent();
+ method @Nullable public android.content.Intent createReEnrollIntent();
+ method @Nullable public android.content.Intent createUnEnrollIntent();
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int getParameter(int);
+ method public int getSupportedAudioCapabilities();
+ method public int getSupportedRecognitionModes();
+ method @Nullable @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int setParameter(int, int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean stopRecognition();
+ field public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 1; // 0x1
+ field public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 2; // 0x2
+ field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0
+ field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
+ field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
+ field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
+ field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
+ field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
+ field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
+ field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
+ field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
+ field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
+ field @Deprecated public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
+ }
+
+ public abstract static class AlwaysOnHotwordDetector.Callback {
+ ctor public AlwaysOnHotwordDetector.Callback();
+ method public abstract void onAvailabilityChanged(int);
+ method public abstract void onDetected(@NonNull android.service.voice.AlwaysOnHotwordDetector.EventPayload);
+ method public abstract void onError();
+ method public abstract void onRecognitionPaused();
+ method public abstract void onRecognitionResumed();
+ }
+
+ public static class AlwaysOnHotwordDetector.EventPayload {
+ method @Nullable public android.media.AudioFormat getCaptureAudioFormat();
+ method @Nullable public byte[] getTriggerAudio();
+ }
+
+ public static final class AlwaysOnHotwordDetector.ModelParamRange {
+ method public int getEnd();
+ method public int getStart();
+ }
+
public class VoiceInteractionService extends android.app.Service {
+ method @NonNull public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
method @NonNull @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public final android.media.voice.KeyphraseModelManager createKeyphraseModelManager();
}
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 80f35a0a2e32..0f1c2a59965b 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,
+ private 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/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 6f941121771e..8f8e6cc3d84a 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -16,9 +16,15 @@
package android.service.voice;
+import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
+import static android.Manifest.permission.RECORD_AUDIO;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
@@ -32,6 +38,7 @@ import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat;
+import android.media.permission.Identity;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
@@ -39,6 +46,7 @@ import android.os.RemoteException;
import android.util.Slog;
import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -48,7 +56,12 @@ import java.util.Locale;
/**
* A class that lets a VoiceInteractionService implementation interact with
* always-on keyphrase detection APIs.
+ *
+ * @hide
+ * TODO(b/168605867): Once Metalava supports expressing a removed public, but current system API,
+ * mark and track it as such.
*/
+@SystemApi
public class AlwaysOnHotwordDetector {
//---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
/**
@@ -228,6 +241,7 @@ public class AlwaysOnHotwordDetector {
private KeyphraseMetadata mKeyphraseMetadata;
private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
private final IVoiceInteractionManagerService mModelManagementService;
+ private final IVoiceInteractionSoundTriggerSession mSoundTriggerSession;
private final SoundTriggerListener mInternalCallback;
private final Callback mExternalCallback;
private final Object mLock = new Object();
@@ -425,6 +439,14 @@ public class AlwaysOnHotwordDetector {
mHandler = new MyHandler();
mInternalCallback = new SoundTriggerListener(mHandler);
mModelManagementService = modelManagementService;
+ try {
+ Identity identity = new Identity();
+ identity.packageName = ActivityThread.currentOpPackageName();
+ mSoundTriggerSession = mModelManagementService.createSoundTriggerSessionAsOriginator(
+ identity);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
new RefreshAvailabiltyTask().execute();
}
@@ -485,7 +507,7 @@ public class AlwaysOnHotwordDetector {
private int getSupportedAudioCapabilitiesLocked() {
try {
ModuleProperties properties =
- mModelManagementService.getDspModuleProperties();
+ mSoundTriggerSession.getDspModuleProperties();
if (properties != null) {
return properties.getAudioCapabilities();
}
@@ -513,6 +535,7 @@ public class AlwaysOnHotwordDetector {
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
*/
+ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
synchronized (mLock) {
@@ -543,6 +566,7 @@ public class AlwaysOnHotwordDetector {
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
*/
+ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public boolean stopRecognition() {
if (DBG) Slog.d(TAG, "stopRecognition()");
synchronized (mLock) {
@@ -577,6 +601,7 @@ public class AlwaysOnHotwordDetector {
* - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
* if API is not supported by HAL
*/
+ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public int setParameter(@ModelParams int modelParam, int value) {
if (DBG) {
Slog.d(TAG, "setParameter(" + modelParam + ", " + value + ")");
@@ -604,6 +629,7 @@ public class AlwaysOnHotwordDetector {
* @param modelParam {@link ModelParams}
* @return value of parameter
*/
+ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public int getParameter(@ModelParams int modelParam) {
if (DBG) {
Slog.d(TAG, "getParameter(" + modelParam + ")");
@@ -628,6 +654,7 @@ public class AlwaysOnHotwordDetector {
* @param modelParam {@link ModelParams}
* @return supported range of parameter, null if not supported
*/
+ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
@Nullable
public ModelParamRange queryParameter(@ModelParams int modelParam) {
if (DBG) {
@@ -658,6 +685,7 @@ public class AlwaysOnHotwordDetector {
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
*/
+ @Nullable
public Intent createEnrollIntent() {
if (DBG) Slog.d(TAG, "createEnrollIntent");
synchronized (mLock) {
@@ -679,6 +707,7 @@ public class AlwaysOnHotwordDetector {
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
*/
+ @Nullable
public Intent createUnEnrollIntent() {
if (DBG) Slog.d(TAG, "createUnEnrollIntent");
synchronized (mLock) {
@@ -700,6 +729,7 @@ public class AlwaysOnHotwordDetector {
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
*/
+ @Nullable
public Intent createReEnrollIntent() {
if (DBG) Slog.d(TAG, "createReEnrollIntent");
synchronized (mLock) {
@@ -782,7 +812,7 @@ public class AlwaysOnHotwordDetector {
int code;
try {
- code = mModelManagementService.startRecognition(
+ code = mSoundTriggerSession.startRecognition(
mKeyphraseMetadata.getId(), mLocale.toLanguageTag(), mInternalCallback,
new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
recognitionExtra, null /* additional data */, audioCapabilities));
@@ -799,7 +829,7 @@ public class AlwaysOnHotwordDetector {
private int stopRecognitionLocked() {
int code;
try {
- code = mModelManagementService.stopRecognition(mKeyphraseMetadata.getId(),
+ code = mSoundTriggerSession.stopRecognition(mKeyphraseMetadata.getId(),
mInternalCallback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -813,7 +843,7 @@ public class AlwaysOnHotwordDetector {
private int setParameterLocked(@ModelParams int modelParam, int value) {
try {
- int code = mModelManagementService.setParameter(mKeyphraseMetadata.getId(), modelParam,
+ int code = mSoundTriggerSession.setParameter(mKeyphraseMetadata.getId(), modelParam,
value);
if (code != STATUS_OK) {
@@ -828,7 +858,7 @@ public class AlwaysOnHotwordDetector {
private int getParameterLocked(@ModelParams int modelParam) {
try {
- return mModelManagementService.getParameter(mKeyphraseMetadata.getId(), modelParam);
+ return mSoundTriggerSession.getParameter(mKeyphraseMetadata.getId(), modelParam);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -838,7 +868,7 @@ public class AlwaysOnHotwordDetector {
private ModelParamRange queryParameterLocked(@ModelParams int modelParam) {
try {
SoundTrigger.ModelParamRange modelParamRange =
- mModelManagementService.queryParameter(mKeyphraseMetadata.getId(), modelParam);
+ mSoundTriggerSession.queryParameter(mKeyphraseMetadata.getId(), modelParam);
if (modelParamRange == null) {
return null;
@@ -972,7 +1002,7 @@ public class AlwaysOnHotwordDetector {
ModuleProperties dspModuleProperties;
try {
dspModuleProperties =
- mModelManagementService.getDspModuleProperties();
+ mSoundTriggerSession.getDspModuleProperties();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 45d3465fdae8..fb03ed45113e 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -20,6 +20,7 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.compat.annotation.UnsupportedAppUsage;
@@ -237,9 +238,8 @@ public class VoiceInteractionService extends Service {
/**
* Called during service initialization to tell you when the system is ready
* to receive interaction from it. You should generally do initialization here
- * rather than in {@link #onCreate}. Methods such as {@link #showSession} and
- * {@link #createAlwaysOnHotwordDetector}
- * will not be operational until this point.
+ * rather than in {@link #onCreate}. Methods such as {@link #showSession} will
+ * not be operational until this point.
*/
public void onReady() {
mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
@@ -309,9 +309,15 @@ public class VoiceInteractionService extends Service {
* @param locale The locale for which the enrollment needs to be performed.
* @param callback The callback to notify of detection events.
* @return An always-on hotword detector for the given keyphrase and locale.
+ *
+ * @hide
*/
+ @SystemApi
+ @NonNull
public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
- String keyphrase, Locale locale, AlwaysOnHotwordDetector.Callback callback) {
+ @SuppressLint("MissingNullability") String keyphrase, // TODO: annotate nullability properly
+ @SuppressLint({"MissingNullability", "UseIcu"}) Locale locale,
+ @SuppressLint("MissingNullability") AlwaysOnHotwordDetector.Callback callback) {
if (mSystemService == null) {
throw new IllegalStateException("Not available until onReady() is called");
}
diff --git a/core/java/com/android/internal/app/ISoundTriggerService.aidl b/core/java/com/android/internal/app/ISoundTriggerService.aidl
index 74bfb963c6b0..f8aac42b8018 100644
--- a/core/java/com/android/internal/app/ISoundTriggerService.aidl
+++ b/core/java/com/android/internal/app/ISoundTriggerService.aidl
@@ -16,53 +16,45 @@
package com.android.internal.app;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.hardware.soundtrigger.IRecognitionStatusCallback;
-import android.hardware.soundtrigger.SoundTrigger;
-import android.hardware.soundtrigger.ModelParams;
-import android.os.Bundle;
-import android.os.ParcelUuid;
+import android.media.permission.Identity;
+import com.android.internal.app.ISoundTriggerSession;
/**
* Service interface for a generic sound recognition model.
+ *
+ * This interface serves as an entry point to establish a session, associated with a client
+ * identity, which exposes the actual functionality.
+ *
* @hide
*/
interface ISoundTriggerService {
-
- SoundTrigger.GenericSoundModel getSoundModel(in ParcelUuid soundModelId);
-
- void updateSoundModel(in SoundTrigger.GenericSoundModel soundModel);
-
- void deleteSoundModel(in ParcelUuid soundModelId);
-
- int startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback,
- in SoundTrigger.RecognitionConfig config);
-
- int stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
-
- int loadGenericSoundModel(in SoundTrigger.GenericSoundModel soundModel);
- int loadKeyphraseSoundModel(in SoundTrigger.KeyphraseSoundModel soundModel);
-
- int startRecognitionForService(in ParcelUuid soundModelId, in Bundle params,
- in ComponentName callbackIntent,in SoundTrigger.RecognitionConfig config);
-
- int stopRecognitionForService(in ParcelUuid soundModelId);
-
- int unloadSoundModel(in ParcelUuid soundModelId);
-
- /** For both ...Intent and ...Service based usage */
- boolean isRecognitionActive(in ParcelUuid parcelUuid);
-
- int getModelState(in ParcelUuid soundModelId);
-
- @nullable SoundTrigger.ModuleProperties getModuleProperties();
-
- int setParameter(in ParcelUuid soundModelId, in ModelParams modelParam,
- int value);
-
- int getParameter(in ParcelUuid soundModelId, in ModelParams modelParam);
-
- @nullable SoundTrigger.ModelParamRange queryParameter(in ParcelUuid soundModelId,
- in ModelParams modelParam);
+ /**
+ * Creates a new session.
+ *
+ * This version is intended to be used when the caller itself is the originator of the
+ * operations, for authorization purposes.
+ *
+ * The pid/uid fields are ignored and will be replaced by those provided by binder.
+ *
+ * It is good practice to clear the binder calling identity prior to calling this, in case the
+ * caller is ever in the same process as the callee.
+ */
+ ISoundTriggerSession attachAsOriginator(in Identity originatorIdentity);
+
+ /**
+ * Creates a new session.
+ *
+ * This version is intended to be used when the caller is acting on behalf of a separate entity
+ * (the originator) and the sessions operations are to be accounted against that originator for
+ * authorization purposes.
+ *
+ * The caller must hold the SOUNDTRIGGER_DELEGATE_IDENTITY permission in order to be trusted to
+ * provide a reliable originator identity. It should follow the best practices for reliably and
+ * securely verifying the identity of the originator.
+ *
+ * It is good practice to clear the binder calling identity prior to calling this, in case the
+ * caller is ever in the same process as the callee.
+ */
+ ISoundTriggerSession attachAsMiddleman(in Identity middlemanIdentity,
+ in Identity originatorIdentity);
}
diff --git a/core/java/com/android/internal/app/ISoundTriggerSession.aidl b/core/java/com/android/internal/app/ISoundTriggerSession.aidl
new file mode 100644
index 000000000000..ec7d282522c8
--- /dev/null
+++ b/core/java/com/android/internal/app/ISoundTriggerSession.aidl
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.hardware.soundtrigger.IRecognitionStatusCallback;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.ModelParams;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+/**
+ * Service interface for a generic sound recognition model.
+ * @hide
+ */
+interface ISoundTriggerSession {
+
+ SoundTrigger.GenericSoundModel getSoundModel(in ParcelUuid soundModelId);
+
+ void updateSoundModel(in SoundTrigger.GenericSoundModel soundModel);
+
+ void deleteSoundModel(in ParcelUuid soundModelId);
+
+ int startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback,
+ in SoundTrigger.RecognitionConfig config);
+
+ int stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
+
+ int loadGenericSoundModel(in SoundTrigger.GenericSoundModel soundModel);
+ int loadKeyphraseSoundModel(in SoundTrigger.KeyphraseSoundModel soundModel);
+
+ int startRecognitionForService(in ParcelUuid soundModelId, in Bundle params,
+ in ComponentName callbackIntent,in SoundTrigger.RecognitionConfig config);
+
+ int stopRecognitionForService(in ParcelUuid soundModelId);
+
+ int unloadSoundModel(in ParcelUuid soundModelId);
+
+ /** For both ...Intent and ...Service based usage */
+ boolean isRecognitionActive(in ParcelUuid parcelUuid);
+
+ int getModelState(in ParcelUuid soundModelId);
+
+ @nullable SoundTrigger.ModuleProperties getModuleProperties();
+
+ int setParameter(in ParcelUuid soundModelId, in ModelParams modelParam,
+ int value);
+
+ int getParameter(in ParcelUuid soundModelId, in ModelParams modelParam);
+
+ @nullable SoundTrigger.ModelParamRange queryParameter(in ParcelUuid soundModelId,
+ in ModelParams modelParam);
+}
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 15ba8e8c11f7..49b4cd1e6a73 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -18,6 +18,7 @@ package com.android.internal.app;
import android.content.ComponentName;
import android.content.Intent;
+import android.media.permission.Identity;
import android.os.Bundle;
import android.os.RemoteCallback;
@@ -25,9 +26,8 @@ import com.android.internal.app.IVoiceActionCheckCallback;
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.IVoiceInteractionSessionListener;
-import android.hardware.soundtrigger.IRecognitionStatusCallback;
+import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
import android.hardware.soundtrigger.KeyphraseMetadata;
-import android.hardware.soundtrigger.ModelParams;
import android.hardware.soundtrigger.SoundTrigger;
import android.service.voice.IVoiceInteractionService;
import android.service.voice.IVoiceInteractionSession;
@@ -86,13 +86,6 @@ interface IVoiceInteractionManagerService {
* @RequiresPermission Manifest.permission.MANAGE_VOICE_KEYPHRASES
*/
int deleteKeyphraseSoundModel(int keyphraseId, in String bcp47Locale);
-
- /**
- * Gets the properties of the DSP hardware on this device, null if not present.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- */
- SoundTrigger.ModuleProperties getDspModuleProperties();
/**
* Indicates if there's a keyphrase sound model available for the given keyphrase ID and the
* user ID of the caller.
@@ -116,65 +109,6 @@ interface IVoiceInteractionManagerService {
*/
KeyphraseMetadata getEnrolledKeyphraseMetadata(String keyphrase, String bcp47Locale);
/**
- * Starts a recognition for the given keyphrase.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- */
- int startRecognition(int keyphraseId, in String bcp47Locale,
- in IRecognitionStatusCallback callback,
- in SoundTrigger.RecognitionConfig recognitionConfig);
- /**
- * Stops a recognition for the given keyphrase.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- */
- int stopRecognition(int keyphraseId, in IRecognitionStatusCallback callback);
- /**
- * Set a model specific ModelParams with the given value. This
- * parameter will keep its value for the duration the model is loaded regardless of starting and
- * stopping recognition. Once the model is unloaded, the value will be lost.
- * queryParameter should be checked first before calling this method.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- *
- * @param keyphraseId The unique identifier for the keyphrase.
- * @param modelParam ModelParams
- * @param value Value to set
- * @return - {@link SoundTrigger#STATUS_OK} in case of success
- * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
- * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
- * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
- * if API is not supported by HAL
- */
- int setParameter(int keyphraseId, in ModelParams modelParam, int value);
- /**
- * Get a model specific ModelParams. This parameter will keep its value
- * for the duration the model is loaded regardless of starting and stopping recognition.
- * Once the model is unloaded, the value will be lost. If the value is not set, a default
- * value is returned. See ModelParams for parameter default values.
- * queryParameter should be checked first before calling this method.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- *
- * @param keyphraseId The unique identifier for the keyphrase.
- * @param modelParam ModelParams
- * @return value of parameter
- */
- int getParameter(int keyphraseId, in ModelParams modelParam);
- /**
- * Determine if parameter control is supported for the given model handle.
- * This method should be checked prior to calling setParameter or getParameter.
- * Caller must be the active voice interaction service via
- * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
- *
- * @param keyphraseId The unique identifier for the keyphrase.
- * @param modelParam ModelParams
- * @return supported range of parameter, null if not supported
- */
- @nullable SoundTrigger.ModelParamRange queryParameter(int keyphraseId,
- in ModelParams modelParam);
-
- /**
* @return the component name for the currently active voice interaction service
* @RequiresPermission Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE
*/
@@ -277,4 +211,12 @@ interface IVoiceInteractionManagerService {
*/
void setDisabled(boolean disabled);
+ /**
+ * Creates a session, allowing controlling running sound models on detection hardware.
+ * Caller must provide an identity, used for permission tracking purposes.
+ * The uid/pid elements of the identity will be ignored by the server and replaced with the ones
+ * provided by binder.
+ */
+ IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator(
+ in Identity originatorIdentity);
}
diff --git a/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl b/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl
new file mode 100644
index 000000000000..33aab4132150
--- /dev/null
+++ b/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import android.hardware.soundtrigger.IRecognitionStatusCallback;
+import android.hardware.soundtrigger.ModelParams;
+import android.hardware.soundtrigger.SoundTrigger;
+
+/**
+ * This interface allows performing sound-trigger related operations with the actual sound trigger
+ * hardware.
+ *
+ * Every instance of this interface is associated with a client identity ("originator"), established
+ * upon the creation of the instance and used for permission accounting.
+ */
+interface IVoiceInteractionSoundTriggerSession {
+ /**
+ * Gets the properties of the DSP hardware on this device, null if not present.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ */
+ SoundTrigger.ModuleProperties getDspModuleProperties();
+ /**
+ * Starts a recognition for the given keyphrase.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ */
+ int startRecognition(int keyphraseId, in String bcp47Locale,
+ in IRecognitionStatusCallback callback,
+ in SoundTrigger.RecognitionConfig recognitionConfig);
+ /**
+ * Stops a recognition for the given keyphrase.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ */
+ int stopRecognition(int keyphraseId, in IRecognitionStatusCallback callback);
+ /**
+ * Set a model specific ModelParams with the given value. This
+ * parameter will keep its value for the duration the model is loaded regardless of starting and
+ * stopping recognition. Once the model is unloaded, the value will be lost.
+ * queryParameter should be checked first before calling this method.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ *
+ * @param keyphraseId The unique identifier for the keyphrase.
+ * @param modelParam ModelParams
+ * @param value Value to set
+ * @return - {@link SoundTrigger#STATUS_OK} in case of success
+ * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
+ * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
+ * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
+ * if API is not supported by HAL
+ */
+ int setParameter(int keyphraseId, in ModelParams modelParam, int value);
+ /**
+ * Get a model specific ModelParams. This parameter will keep its value
+ * for the duration the model is loaded regardless of starting and stopping recognition.
+ * Once the model is unloaded, the value will be lost. If the value is not set, a default
+ * value is returned. See ModelParams for parameter default values.
+ * queryParameter should be checked first before calling this method.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ *
+ * @param keyphraseId The unique identifier for the keyphrase.
+ * @param modelParam ModelParams
+ * @return value of parameter
+ */
+ int getParameter(int keyphraseId, in ModelParams modelParam);
+ /**
+ * Determine if parameter control is supported for the given model handle.
+ * This method should be checked prior to calling setParameter or getParameter.
+ * Caller must be the active voice interaction service via
+ * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}.
+ *
+ * @param keyphraseId The unique identifier for the keyphrase.
+ * @param modelParam ModelParams
+ * @return supported range of parameter, null if not supported
+ */
+ @nullable SoundTrigger.ModelParamRange queryParameter(int keyphraseId,
+ in ModelParams modelParam);
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4fd9e1dfc0ea..258a050b21af 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3984,6 +3984,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/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
index 08953392ca18..16a517b72119 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerDetector.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
@@ -35,7 +35,7 @@ import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Slog;
-import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.app.ISoundTriggerSession;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -64,7 +64,7 @@ public final class SoundTriggerDetector {
private final Object mLock = new Object();
- private final ISoundTriggerService mSoundTriggerService;
+ private final ISoundTriggerSession mSoundTriggerSession;
private final UUID mSoundModelId;
private final Callback mCallback;
private final Handler mHandler;
@@ -266,9 +266,9 @@ public final class SoundTriggerDetector {
* This class should be constructed by the {@link SoundTriggerManager}.
* @hide
*/
- SoundTriggerDetector(ISoundTriggerService soundTriggerService, UUID soundModelId,
+ SoundTriggerDetector(ISoundTriggerSession soundTriggerSession, UUID soundModelId,
@NonNull Callback callback, @Nullable Handler handler) {
- mSoundTriggerService = soundTriggerService;
+ mSoundTriggerSession = soundTriggerSession;
mSoundModelId = soundModelId;
mCallback = callback;
if (handler == null) {
@@ -305,7 +305,7 @@ public final class SoundTriggerDetector {
int status;
try {
- status = mSoundTriggerService.startRecognition(new ParcelUuid(mSoundModelId),
+ status = mSoundTriggerSession.startRecognition(new ParcelUuid(mSoundModelId),
mRecognitionCallback, new RecognitionConfig(captureTriggerAudio,
allowMultipleTriggers, null, null, audioCapabilities));
} catch (RemoteException e) {
@@ -321,7 +321,7 @@ public final class SoundTriggerDetector {
public boolean stopRecognition() {
int status = STATUS_OK;
try {
- status = mSoundTriggerService.stopRecognition(new ParcelUuid(mSoundModelId),
+ status = mSoundTriggerSession.stopRecognition(new ParcelUuid(mSoundModelId),
mRecognitionCallback);
} catch (RemoteException e) {
return false;
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 7d51b104a47d..0ff8d9e96220 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -23,6 +23,7 @@ import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -33,6 +34,10 @@ import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModelParamRange;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.hardware.soundtrigger.SoundTrigger.SoundModel;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.Identity;
+import android.media.permission.SafeCloseable;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelUuid;
@@ -41,6 +46,7 @@ import android.provider.Settings;
import android.util.Slog;
import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.app.ISoundTriggerSession;
import com.android.internal.util.Preconditions;
import java.util.HashMap;
@@ -61,7 +67,7 @@ public final class SoundTriggerManager {
private static final String TAG = "SoundTriggerManager";
private final Context mContext;
- private final ISoundTriggerService mSoundTriggerService;
+ private final ISoundTriggerSession mSoundTriggerSession;
// Stores a mapping from the sound model UUID to the SoundTriggerInstance created by
// the createSoundTriggerDetector() call.
@@ -74,7 +80,20 @@ public final class SoundTriggerManager {
if (DBG) {
Slog.i(TAG, "SoundTriggerManager created.");
}
- mSoundTriggerService = soundTriggerService;
+ try {
+ // This assumes that whoever is calling this ctor is the originator of the operations,
+ // as opposed to a service acting on behalf of a separate identity.
+ // Services acting on behalf of some other identity should not be using this class at
+ // all, but rather directly connect to the server and attach with explicit credentials.
+ Identity originatorIdentity = new Identity();
+ originatorIdentity.packageName = ActivityThread.currentOpPackageName();
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ mSoundTriggerSession = soundTriggerService.attachAsOriginator(originatorIdentity);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
mContext = context;
mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>();
}
@@ -85,7 +104,7 @@ public final class SoundTriggerManager {
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
public void updateModel(Model model) {
try {
- mSoundTriggerService.updateSoundModel(model.getGenericSoundModel());
+ mSoundTriggerSession.updateSoundModel(model.getGenericSoundModel());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -102,7 +121,7 @@ public final class SoundTriggerManager {
public Model getModel(UUID soundModelId) {
try {
GenericSoundModel model =
- mSoundTriggerService.getSoundModel(new ParcelUuid(soundModelId));
+ mSoundTriggerSession.getSoundModel(new ParcelUuid(soundModelId));
if (model == null) {
return null;
}
@@ -119,7 +138,7 @@ public final class SoundTriggerManager {
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
public void deleteModel(UUID soundModelId) {
try {
- mSoundTriggerService.deleteSoundModel(new ParcelUuid(soundModelId));
+ mSoundTriggerSession.deleteSoundModel(new ParcelUuid(soundModelId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -149,7 +168,7 @@ public final class SoundTriggerManager {
if (oldInstance != null) {
// Shutdown old instance.
}
- SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerService,
+ SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerSession,
soundModelId, callback, handler);
mReceiverInstanceMap.put(soundModelId, newInstance);
return newInstance;
@@ -309,10 +328,10 @@ public final class SoundTriggerManager {
try {
switch (soundModel.getType()) {
case SoundModel.TYPE_GENERIC_SOUND:
- return mSoundTriggerService.loadGenericSoundModel(
+ return mSoundTriggerSession.loadGenericSoundModel(
(GenericSoundModel) soundModel);
case SoundModel.TYPE_KEYPHRASE:
- return mSoundTriggerService.loadKeyphraseSoundModel(
+ return mSoundTriggerSession.loadKeyphraseSoundModel(
(KeyphraseSoundModel) soundModel);
default:
Slog.e(TAG, "Unkown model type");
@@ -351,7 +370,7 @@ public final class SoundTriggerManager {
Preconditions.checkNotNull(config);
try {
- return mSoundTriggerService.startRecognitionForService(new ParcelUuid(soundModelId),
+ return mSoundTriggerSession.startRecognitionForService(new ParcelUuid(soundModelId),
params, detectionService, config);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -369,7 +388,7 @@ public final class SoundTriggerManager {
return STATUS_ERROR;
}
try {
- return mSoundTriggerService.stopRecognitionForService(new ParcelUuid(soundModelId));
+ return mSoundTriggerSession.stopRecognitionForService(new ParcelUuid(soundModelId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -386,7 +405,7 @@ public final class SoundTriggerManager {
return STATUS_ERROR;
}
try {
- return mSoundTriggerService.unloadSoundModel(
+ return mSoundTriggerSession.unloadSoundModel(
new ParcelUuid(soundModelId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -404,7 +423,7 @@ public final class SoundTriggerManager {
return false;
}
try {
- return mSoundTriggerService.isRecognitionActive(
+ return mSoundTriggerSession.isRecognitionActive(
new ParcelUuid(soundModelId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -439,7 +458,7 @@ public final class SoundTriggerManager {
return STATUS_ERROR;
}
try {
- return mSoundTriggerService.getModelState(new ParcelUuid(soundModelId));
+ return mSoundTriggerSession.getModelState(new ParcelUuid(soundModelId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -455,7 +474,7 @@ public final class SoundTriggerManager {
public SoundTrigger.ModuleProperties getModuleProperties() {
try {
- return mSoundTriggerService.getModuleProperties();
+ return mSoundTriggerSession.getModuleProperties();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -481,7 +500,7 @@ public final class SoundTriggerManager {
public int setParameter(@Nullable UUID soundModelId,
@ModelParams int modelParam, int value) {
try {
- return mSoundTriggerService.setParameter(new ParcelUuid(soundModelId), modelParam,
+ return mSoundTriggerSession.setParameter(new ParcelUuid(soundModelId), modelParam,
value);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -504,7 +523,7 @@ public final class SoundTriggerManager {
public int getParameter(@NonNull UUID soundModelId,
@ModelParams int modelParam) {
try {
- return mSoundTriggerService.getParameter(new ParcelUuid(soundModelId), modelParam);
+ return mSoundTriggerSession.getParameter(new ParcelUuid(soundModelId), modelParam);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -524,7 +543,7 @@ public final class SoundTriggerManager {
public ModelParamRange queryParameter(@Nullable UUID soundModelId,
@ModelParams int modelParam) {
try {
- return mSoundTriggerService.queryParameter(new ParcelUuid(soundModelId), modelParam);
+ return mSoundTriggerSession.queryParameter(new ParcelUuid(soundModelId), modelParam);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
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/non-updatable-api/current.txt b/non-updatable-api/current.txt
index fce66164dae6..7ddc742f65c6 100644
--- a/non-updatable-api/current.txt
+++ b/non-updatable-api/current.txt
@@ -42430,54 +42430,8 @@ package android.service.textservice {
package android.service.voice {
- public class AlwaysOnHotwordDetector {
- method public android.content.Intent createEnrollIntent();
- method public android.content.Intent createReEnrollIntent();
- method public android.content.Intent createUnEnrollIntent();
- method public int getParameter(int);
- method public int getSupportedAudioCapabilities();
- method public int getSupportedRecognitionModes();
- method @Nullable public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int);
- method public int setParameter(int, int);
- method public boolean startRecognition(int);
- method public boolean stopRecognition();
- field public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 1; // 0x1
- field public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 2; // 0x2
- field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0
- field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
- field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
- field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
- field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
- field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
- field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
- field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
- field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
- field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
- field @Deprecated public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
- }
-
- public abstract static class AlwaysOnHotwordDetector.Callback {
- ctor public AlwaysOnHotwordDetector.Callback();
- method public abstract void onAvailabilityChanged(int);
- method public abstract void onDetected(@NonNull android.service.voice.AlwaysOnHotwordDetector.EventPayload);
- method public abstract void onError();
- method public abstract void onRecognitionPaused();
- method public abstract void onRecognitionResumed();
- }
-
- public static class AlwaysOnHotwordDetector.EventPayload {
- method @Nullable public android.media.AudioFormat getCaptureAudioFormat();
- method @Nullable public byte[] getTriggerAudio();
- }
-
- public static final class AlwaysOnHotwordDetector.ModelParamRange {
- method public int getEnd();
- method public int getStart();
- }
-
public class VoiceInteractionService extends android.app.Service {
ctor public VoiceInteractionService();
- method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
method public int getDisabledShowContext();
method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
method public android.os.IBinder onBind(android.content.Intent);
diff --git a/non-updatable-api/system-current.txt b/non-updatable-api/system-current.txt
index 82f2021af425..cadad0fe27d2 100644
--- a/non-updatable-api/system-current.txt
+++ b/non-updatable-api/system-current.txt
@@ -9135,7 +9135,53 @@ package android.service.trust {
package android.service.voice {
+ public class AlwaysOnHotwordDetector {
+ method @Nullable public android.content.Intent createEnrollIntent();
+ method @Nullable public android.content.Intent createReEnrollIntent();
+ method @Nullable public android.content.Intent createUnEnrollIntent();
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int getParameter(int);
+ method public int getSupportedAudioCapabilities();
+ method public int getSupportedRecognitionModes();
+ method @Nullable @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int setParameter(int, int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int);
+ method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean stopRecognition();
+ field public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 1; // 0x1
+ field public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 2; // 0x2
+ field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0
+ field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
+ field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
+ field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
+ field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
+ field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
+ field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
+ field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
+ field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
+ field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
+ field @Deprecated public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
+ }
+
+ public abstract static class AlwaysOnHotwordDetector.Callback {
+ ctor public AlwaysOnHotwordDetector.Callback();
+ method public abstract void onAvailabilityChanged(int);
+ method public abstract void onDetected(@NonNull android.service.voice.AlwaysOnHotwordDetector.EventPayload);
+ method public abstract void onError();
+ method public abstract void onRecognitionPaused();
+ method public abstract void onRecognitionResumed();
+ }
+
+ public static class AlwaysOnHotwordDetector.EventPayload {
+ method @Nullable public android.media.AudioFormat getCaptureAudioFormat();
+ method @Nullable public byte[] getTriggerAudio();
+ }
+
+ public static final class AlwaysOnHotwordDetector.ModelParamRange {
+ method public int getEnd();
+ method public int getStart();
+ }
+
public class VoiceInteractionService extends android.app.Service {
+ method @NonNull public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
method @NonNull @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public final android.media.voice.KeyphraseModelManager createKeyphraseModelManager();
}
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/SoundTriggerHw2Enforcer.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Enforcer.java
index 761858ccd238..eced8940947a 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Enforcer.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Enforcer.java
@@ -21,6 +21,7 @@ import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback;
import android.hardware.soundtrigger.V2_3.ModelParameterRange;
import android.hardware.soundtrigger.V2_3.Properties;
import android.hardware.soundtrigger.V2_3.RecognitionConfig;
+import android.media.soundtrigger_middleware.Status;
import android.os.DeadObjectException;
import android.os.IHwBinder;
import android.os.RemoteException;
@@ -195,10 +196,10 @@ public class SoundTriggerHw2Enforcer implements ISoundTriggerHw2 {
if (e.getCause() instanceof DeadObjectException) {
// Server is dead, no need to reboot.
Log.e(TAG, "HAL died");
- } else {
- Log.e(TAG, "Exception caught from HAL, rebooting HAL");
- rebootHal();
+ throw new RecoverableException(Status.DEAD_OBJECT);
}
+ Log.e(TAG, "Exception caught from HAL, rebooting HAL");
+ rebootHal();
throw e;
}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
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
new file mode 100644
index 000000000000..7b6c6561ccf1
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import 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;
+import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionEvent;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
+import android.media.soundtrigger_middleware.Status;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import java.io.PrintWriter;
+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 a similar
+ * pattern:
+ * <code><pre>
+ * @Override public T method(S arg) {
+ * // Permission check.
+ * enforcePermissions*(...);
+ * return mDelegate.method(arg);
+ * }
+ * </pre></code>
+ *
+ * {@hide}
+ */
+public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddlewareInternal, Dumpable {
+ private static final String TAG = "SoundTriggerMiddlewarePermission";
+
+ private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
+ private final @NonNull Context mContext;
+
+ public SoundTriggerMiddlewarePermission(
+ @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
+ mDelegate = delegate;
+ mContext = context;
+ }
+
+ @Override
+ public @NonNull
+ SoundTriggerModuleDescriptor[] listModules() {
+ Identity identity = getIdentity();
+ enforcePermissionsForPreflight(identity);
+ return mDelegate.listModules();
+ }
+
+ @Override
+ public @NonNull
+ ISoundTriggerModule attach(int handle,
+ @NonNull ISoundTriggerCallback callback) {
+ Identity identity = getIdentity();
+ enforcePermissionsForPreflight(identity);
+ ModuleWrapper wrapper = new ModuleWrapper(identity, callback);
+ return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper()));
+ }
+
+ @Override
+ public void setCaptureState(boolean active) throws RemoteException {
+ // This is an internal call. No permissions needed.
+ mDelegate.setCaptureState(active);
+ }
+
+ // Override toString() in order to have the delegate's ID in it.
+ @Override
+ public String toString() {
+ return Objects.toString(mDelegate);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ throw new UnsupportedOperationException(
+ "This implementation is not inteded to be used directly with Binder.");
+ }
+
+ /**
+ * Get the identity context, or throws an InternalServerError if it has not been established.
+ *
+ * @return The identity.
+ */
+ private static @NonNull
+ Identity getIdentity() {
+ return IdentityContext.getNonNull();
+ }
+
+ /**
+ * 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
+ * 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 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("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("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) {
+ ((Dumpable) mDelegate).dump(pw);
+ }
+ }
+
+ /**
+ * 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 ModuleWrapper extends ISoundTriggerModule.Stub {
+ 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 attach(@NonNull ISoundTriggerModule delegate) {
+ mDelegate = delegate;
+ return this;
+ }
+
+ ISoundTriggerCallback getCallbackWrapper() {
+ return mCallbackWrapper;
+ }
+
+ @Override
+ public int loadModel(@NonNull SoundModel model) throws RemoteException {
+ enforcePermissions();
+ return mDelegate.loadModel(model);
+ }
+
+ @Override
+ public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException {
+ enforcePermissions();
+ return mDelegate.loadPhraseModel(model);
+ }
+
+ @Override
+ public void unloadModel(int modelHandle) throws RemoteException {
+ enforcePermissions();
+ mDelegate.unloadModel(modelHandle);
+
+ }
+
+ @Override
+ public void startRecognition(int modelHandle, @NonNull RecognitionConfig config)
+ throws RemoteException {
+ enforcePermissions();
+ mDelegate.startRecognition(modelHandle, config);
+ }
+
+ @Override
+ public void stopRecognition(int modelHandle) throws RemoteException {
+ enforcePermissions();
+ mDelegate.stopRecognition(modelHandle);
+ }
+
+ @Override
+ public void forceRecognitionEvent(int modelHandle) throws RemoteException {
+ enforcePermissions();
+ mDelegate.forceRecognitionEvent(modelHandle);
+ }
+
+ @Override
+ public void setModelParameter(int modelHandle, int modelParam, int value)
+ throws RemoteException {
+ enforcePermissions();
+ mDelegate.setModelParameter(modelHandle, modelParam, value);
+ }
+
+ @Override
+ public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
+ enforcePermissions();
+ return mDelegate.getModelParameter(modelHandle, modelParam);
+ }
+
+ @Override
+ @Nullable
+ public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
+ throws RemoteException {
+ enforcePermissions();
+ return mDelegate.queryModelParameterSupport(modelHandle,
+ modelParam);
+ }
+
+ @Override
+ public void detach() throws RemoteException {
+ enforcePermissions();
+ mDelegate.detach();
+ }
+
+ // Override toString() in order to have the delegate's ID in it.
+ @Override
+ public String toString() {
+ return Objects.toString(mDelegate);
+ }
+
+ private void enforcePermissions() {
+ enforcePermissionsForPreflight(mOriginatorIdentity);
+ }
+
+ 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 4b464d2d3e04..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,24 +89,58 @@ 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 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+ @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
+ protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
if (mDelegate instanceof Dumpable) {
((Dumpable) mDelegate).dump(fout);
}
}
+ 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;
@@ -108,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();
+ }
}
}
@@ -182,10 +244,11 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
new SoundTriggerMiddlewareService(
new SoundTriggerMiddlewareLogging(
- new SoundTriggerMiddlewareValidation(
- new SoundTriggerMiddlewareImpl(factories,
- new AudioSessionProviderImpl()),
- getContext()))));
+ new SoundTriggerMiddlewarePermission(
+ new SoundTriggerMiddlewareValidation(
+ new SoundTriggerMiddlewareImpl(factories,
+ new AudioSessionProviderImpl())),
+ 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 5d25d2cb554d..95a30c7f0278 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;
@@ -53,9 +52,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
- * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions and
- * correct usage by the client, as well as makes sure that exceptions representing a server
- * malfunction do not get sent to the client.
+ * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces correct usage by
+ * the client, as well as makes sure that exceptions representing a server malfunction get sent to
+ * the client in a consistent manner, which cannot be confused with a client fault.
* <p>
* This is intended to extract the non-business logic out of the underlying implementation and thus
* make it easier to maintain each one of those separate aspects. A design trade-off is being made
@@ -70,8 +69,6 @@ import java.util.concurrent.atomic.AtomicReference;
* pattern:
* <code><pre>
* @Override public T method(S arg) {
- * // Permission check.
- * checkPermissions();
* // Input validation.
* ValidationUtil.validateS(arg);
* synchronized (this) {
@@ -95,9 +92,8 @@ import java.util.concurrent.atomic.AtomicReference;
* with client-server separation.
* <p>
* <b>Exception handling approach:</b><br>
- * We make sure all client faults (permissions, argument and state validation) happen first, and
- * would throw {@link SecurityException}, {@link IllegalArgumentException}/
- * {@link NullPointerException} or {@link
+ * We make sure all client faults (argument and state validation) happen first, and
+ * would throw {@link IllegalArgumentException}/{@link NullPointerException} or {@link
* IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
* will get sent back to the client.<br>
* Once this is done, any subsequent fault is considered either a recoverable (expected) or
@@ -116,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;
@@ -130,13 +126,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
private AtomicReference<Boolean> mCaptureState = new AtomicReference<>();
private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
- private final @NonNull Context mContext;
private Map<Integer, ModuleState> mModules;
public SoundTriggerMiddlewareValidation(
- @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
+ @NonNull ISoundTriggerMiddlewareInternal delegate) {
mDelegate = delegate;
- mContext = context;
}
/**
@@ -169,8 +163,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public @NonNull
SoundTriggerModuleDescriptor[] listModules() {
- // Permission check.
- checkPermissions();
// Input validation (always valid).
synchronized (this) {
@@ -179,9 +171,22 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// From here on, every exception isn't client's fault.
try {
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
- mModules = new HashMap<>(result.length);
- for (SoundTriggerModuleDescriptor desc : result) {
- mModules.put(desc.handle, new ModuleState(desc.properties));
+ if (mModules == null) {
+ mModules = new HashMap<>(result.length);
+ for (SoundTriggerModuleDescriptor desc : result) {
+ mModules.put(desc.handle, new ModuleState(desc.properties));
+ }
+ } else {
+ if (result.length != mModules.size()) {
+ throw new RuntimeException(
+ "listModules must always return the same result.");
+ }
+ for (SoundTriggerModuleDescriptor desc : result) {
+ if (!mModules.containsKey(desc.handle)) {
+ throw new RuntimeException(
+ "listModules must always return the same result.");
+ }
+ }
}
return result;
} catch (Exception e) {
@@ -193,8 +198,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public @NonNull ISoundTriggerModule attach(int handle,
@NonNull ISoundTriggerCallback callback) {
- // Permission check.
- checkPermissions();
// Input validation.
Objects.requireNonNull(callback);
Objects.requireNonNull(callback.asBinder());
@@ -211,10 +214,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);
}
@@ -244,40 +246,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
return mDelegate.toString();
}
- /**
- * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
- * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
- * caller temporarily doesn't have the right permissions to use this service.
- */
- void checkPermissions() {
- enforcePermission(Manifest.permission.RECORD_AUDIO);
- enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
- }
-
- /**
- * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
- * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
- * caller temporarily doesn't have the given permission.
- *
- * @param permission The permission to check.
- */
- void enforcePermission(String permission) {
- final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
- permission);
- switch (status) {
- case PermissionChecker.PERMISSION_GRANTED:
- return;
- case PermissionChecker.PERMISSION_HARD_DENIED:
- throw new SecurityException(
- String.format("Caller must have the %s permission.", permission));
- case PermissionChecker.PERMISSION_SOFT_DENIED:
- throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
- String.format("Caller must have the %s permission.", permission));
- default:
- throw new RuntimeException("Unexpected perimission check result.");
- }
- }
-
@Override
public IBinder asBinder() {
throw new UnsupportedOperationException(
@@ -297,7 +265,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);
}
}
@@ -327,7 +295,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. */
@@ -400,9 +375,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
@@ -413,15 +386,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) {
@@ -431,8 +406,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public int loadModel(@NonNull SoundModel model) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validateGenericModel(model);
@@ -455,8 +428,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validatePhraseModel(model);
@@ -479,8 +450,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void unloadModel(int modelHandle) {
- // Permission check.
- checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -495,7 +464,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.
@@ -510,8 +479,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validateRecognitionConfig(config);
@@ -527,8 +494,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.
@@ -547,8 +513,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void stopRecognition(int modelHandle) {
- // Permission check.
- checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -565,7 +529,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);
@@ -575,8 +544,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void forceRecognitionEvent(int modelHandle) {
- // Permission check.
- checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -593,7 +560,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);
}
@@ -602,8 +573,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void setModelParameter(int modelHandle, int modelParam, int value) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
@@ -630,8 +599,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public int getModelParameter(int modelHandle, int modelParam) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
@@ -659,8 +626,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
@Nullable
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
- // Permission check.
- checkPermissions();
// Input validation.
ValidationUtil.validateModelParameter(modelParam);
@@ -689,8 +654,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
@Override
public void detach() {
- // Permission check.
- checkPermissions();
// Input validation (always valid).
synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -714,14 +677,14 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
- return Objects.toString(mDelegate.toString());
+ return Objects.toString(mDelegate);
}
private void detachInternal() {
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();
@@ -730,7 +693,10 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
void dump(PrintWriter pw) {
if (mState == ModuleStatus.ALIVE) {
- pw.printf("Loaded models for session %s (handle, active)", toString());
+ pw.println("-------------------------------");
+ 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()) {
@@ -741,16 +707,31 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
pw.print(entry.getValue().description);
pw.println();
}
+ pw.println();
} else {
pw.printf("Session %s is dead", toString());
pw.println();
}
}
- ////////////////////////////////////////////////////////////////////////////////////////////
- // 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
@@ -759,18 +740,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);
+ }
+ }
}
}
@@ -783,18 +767,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);
}
}
@@ -810,41 +797,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);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
index 5a587cc9764c..02d978dfdf99 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
@@ -184,11 +184,13 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
@Override
public void serviceDied(long cookie) {
Log.w(TAG, String.format("Underlying HAL driver died."));
- List<ISoundTriggerCallback> callbacks = new ArrayList<>(mActiveSessions.size());
+ List<ISoundTriggerCallback> callbacks;
synchronized (this) {
+ callbacks = new ArrayList<>(mActiveSessions.size());
for (Session session : mActiveSessions) {
callbacks.add(session.moduleDied());
}
+ mActiveSessions.clear();
reset();
}
// Trigger the callbacks outside of the lock to avoid deadlocks.
@@ -431,7 +433,6 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient {
*/
private ISoundTriggerCallback moduleDied() {
ISoundTriggerCallback callback = mCallback;
- removeSession(this);
mCallback = null;
return callback;
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index edbdd4e94ac8..b6506b408714 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -58,7 +58,9 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import java.util.Objects;
import java.util.UUID;
+import java.util.function.BiFunction;
/**
* Helper for {@link SoundTrigger} APIs. Supports two types of models:
@@ -116,6 +118,9 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
private PowerSaveModeListener mPowerSaveModeListener;
+ private final BiFunction<Integer, SoundTrigger.StatusListener, SoundTriggerModule>
+ mModuleProvider;
+
// Handler to process call state changes will delay to allow time for the audio
// and sound trigger HALs to process the end of call notifications
// before we re enable pending recognition requests.
@@ -123,15 +128,17 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
private static final int MSG_CALL_STATE_CHANGED = 0;
private static final int CALL_INACTIVE_MSG_DELAY_MS = 1000;
- SoundTriggerHelper(Context context) {
+ SoundTriggerHelper(Context context,
+ @NonNull BiFunction<Integer, SoundTrigger.StatusListener,
+ SoundTriggerModule> moduleProvider) {
ArrayList <ModuleProperties> modules = new ArrayList<>();
+ mModuleProvider = moduleProvider;
int status = SoundTrigger.listModules(modules);
mContext = context;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mModelDataMap = new HashMap<UUID, ModelData>();
mKeyphraseUuidMap = new HashMap<Integer, UUID>();
- mPhoneStateListener = new MyCallStateListener();
if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size());
mModuleProperties = null;
@@ -145,6 +152,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
if (looper == null) {
looper = Looper.getMainLooper();
}
+ mPhoneStateListener = new MyCallStateListener(looper);
if (looper != null) {
mHandler = new Handler(looper) {
@Override
@@ -264,7 +272,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
private int prepareForRecognition(ModelData modelData) {
if (mModule == null) {
- mModule = SoundTrigger.attachModule(mModuleProperties.getId(), this, null);
+ mModule = mModuleProvider.apply(mModuleProperties.getId(), this);
if (mModule == null) {
Slog.w(TAG, "prepareForRecognition: cannot attach to sound trigger module");
return STATUS_ERROR;
@@ -1042,6 +1050,10 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
class MyCallStateListener extends PhoneStateListener {
+ MyCallStateListener(@NonNull Looper looper) {
+ super(Objects.requireNonNull(looper));
+ }
+
@Override
public void onCallStateChanged(int state, String arg1) {
if (DBG) Slog.d(TAG, "onCallStateChanged: " + state);
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
index 54dffdc4c13a..f77d4907cf03 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
@@ -16,6 +16,7 @@
package com.android.server.soundtrigger;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.ModelParams;
@@ -25,6 +26,7 @@ import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModelParamRange;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.permission.Identity;
import com.android.server.voiceinteraction.VoiceInteractionManagerService;
@@ -35,97 +37,114 @@ import java.io.PrintWriter;
* Provides a local service for managing voice-related recoginition models. This is primarily used
* by the {@link VoiceInteractionManagerService}.
*/
-public abstract class SoundTriggerInternal {
+public interface SoundTriggerInternal {
/**
- * Return codes for {@link #startRecognition(int, KeyphraseSoundModel,
+ * Return codes for {@link Session#startRecognition(int, KeyphraseSoundModel,
* IRecognitionStatusCallback, RecognitionConfig)},
- * {@link #stopRecognition(int, IRecognitionStatusCallback)}
+ * {@link Session#stopRecognition(int, IRecognitionStatusCallback)}
*/
- public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
- public static final int STATUS_OK = SoundTrigger.STATUS_OK;
+ int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
+ int STATUS_OK = SoundTrigger.STATUS_OK;
- /**
- * Starts recognition for the given keyphraseId.
- *
- * @param keyphraseId The identifier of the keyphrase for which
- * the recognition is to be started.
- * @param soundModel The sound model to use for recognition.
- * @param listener The listener for the recognition events related to the given keyphrase.
- * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
- */
- public abstract int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
- IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig);
+ Session attachAsOriginator(@NonNull Identity originatorIdentity);
+
+ Session attachAsMiddleman(@NonNull Identity middlemanIdentity,
+ @NonNull Identity originatorIdentity);
/**
- * Stops recognition for the given {@link Keyphrase} if a recognition is
- * currently active.
- *
- * @param keyphraseId The identifier of the keyphrase for which
- * the recognition is to be stopped.
- * @param listener The listener for the recognition events related to the given keyphrase.
- *
- * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ * Dumps service-wide information.
*/
- public abstract int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener);
+ void dump(FileDescriptor fd, PrintWriter pw, String[] args);
- public abstract ModuleProperties getModuleProperties();
+ interface Session {
+ /**
+ * Starts recognition for the given keyphraseId.
+ *
+ * @param keyphraseId The identifier of the keyphrase for which
+ * the recognition is to be started.
+ * @param soundModel The sound model to use for recognition.
+ * @param listener The listener for the recognition events related to the given
+ * keyphrase.
+ * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ */
+ int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
+ IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig);
- /**
- * Set a model specific {@link ModelParams} with the given value. This
- * parameter will keep its value for the duration the model is loaded regardless of starting and
- * stopping recognition. Once the model is unloaded, the value will be lost.
- * {@link SoundTriggerInternal#queryParameter} should be checked first before calling this
- * method.
- *
- * @param keyphraseId The identifier of the keyphrase for which
- * to modify model parameters
- * @param modelParam {@link ModelParams}
- * @param value Value to set
- * @return - {@link SoundTrigger#STATUS_OK} in case of success
- * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
- * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
- * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
- * if API is not supported by HAL
- */
- public abstract int setParameter(int keyphraseId, @ModelParams int modelParam, int value);
+ /**
+ * Stops recognition for the given {@link Keyphrase} if a recognition is
+ * currently active.
+ *
+ * @param keyphraseId The identifier of the keyphrase for which
+ * the recognition is to be stopped.
+ * @param listener The listener for the recognition events related to the given
+ * keyphrase.
+ * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ */
+ int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener);
- /**
- * Get a model specific {@link ModelParams}. This parameter will keep its value
- * for the duration the model is loaded regardless of starting and stopping recognition.
- * Once the model is unloaded, the value will be lost. If the value is not set, a default
- * value is returned. See ModelParams for parameter default values.
- * {@link SoundTriggerInternal#queryParameter} should be checked first before calling this
- * method.
- *
- * @param keyphraseId The identifier of the keyphrase for which
- * to modify model parameters
- * @param modelParam {@link ModelParams}
- * @return value of parameter
- * @throws UnsupportedOperationException if hal or model do not support this API.
- * queryParameter should be checked first.
- * @throws IllegalArgumentException if invalid model handle or parameter is passed.
- * queryParameter should be checked first.
- */
- public abstract int getParameter(int keyphraseId, @ModelParams int modelParam);
+ ModuleProperties getModuleProperties();
- /**
- * Determine if parameter control is supported for the given model handle.
- * This method should be checked prior to calling {@link SoundTriggerInternal#setParameter}
- * or {@link SoundTriggerInternal#getParameter}.
- *
- * @param keyphraseId The identifier of the keyphrase for which
- * to modify model parameters
- * @param modelParam {@link ModelParams}
- * @return supported range of parameter, null if not supported
- */
- @Nullable
- public abstract ModelParamRange queryParameter(int keyphraseId,
- @ModelParams int modelParam);
+ /**
+ * Set a model specific {@link ModelParams} with the given value. This
+ * parameter will keep its value for the duration the model is loaded regardless of starting
+ * and
+ * stopping recognition. Once the model is unloaded, the value will be lost.
+ * {@link #queryParameter} should be checked first before calling this
+ * method.
+ *
+ * @param keyphraseId The identifier of the keyphrase for which
+ * to modify model parameters
+ * @param modelParam {@link ModelParams}
+ * @param value Value to set
+ * @return - {@link SoundTrigger#STATUS_OK} in case of success
+ * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
+ * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
+ * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
+ * if API is not supported by HAL
+ */
+ int setParameter(int keyphraseId, @ModelParams int modelParam, int value);
- /**
- * Unloads (and stops if running) the given keyphraseId
- */
- public abstract int unloadKeyphraseModel(int keyphaseId);
+ /**
+ * Get a model specific {@link ModelParams}. This parameter will keep its value
+ * for the duration the model is loaded regardless of starting and stopping recognition.
+ * Once the model is unloaded, the value will be lost. If the value is not set, a default
+ * value is returned. See ModelParams for parameter default values.
+ * {{@link #queryParameter}} should be checked first before calling this
+ * method.
+ *
+ * @param keyphraseId The identifier of the keyphrase for which
+ * to modify model parameters
+ * @param modelParam {@link ModelParams}
+ * @return value of parameter
+ * @throws UnsupportedOperationException if hal or model do not support this API.
+ * queryParameter should be checked first.
+ * @throws IllegalArgumentException if invalid model handle or parameter is passed.
+ * queryParameter should be checked first.
+ */
+ int getParameter(int keyphraseId, @ModelParams int modelParam);
+
+ /**
+ * Determine if parameter control is supported for the given model handle.
+ * This method should be checked prior to calling {@link #setParameter}
+ * or {@link #getParameter}.
+ *
+ * @param keyphraseId The identifier of the keyphrase for which
+ * to modify model parameters
+ * @param modelParam {@link ModelParams}
+ * @return supported range of parameter, null if not supported
+ */
+ @Nullable
+ ModelParamRange queryParameter(int keyphraseId,
+ @ModelParams int modelParam);
+
+ /**
+ * Unloads (and stops if running) the given keyphraseId
+ */
+ int unloadKeyphraseModel(int keyphaseId);
- public abstract void dump(FileDescriptor fd, PrintWriter pw, String[] args);
+ /**
+ * Dumps session-wide information.
+ */
+ void dump(FileDescriptor fd, PrintWriter pw, String[] args);
+ }
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 26d46dbd2ab7..a73e73e37fff 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -17,6 +17,7 @@
package com.android.server.soundtrigger;
import static android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE;
+import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_FOREGROUND_SERVICE;
import static android.content.Context.BIND_INCLUDE_CAPABILITIES;
@@ -25,7 +26,6 @@ import static android.content.pm.PackageManager.GET_SERVICES;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
import static android.hardware.soundtrigger.SoundTrigger.STATUS_BAD_VALUE;
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
-import static android.hardware.soundtrigger.SoundTrigger.STATUS_NO_INIT;
import static android.hardware.soundtrigger.SoundTrigger.STATUS_OK;
import static android.provider.Settings.Global.MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY;
import static android.provider.Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT;
@@ -35,6 +35,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -54,6 +55,10 @@ import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
+import android.media.permission.Identity;
+import android.media.permission.IdentityContext;
+import android.media.permission.PermissionUtil;
+import android.media.permission.SafeCloseable;
import android.media.soundtrigger.ISoundTriggerDetectionService;
import android.media.soundtrigger.ISoundTriggerDetectionServiceClient;
import android.media.soundtrigger.SoundTriggerDetectionService;
@@ -75,6 +80,7 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.app.ISoundTriggerSession;
import com.android.server.SystemService;
import java.io.FileDescriptor;
@@ -104,10 +110,6 @@ public class SoundTriggerService extends SystemService {
private final SoundTriggerServiceStub mServiceStub;
private final LocalSoundTriggerService mLocalSoundTriggerService;
private SoundTriggerDbHelper mDbHelper;
- private SoundTriggerHelper mSoundTriggerHelper;
- private final TreeMap<UUID, SoundModel> mLoadedModels;
- private Object mCallbacksLock;
- private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks;
class SoundModelStatTracker {
private class SoundModelStat {
@@ -192,9 +194,6 @@ public class SoundTriggerService extends SystemService {
mContext = context;
mServiceStub = new SoundTriggerServiceStub();
mLocalSoundTriggerService = new LocalSoundTriggerService(context);
- mLoadedModels = new TreeMap<UUID, SoundModel>();
- mCallbacksLock = new Object();
- mCallbacks = new TreeMap<>();
mLock = new Object();
mSoundModelStatTracker = new SoundModelStatTracker();
}
@@ -208,34 +207,54 @@ public class SoundTriggerService extends SystemService {
@Override
public void onBootPhase(int phase) {
Slog.d(TAG, "onBootPhase: " + phase + " : " + isSafeMode());
- if (PHASE_DEVICE_SPECIFIC_SERVICES_READY == phase) {
- if (isSafeMode()) {
- Slog.w(TAG, "not enabling SoundTriggerService in safe mode");
- return;
- }
-
- initSoundTriggerHelper();
- mLocalSoundTriggerService.setSoundTriggerHelper(mSoundTriggerHelper);
- } else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
+ if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
mDbHelper = new SoundTriggerDbHelper(mContext);
}
}
- private synchronized void initSoundTriggerHelper() {
- if (mSoundTriggerHelper == null) {
- mSoundTriggerHelper = new SoundTriggerHelper(mContext);
- }
+ private SoundTriggerHelper newSoundTriggerHelper() {
+ Identity middlemanIdentity = new Identity();
+ middlemanIdentity.packageName = ActivityThread.currentOpPackageName();
+
+ Identity originatorIdentity = IdentityContext.getNonNull();
+
+ return new SoundTriggerHelper(mContext,
+ (moduleId, listener) -> SoundTrigger.attachModuleAsMiddleman(moduleId, listener,
+ null,
+ middlemanIdentity, originatorIdentity));
}
- private synchronized boolean isInitialized() {
- if (mSoundTriggerHelper == null ) {
- Slog.e(TAG, "SoundTriggerHelper not initialized.");
- return false;
+ class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
+ @Override
+ public ISoundTriggerSession attachAsOriginator(Identity originatorIdentity) {
+ try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
+ originatorIdentity)) {
+ return new SoundTriggerSessionStub(newSoundTriggerHelper());
+ }
+ }
+
+ @Override
+ public ISoundTriggerSession attachAsMiddleman(Identity originatorIdentity,
+ Identity middlemanIdentity) {
+ try (SafeCloseable ignored = PermissionUtil.establishIdentityIndirect(mContext,
+ SOUNDTRIGGER_DELEGATE_IDENTITY, middlemanIdentity,
+ originatorIdentity)) {
+ return new SoundTriggerSessionStub(newSoundTriggerHelper());
+ }
}
- return true;
}
- class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
+ class SoundTriggerSessionStub extends ISoundTriggerSession.Stub {
+ private final SoundTriggerHelper mSoundTriggerHelper;
+ private final TreeMap<UUID, SoundModel> mLoadedModels = new TreeMap<>();
+ private final Object mCallbacksLock = new Object();
+ private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks = new TreeMap<>();
+
+ SoundTriggerSessionStub(
+ SoundTriggerHelper soundTriggerHelper) {
+ mSoundTriggerHelper = soundTriggerHelper;
+ }
+
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
@@ -255,7 +274,6 @@ public class SoundTriggerService extends SystemService {
public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
RecognitionConfig config) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_ERROR;
if (DEBUG) {
Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
}
@@ -291,8 +309,6 @@ public class SoundTriggerService extends SystemService {
sEventLogger.log(new SoundTriggerLogger.StringEvent("stopRecognition(): Uuid : "
+ parcelUuid));
- if (!isInitialized()) return STATUS_ERROR;
-
int ret = mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
if (ret == STATUS_OK) {
mSoundModelStatTracker.onStop(parcelUuid.getUuid());
@@ -338,13 +354,11 @@ public class SoundTriggerService extends SystemService {
sEventLogger.log(new SoundTriggerLogger.StringEvent("deleteSoundModel(): id = "
+ soundModelId));
- if (isInitialized()) {
- // Unload the model if it is loaded.
- mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
+ // Unload the model if it is loaded.
+ mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
- // Stop tracking recognition if it is started.
- mSoundModelStatTracker.onStop(soundModelId.getUuid());
- }
+ // Stop tracking recognition if it is started.
+ mSoundModelStatTracker.onStop(soundModelId.getUuid());
mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
}
@@ -352,7 +366,6 @@ public class SoundTriggerService extends SystemService {
@Override
public int loadGenericSoundModel(GenericSoundModel soundModel) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_ERROR;
if (soundModel == null || soundModel.getUuid() == null) {
Slog.e(TAG, "Invalid sound model");
@@ -387,7 +400,6 @@ public class SoundTriggerService extends SystemService {
@Override
public int loadKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_ERROR;
if (soundModel == null || soundModel.getUuid() == null) {
Slog.e(TAG, "Invalid sound model");
@@ -440,7 +452,6 @@ public class SoundTriggerService extends SystemService {
enforceDetectionPermissions(detectionService);
- if (!isInitialized()) return STATUS_ERROR;
if (DEBUG) {
Slog.i(TAG, "startRecognition(): id = " + soundModelId);
}
@@ -510,7 +521,6 @@ public class SoundTriggerService extends SystemService {
@Override
public int stopRecognitionForService(ParcelUuid soundModelId) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_ERROR;
if (DEBUG) {
Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
}
@@ -577,7 +587,6 @@ public class SoundTriggerService extends SystemService {
@Override
public int unloadSoundModel(ParcelUuid soundModelId) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_ERROR;
if (DEBUG) {
Slog.i(TAG, "unloadSoundModel(): id = " + soundModelId);
}
@@ -628,7 +637,6 @@ public class SoundTriggerService extends SystemService {
@Override
public boolean isRecognitionActive(ParcelUuid parcelUuid) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return false;
synchronized (mCallbacksLock) {
IRecognitionStatusCallback callback = mCallbacks.get(parcelUuid.getUuid());
if (callback == null) {
@@ -642,7 +650,6 @@ public class SoundTriggerService extends SystemService {
public int getModelState(ParcelUuid soundModelId) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
int ret = STATUS_ERROR;
- if (!isInitialized()) return ret;
if (DEBUG) {
Slog.i(TAG, "getModelState(): id = " + soundModelId);
}
@@ -681,7 +688,6 @@ public class SoundTriggerService extends SystemService {
@Nullable
public ModuleProperties getModuleProperties() {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return null;
if (DEBUG) {
Slog.i(TAG, "getModuleProperties()");
}
@@ -698,7 +704,6 @@ public class SoundTriggerService extends SystemService {
public int setParameter(ParcelUuid soundModelId,
@ModelParams int modelParam, int value) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return STATUS_NO_INIT;
if (DEBUG) {
Slog.d(TAG, "setParameter(): id=" + soundModelId
+ ", param=" + modelParam
@@ -731,9 +736,6 @@ public class SoundTriggerService extends SystemService {
@ModelParams int modelParam)
throws UnsupportedOperationException, IllegalArgumentException {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) {
- throw new UnsupportedOperationException("SoundTriggerHelper not initialized");
- }
if (DEBUG) {
Slog.d(TAG, "getParameter(): id=" + soundModelId
+ ", param=" + modelParam);
@@ -763,7 +765,6 @@ public class SoundTriggerService extends SystemService {
public ModelParamRange queryParameter(@NonNull ParcelUuid soundModelId,
@ModelParams int modelParam) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
- if (!isInitialized()) return null;
if (DEBUG) {
Slog.d(TAG, "queryParameter(): id=" + soundModelId
+ ", param=" + modelParam);
@@ -788,712 +789,745 @@ public class SoundTriggerService extends SystemService {
return mSoundTriggerHelper.queryParameter(soundModel.getUuid(), modelParam);
}
}
- }
-
- /**
- * Counts the number of operations added in the last 24 hours.
- */
- private static class NumOps {
- private final Object mLock = new Object();
-
- @GuardedBy("mLock")
- private int[] mNumOps = new int[24];
- @GuardedBy("mLock")
- private long mLastOpsHourSinceBoot;
/**
- * Clear buckets of new hours that have elapsed since last operation.
+ * Local end for a {@link SoundTriggerDetectionService}. Operations are queued up and
+ * executed when the service connects.
*
- * <p>I.e. when the last operation was triggered at 1:40 and the current operation was
- * triggered at 4:03, the buckets "2, 3, and 4" are cleared.
+ * <p>If operations take too long they are forcefully aborted.
*
- * @param currentTime Current elapsed time since boot in ns
+ * <p>This also limits the amount of operations in 24 hours.
*/
- void clearOldOps(long currentTime) {
- synchronized (mLock) {
- long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
-
- // Clear buckets of new hours that have elapsed since last operation
- // I.e. when the last operation was triggered at 1:40 and the current
- // operation was triggered at 4:03, the bucket "2, 3, and 4" is cleared
- if (mLastOpsHourSinceBoot != 0) {
- for (long hour = mLastOpsHourSinceBoot + 1; hour <= numHoursSinceBoot; hour++) {
- mNumOps[(int) (hour % 24)] = 0;
+ private class RemoteSoundTriggerDetectionService
+ extends IRecognitionStatusCallback.Stub implements ServiceConnection {
+ private static final int MSG_STOP_ALL_PENDING_OPERATIONS = 1;
+
+ private final Object mRemoteServiceLock = new Object();
+
+ /** UUID of the model the service is started for */
+ private final @NonNull
+ ParcelUuid mPuuid;
+ /** Params passed into the start method for the service */
+ private final @Nullable
+ Bundle mParams;
+ /** Component name passed when starting the service */
+ private final @NonNull
+ ComponentName mServiceName;
+ /** User that started the service */
+ private final @NonNull
+ UserHandle mUser;
+ /** Configuration of the recognition the service is handling */
+ private final @NonNull
+ RecognitionConfig mRecognitionConfig;
+ /** Wake lock keeping the remote service alive */
+ private final @NonNull
+ PowerManager.WakeLock mRemoteServiceWakeLock;
+
+ private final @NonNull
+ Handler mHandler;
+
+ /** Callbacks that are called by the service */
+ private final @NonNull
+ ISoundTriggerDetectionServiceClient mClient;
+
+ /** Operations that are pending because the service is not yet connected */
+ @GuardedBy("mRemoteServiceLock")
+ private final ArrayList<Operation> mPendingOps = new ArrayList<>();
+ /** Operations that have been send to the service but have no yet finished */
+ @GuardedBy("mRemoteServiceLock")
+ private final ArraySet<Integer> mRunningOpIds = new ArraySet<>();
+ /** The number of operations executed in each of the last 24 hours */
+ private final NumOps mNumOps;
+
+ /** The service binder if connected */
+ @GuardedBy("mRemoteServiceLock")
+ private @Nullable
+ ISoundTriggerDetectionService mService;
+ /** Whether the service has been bound */
+ @GuardedBy("mRemoteServiceLock")
+ private boolean mIsBound;
+ /** Whether the service has been destroyed */
+ @GuardedBy("mRemoteServiceLock")
+ private boolean mIsDestroyed;
+ /**
+ * Set once a final op is scheduled. No further ops can be added and the service is
+ * destroyed once the op finishes.
+ */
+ @GuardedBy("mRemoteServiceLock")
+ private boolean mDestroyOnceRunningOpsDone;
+
+ /** Total number of operations performed by this service */
+ @GuardedBy("mRemoteServiceLock")
+ private int mNumTotalOpsPerformed;
+
+ /**
+ * Create a new remote sound trigger detection service. This only binds to the service
+ * when operations are in flight. Each operation has a certain time it can run. Once no
+ * operations are allowed to run anymore, {@link #stopAllPendingOperations() all
+ * operations are aborted and stopped} and the service is disconnected.
+ *
+ * @param modelUuid The UUID of the model the recognition is for
+ * @param params The params passed to each method of the service
+ * @param serviceName The component name of the service
+ * @param user The user of the service
+ * @param config The configuration of the recognition
+ */
+ public RemoteSoundTriggerDetectionService(@NonNull UUID modelUuid,
+ @Nullable Bundle params, @NonNull ComponentName serviceName,
+ @NonNull UserHandle user, @NonNull RecognitionConfig config) {
+ mPuuid = new ParcelUuid(modelUuid);
+ mParams = params;
+ mServiceName = serviceName;
+ mUser = user;
+ mRecognitionConfig = config;
+ mHandler = new Handler(Looper.getMainLooper());
+
+ PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
+ mRemoteServiceWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "RemoteSoundTriggerDetectionService " + mServiceName.getPackageName() + ":"
+ + mServiceName.getClassName());
+
+ synchronized (mLock) {
+ NumOps numOps = mNumOpsPerPackage.get(mServiceName.getPackageName());
+ if (numOps == null) {
+ numOps = new NumOps();
+ mNumOpsPerPackage.put(mServiceName.getPackageName(), numOps);
}
+ mNumOps = numOps;
}
- }
- }
- /**
- * Add a new operation.
- *
- * @param currentTime Current elapsed time since boot in ns
- */
- void addOp(long currentTime) {
- synchronized (mLock) {
- long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
-
- mNumOps[(int) (numHoursSinceBoot % 24)]++;
- mLastOpsHourSinceBoot = numHoursSinceBoot;
- }
- }
-
- /**
- * Get the total operations added in the last 24 hours.
- *
- * @return The total number of operations added in the last 24 hours
- */
- int getOpsAdded() {
- synchronized (mLock) {
- int totalOperationsInLastDay = 0;
- for (int i = 0; i < 24; i++) {
- totalOperationsInLastDay += mNumOps[i];
- }
-
- return totalOperationsInLastDay;
- }
- }
- }
-
- /**
- * A single operation run in a {@link RemoteSoundTriggerDetectionService}.
- *
- * <p>Once the remote service is connected either setup + execute or setup + stop is executed.
- */
- private static class Operation {
- private interface ExecuteOp {
- void run(int opId, ISoundTriggerDetectionService service) throws RemoteException;
- }
-
- private final @Nullable Runnable mSetupOp;
- private final @NonNull ExecuteOp mExecuteOp;
- private final @Nullable Runnable mDropOp;
-
- private Operation(@Nullable Runnable setupOp, @NonNull ExecuteOp executeOp,
- @Nullable Runnable cancelOp) {
- mSetupOp = setupOp;
- mExecuteOp = executeOp;
- mDropOp = cancelOp;
- }
-
- private void setup() {
- if (mSetupOp != null) {
- mSetupOp.run();
- }
- }
-
- void run(int opId, @NonNull ISoundTriggerDetectionService service) throws RemoteException {
- setup();
- mExecuteOp.run(opId, service);
- }
-
- void drop() {
- setup();
-
- if (mDropOp != null) {
- mDropOp.run();
- }
- }
- }
-
- /**
- * Local end for a {@link SoundTriggerDetectionService}. Operations are queued up and executed
- * when the service connects.
- *
- * <p>If operations take too long they are forcefully aborted.
- *
- * <p>This also limits the amount of operations in 24 hours.
- */
- private class RemoteSoundTriggerDetectionService
- extends IRecognitionStatusCallback.Stub implements ServiceConnection {
- private static final int MSG_STOP_ALL_PENDING_OPERATIONS = 1;
-
- private final Object mRemoteServiceLock = new Object();
-
- /** UUID of the model the service is started for */
- private final @NonNull ParcelUuid mPuuid;
- /** Params passed into the start method for the service */
- private final @Nullable Bundle mParams;
- /** Component name passed when starting the service */
- private final @NonNull ComponentName mServiceName;
- /** User that started the service */
- private final @NonNull UserHandle mUser;
- /** Configuration of the recognition the service is handling */
- private final @NonNull RecognitionConfig mRecognitionConfig;
- /** Wake lock keeping the remote service alive */
- private final @NonNull PowerManager.WakeLock mRemoteServiceWakeLock;
-
- private final @NonNull Handler mHandler;
-
- /** Callbacks that are called by the service */
- private final @NonNull ISoundTriggerDetectionServiceClient mClient;
-
- /** Operations that are pending because the service is not yet connected */
- @GuardedBy("mRemoteServiceLock")
- private final ArrayList<Operation> mPendingOps = new ArrayList<>();
- /** Operations that have been send to the service but have no yet finished */
- @GuardedBy("mRemoteServiceLock")
- private final ArraySet<Integer> mRunningOpIds = new ArraySet<>();
- /** The number of operations executed in each of the last 24 hours */
- private final NumOps mNumOps;
-
- /** The service binder if connected */
- @GuardedBy("mRemoteServiceLock")
- private @Nullable ISoundTriggerDetectionService mService;
- /** Whether the service has been bound */
- @GuardedBy("mRemoteServiceLock")
- private boolean mIsBound;
- /** Whether the service has been destroyed */
- @GuardedBy("mRemoteServiceLock")
- private boolean mIsDestroyed;
- /**
- * Set once a final op is scheduled. No further ops can be added and the service is
- * destroyed once the op finishes.
- */
- @GuardedBy("mRemoteServiceLock")
- private boolean mDestroyOnceRunningOpsDone;
-
- /** Total number of operations performed by this service */
- @GuardedBy("mRemoteServiceLock")
- private int mNumTotalOpsPerformed;
-
- /**
- * Create a new remote sound trigger detection service. This only binds to the service when
- * operations are in flight. Each operation has a certain time it can run. Once no
- * operations are allowed to run anymore, {@link #stopAllPendingOperations() all operations
- * are aborted and stopped} and the service is disconnected.
- *
- * @param modelUuid The UUID of the model the recognition is for
- * @param params The params passed to each method of the service
- * @param serviceName The component name of the service
- * @param user The user of the service
- * @param config The configuration of the recognition
- */
- public RemoteSoundTriggerDetectionService(@NonNull UUID modelUuid,
- @Nullable Bundle params, @NonNull ComponentName serviceName, @NonNull UserHandle user,
- @NonNull RecognitionConfig config) {
- mPuuid = new ParcelUuid(modelUuid);
- mParams = params;
- mServiceName = serviceName;
- mUser = user;
- mRecognitionConfig = config;
- mHandler = new Handler(Looper.getMainLooper());
-
- PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
- mRemoteServiceWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
- "RemoteSoundTriggerDetectionService " + mServiceName.getPackageName() + ":"
- + mServiceName.getClassName());
-
- synchronized (mLock) {
- NumOps numOps = mNumOpsPerPackage.get(mServiceName.getPackageName());
- if (numOps == null) {
- numOps = new NumOps();
- mNumOpsPerPackage.put(mServiceName.getPackageName(), numOps);
- }
- mNumOps = numOps;
- }
-
- mClient = new ISoundTriggerDetectionServiceClient.Stub() {
- @Override
- public void onOpFinished(int opId) {
- long token = Binder.clearCallingIdentity();
- try {
- synchronized (mRemoteServiceLock) {
- mRunningOpIds.remove(opId);
-
- if (mRunningOpIds.isEmpty() && mPendingOps.isEmpty()) {
- if (mDestroyOnceRunningOpsDone) {
- destroy();
- } else {
- disconnectLocked();
+ mClient = new ISoundTriggerDetectionServiceClient.Stub() {
+ @Override
+ public void onOpFinished(int opId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mRemoteServiceLock) {
+ mRunningOpIds.remove(opId);
+
+ if (mRunningOpIds.isEmpty() && mPendingOps.isEmpty()) {
+ if (mDestroyOnceRunningOpsDone) {
+ destroy();
+ } else {
+ disconnectLocked();
+ }
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- } finally {
- Binder.restoreCallingIdentity(token);
}
- }
- };
- }
+ };
+ }
- @Override
- public boolean pingBinder() {
- return !(mIsDestroyed || mDestroyOnceRunningOpsDone);
- }
+ @Override
+ public boolean pingBinder() {
+ return !(mIsDestroyed || mDestroyOnceRunningOpsDone);
+ }
- /**
- * Disconnect from the service, but allow to re-connect when new operations are triggered.
- */
- @GuardedBy("mRemoteServiceLock")
- private void disconnectLocked() {
- if (mService != null) {
- try {
- mService.removeClient(mPuuid);
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Cannot remove client", e);
+ /**
+ * Disconnect from the service, but allow to re-connect when new operations are
+ * triggered.
+ */
+ @GuardedBy("mRemoteServiceLock")
+ private void disconnectLocked() {
+ if (mService != null) {
+ try {
+ mService.removeClient(mPuuid);
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Cannot remove client", e);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": Cannot remove client"));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": Cannot remove client"));
- }
+ }
- mService = null;
- }
+ mService = null;
+ }
- if (mIsBound) {
- mContext.unbindService(RemoteSoundTriggerDetectionService.this);
- mIsBound = false;
+ if (mIsBound) {
+ mContext.unbindService(RemoteSoundTriggerDetectionService.this);
+ mIsBound = false;
- synchronized (mCallbacksLock) {
- mRemoteServiceWakeLock.release();
+ synchronized (mCallbacksLock) {
+ mRemoteServiceWakeLock.release();
+ }
}
}
- }
- /**
- * Disconnect, do not allow to reconnect to the service. All further operations will be
- * dropped.
- */
- private void destroy() {
- if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
+ /**
+ * Disconnect, do not allow to reconnect to the service. All further operations will be
+ * dropped.
+ */
+ private void destroy() {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + ": destroy"));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + ": destroy"));
- synchronized (mRemoteServiceLock) {
- disconnectLocked();
+ synchronized (mRemoteServiceLock) {
+ disconnectLocked();
- mIsDestroyed = true;
- }
+ mIsDestroyed = true;
+ }
- // The callback is removed before the flag is set
- if (!mDestroyOnceRunningOpsDone) {
- synchronized (mCallbacksLock) {
- mCallbacks.remove(mPuuid.getUuid());
+ // The callback is removed before the flag is set
+ if (!mDestroyOnceRunningOpsDone) {
+ synchronized (mCallbacksLock) {
+ mCallbacks.remove(mPuuid.getUuid());
+ }
}
}
- }
- /**
- * Stop all pending operations and then disconnect for the service.
- */
- private void stopAllPendingOperations() {
- synchronized (mRemoteServiceLock) {
- if (mIsDestroyed) {
- return;
- }
+ /**
+ * Stop all pending operations and then disconnect for the service.
+ */
+ private void stopAllPendingOperations() {
+ synchronized (mRemoteServiceLock) {
+ if (mIsDestroyed) {
+ return;
+ }
- if (mService != null) {
- int numOps = mRunningOpIds.size();
- for (int i = 0; i < numOps; i++) {
- try {
- mService.onStopOperation(mPuuid, mRunningOpIds.valueAt(i));
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Could not stop operation "
- + mRunningOpIds.valueAt(i), e);
+ if (mService != null) {
+ int numOps = mRunningOpIds.size();
+ for (int i = 0; i < numOps; i++) {
+ try {
+ mService.onStopOperation(mPuuid, mRunningOpIds.valueAt(i));
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Could not stop operation "
+ + mRunningOpIds.valueAt(i), e);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": Could not stop operation " + mRunningOpIds.valueAt(i)));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": Could not stop operation " + mRunningOpIds.valueAt(
+ i)));
+ }
}
+
+ mRunningOpIds.clear();
}
- mRunningOpIds.clear();
+ disconnectLocked();
}
-
- disconnectLocked();
}
- }
- /**
- * Verify that the service has the expected properties and then bind to the service
- */
- private void bind() {
- long token = Binder.clearCallingIdentity();
- try {
- Intent i = new Intent();
- i.setComponent(mServiceName);
+ /**
+ * Verify that the service has the expected properties and then bind to the service
+ */
+ private void bind() {
+ long token = Binder.clearCallingIdentity();
+ try {
+ Intent i = new Intent();
+ i.setComponent(mServiceName);
- ResolveInfo ri = mContext.getPackageManager().resolveServiceAsUser(i,
- GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
- mUser.getIdentifier());
+ ResolveInfo ri = mContext.getPackageManager().resolveServiceAsUser(i,
+ GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
+ mUser.getIdentifier());
- if (ri == null) {
- Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
+ if (ri == null) {
+ Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": " + mServiceName + " not found"));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": " + mServiceName + " not found"));
- return;
- }
+ return;
+ }
- if (!BIND_SOUND_TRIGGER_DETECTION_SERVICE
- .equals(ri.serviceInfo.permission)) {
- Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
- + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
+ if (!BIND_SOUND_TRIGGER_DETECTION_SERVICE
+ .equals(ri.serviceInfo.permission)) {
+ Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
+ + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": " + mServiceName + " does not require "
- + BIND_SOUND_TRIGGER_DETECTION_SERVICE));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": " + mServiceName + " does not require "
+ + BIND_SOUND_TRIGGER_DETECTION_SERVICE));
- return;
- }
+ return;
+ }
- mIsBound = mContext.bindServiceAsUser(i, this,
- BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES,
- mUser);
+ mIsBound = mContext.bindServiceAsUser(i, this,
+ BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES,
+ mUser);
- if (mIsBound) {
- mRemoteServiceWakeLock.acquire();
- } else {
- Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
+ if (mIsBound) {
+ mRemoteServiceWakeLock.acquire();
+ } else {
+ Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": Could not bind to " + mServiceName));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": Could not bind to " + mServiceName));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- } finally {
- Binder.restoreCallingIdentity(token);
}
- }
- /**
- * Run an operation (i.e. send it do the service). If the service is not connected, this
- * binds the service and then runs the operation once connected.
- *
- * @param op The operation to run
- */
- private void runOrAddOperation(Operation op) {
- synchronized (mRemoteServiceLock) {
- if (mIsDestroyed || mDestroyOnceRunningOpsDone) {
- Slog.w(TAG, mPuuid + ": Dropped operation as already destroyed or marked for "
- + "destruction");
+ /**
+ * Run an operation (i.e. send it do the service). If the service is not connected, this
+ * binds the service and then runs the operation once connected.
+ *
+ * @param op The operation to run
+ */
+ private void runOrAddOperation(Operation op) {
+ synchronized (mRemoteServiceLock) {
+ if (mIsDestroyed || mDestroyOnceRunningOpsDone) {
+ Slog.w(TAG,
+ mPuuid + ": Dropped operation as already destroyed or marked for "
+ + "destruction");
+
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ":Dropped operation as already destroyed or marked for "
+ + "destruction"));
+
+ op.drop();
+ return;
+ }
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ":Dropped operation as already destroyed or marked for destruction"));
+ if (mService == null) {
+ mPendingOps.add(op);
- op.drop();
- return;
- }
+ if (!mIsBound) {
+ bind();
+ }
+ } else {
+ long currentTime = System.nanoTime();
+ mNumOps.clearOldOps(currentTime);
- if (mService == null) {
- mPendingOps.add(op);
+ // Drop operation if too many were executed in the last 24 hours.
+ int opsAllowed = Settings.Global.getInt(mContext.getContentResolver(),
+ MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY,
+ Integer.MAX_VALUE);
- if (!mIsBound) {
- bind();
- }
- } else {
- long currentTime = System.nanoTime();
- mNumOps.clearOldOps(currentTime);
-
- // Drop operation if too many were executed in the last 24 hours.
- int opsAllowed = Settings.Global.getInt(mContext.getContentResolver(),
- MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY,
- Integer.MAX_VALUE);
-
- // As we currently cannot dropping an op safely, disable throttling
- int opsAdded = mNumOps.getOpsAdded();
- if (false && mNumOps.getOpsAdded() >= opsAllowed) {
- try {
- if (DEBUG || opsAllowed + 10 > opsAdded) {
- Slog.w(TAG, mPuuid + ": Dropped operation as too many operations "
- + "were run in last 24 hours");
+ // As we currently cannot dropping an op safely, disable throttling
+ int opsAdded = mNumOps.getOpsAdded();
+ if (false && mNumOps.getOpsAdded() >= opsAllowed) {
+ try {
+ if (DEBUG || opsAllowed + 10 > opsAdded) {
+ Slog.w(TAG,
+ mPuuid + ": Dropped operation as too many operations "
+ + "were run in last 24 hours");
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": Dropped operation as too many operations "
- + "were run in last 24 hours"));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": Dropped operation as too many operations "
+ + "were run in last 24 hours"));
- }
+ }
- op.drop();
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Could not drop operation", e);
+ op.drop();
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Could not drop operation", e);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ ": Could not drop operation"));
- }
- } else {
- mNumOps.addOp(currentTime);
+ }
+ } else {
+ mNumOps.addOp(currentTime);
- // Find a free opID
- int opId = mNumTotalOpsPerformed;
- do {
- mNumTotalOpsPerformed++;
- } while (mRunningOpIds.contains(opId));
+ // Find a free opID
+ int opId = mNumTotalOpsPerformed;
+ do {
+ mNumTotalOpsPerformed++;
+ } while (mRunningOpIds.contains(opId));
- // Run OP
- try {
- if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
+ // Run OP
+ try {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ ": runOp " + opId));
- op.run(opId, mService);
- mRunningOpIds.add(opId);
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+ op.run(opId, mService);
+ mRunningOpIds.add(opId);
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ ": Could not run operation " + opId));
+ }
}
- }
- // Unbind from service if no operations are left (i.e. if the operation failed)
- if (mPendingOps.isEmpty() && mRunningOpIds.isEmpty()) {
- if (mDestroyOnceRunningOpsDone) {
- destroy();
+ // Unbind from service if no operations are left (i.e. if the operation
+ // failed)
+ if (mPendingOps.isEmpty() && mRunningOpIds.isEmpty()) {
+ if (mDestroyOnceRunningOpsDone) {
+ destroy();
+ } else {
+ disconnectLocked();
+ }
} else {
- disconnectLocked();
+ mHandler.removeMessages(MSG_STOP_ALL_PENDING_OPERATIONS);
+ mHandler.sendMessageDelayed(obtainMessage(
+ RemoteSoundTriggerDetectionService::stopAllPendingOperations,
+ this)
+ .setWhat(MSG_STOP_ALL_PENDING_OPERATIONS),
+ Settings.Global.getLong(mContext.getContentResolver(),
+ SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT,
+ Long.MAX_VALUE));
}
- } else {
- mHandler.removeMessages(MSG_STOP_ALL_PENDING_OPERATIONS);
- mHandler.sendMessageDelayed(obtainMessage(
- RemoteSoundTriggerDetectionService::stopAllPendingOperations, this)
- .setWhat(MSG_STOP_ALL_PENDING_OPERATIONS),
- Settings.Global.getLong(mContext.getContentResolver(),
- SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT,
- Long.MAX_VALUE));
}
}
}
- }
-
- @Override
- public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
- Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
- + ")");
-
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + "->" + mServiceName
- + ": IGNORED onKeyphraseDetected(" + event + ")"));
- }
+ @Override
+ public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
+ Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
+ + ")");
- /**
- * Create an AudioRecord enough for starting and releasing the data buffered for the event.
- *
- * @param event The event that was received
- * @return The initialized AudioRecord
- */
- private @NonNull AudioRecord createAudioRecordForEvent(
- @NonNull SoundTrigger.GenericRecognitionEvent event)
- throws IllegalArgumentException, UnsupportedOperationException {
- AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
- attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
- AudioAttributes attributes = attributesBuilder.build();
-
- AudioFormat originalFormat = event.getCaptureFormat();
-
- sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
-
- return (new AudioRecord.Builder())
- .setAudioAttributes(attributes)
- .setAudioFormat((new AudioFormat.Builder())
- .setChannelMask(originalFormat.getChannelMask())
- .setEncoding(originalFormat.getEncoding())
- .setSampleRate(originalFormat.getSampleRate())
- .build())
- .setSessionId(event.getCaptureSession())
- .build();
- }
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + "->" + mServiceName
+ + ": IGNORED onKeyphraseDetected(" + event + ")"));
+ }
- @Override
- public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
- if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
-
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": Generic sound trigger event: " + event));
-
- runOrAddOperation(new Operation(
- // always execute:
- () -> {
- if (!mRecognitionConfig.allowMultipleTriggers) {
- // Unregister this remoteService once op is done
- synchronized (mCallbacksLock) {
- mCallbacks.remove(mPuuid.getUuid());
- }
- mDestroyOnceRunningOpsDone = true;
- }
- },
- // execute if not throttled:
- (opId, service) -> service.onGenericRecognitionEvent(mPuuid, opId, event),
- // execute if throttled:
- () -> {
- if (event.isCaptureAvailable()) {
- try {
- AudioRecord capturedData = createAudioRecordForEvent(event);
- capturedData.startRecording();
- capturedData.release();
- } catch (IllegalArgumentException | UnsupportedOperationException e) {
- Slog.w(TAG, mPuuid + ": createAudioRecordForEvent(" + event
- + "), failed to create AudioRecord");
- }
- }
- }));
- }
+ /**
+ * Create an AudioRecord enough for starting and releasing the data buffered for the event.
+ *
+ * @param event The event that was received
+ * @return The initialized AudioRecord
+ */
+ private @NonNull AudioRecord createAudioRecordForEvent(
+ @NonNull SoundTrigger.GenericRecognitionEvent event)
+ throws IllegalArgumentException, UnsupportedOperationException {
+ AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
+ attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
+ AudioAttributes attributes = attributesBuilder.build();
+
+ AudioFormat originalFormat = event.getCaptureFormat();
+
+ sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
+
+ return (new AudioRecord.Builder())
+ .setAudioAttributes(attributes)
+ .setAudioFormat((new AudioFormat.Builder())
+ .setChannelMask(originalFormat.getChannelMask())
+ .setEncoding(originalFormat.getEncoding())
+ .setSampleRate(originalFormat.getSampleRate())
+ .build())
+ .setSessionId(event.getCaptureSession())
+ .build();
+ }
- @Override
- public void onError(int status) {
- if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
+ @Override
+ public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": onError: " + status));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": Generic sound trigger event: " + event));
- runOrAddOperation(
- new Operation(
- // always execute:
- () -> {
+ runOrAddOperation(new Operation(
+ // always execute:
+ () -> {
+ if (!mRecognitionConfig.allowMultipleTriggers) {
// Unregister this remoteService once op is done
synchronized (mCallbacksLock) {
mCallbacks.remove(mPuuid.getUuid());
}
mDestroyOnceRunningOpsDone = true;
- },
- // execute if not throttled:
- (opId, service) -> service.onError(mPuuid, opId, status),
- // nothing to do if throttled
- null));
- }
+ }
+ },
+ // execute if not throttled:
+ (opId, service) -> service.onGenericRecognitionEvent(mPuuid, opId, event),
+ // execute if throttled:
+ () -> {
+ if (event.isCaptureAvailable()) {
+ try {
+ AudioRecord capturedData = createAudioRecordForEvent(event);
+ capturedData.startRecording();
+ capturedData.release();
+ } catch (IllegalArgumentException | UnsupportedOperationException e) {
+ Slog.w(TAG, mPuuid + ": createAudioRecordForEvent(" + event
+ + "), failed to create AudioRecord");
+ }
+ }
+ }));
+ }
- @Override
- public void onRecognitionPaused() {
- Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
+ @Override
+ public void onError(int status) {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
+
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": onError: " + status));
+
+ runOrAddOperation(
+ new Operation(
+ // always execute:
+ () -> {
+ // Unregister this remoteService once op is done
+ synchronized (mCallbacksLock) {
+ mCallbacks.remove(mPuuid.getUuid());
+ }
+ mDestroyOnceRunningOpsDone = true;
+ },
+ // execute if not throttled:
+ (opId, service) -> service.onError(mPuuid, opId, status),
+ // nothing to do if throttled
+ null));
+ }
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + "->" + mServiceName + ": IGNORED onRecognitionPaused"));
+ @Override
+ public void onRecognitionPaused() {
+ Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
- }
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + "->" + mServiceName + ": IGNORED onRecognitionPaused"));
- @Override
- public void onRecognitionResumed() {
- Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
+ }
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + "->" + mServiceName + ": IGNORED onRecognitionResumed"));
+ @Override
+ public void onRecognitionResumed() {
+ Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
- }
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + "->" + mServiceName + ": IGNORED onRecognitionResumed"));
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
+ }
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": onServiceConnected(" + service + ")"));
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
- synchronized (mRemoteServiceLock) {
- mService = ISoundTriggerDetectionService.Stub.asInterface(service);
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": onServiceConnected(" + service + ")"));
- try {
- mService.setClient(mPuuid, mParams, mClient);
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Could not init " + mServiceName, e);
- return;
+ synchronized (mRemoteServiceLock) {
+ mService = ISoundTriggerDetectionService.Stub.asInterface(service);
+
+ try {
+ mService.setClient(mPuuid, mParams, mClient);
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Could not init " + mServiceName, e);
+ return;
+ }
+
+ while (!mPendingOps.isEmpty()) {
+ runOrAddOperation(mPendingOps.remove(0));
+ }
}
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
- while (!mPendingOps.isEmpty()) {
- runOrAddOperation(mPendingOps.remove(0));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": onServiceDisconnected"));
+
+ synchronized (mRemoteServiceLock) {
+ mService = null;
}
}
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
+ @Override
+ public void onBindingDied(ComponentName name) {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": onServiceDisconnected"));
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+ + ": onBindingDied"));
- synchronized (mRemoteServiceLock) {
- mService = null;
+ synchronized (mRemoteServiceLock) {
+ destroy();
+ }
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
+
+ sEventLogger.log(new SoundTriggerLogger.StringEvent(name + " for model "
+ + mPuuid + " returned a null binding"));
+
+ synchronized (mRemoteServiceLock) {
+ disconnectLocked();
+ }
}
}
+ }
- @Override
- public void onBindingDied(ComponentName name) {
- if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
+ /**
+ * Counts the number of operations added in the last 24 hours.
+ */
+ private static class NumOps {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private int[] mNumOps = new int[24];
+ @GuardedBy("mLock")
+ private long mLastOpsHourSinceBoot;
- sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
- + ": onBindingDied"));
+ /**
+ * Clear buckets of new hours that have elapsed since last operation.
+ *
+ * <p>I.e. when the last operation was triggered at 1:40 and the current operation was
+ * triggered at 4:03, the buckets "2, 3, and 4" are cleared.
+ *
+ * @param currentTime Current elapsed time since boot in ns
+ */
+ void clearOldOps(long currentTime) {
+ synchronized (mLock) {
+ long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
- synchronized (mRemoteServiceLock) {
- destroy();
+ // Clear buckets of new hours that have elapsed since last operation
+ // I.e. when the last operation was triggered at 1:40 and the current
+ // operation was triggered at 4:03, the bucket "2, 3, and 4" is cleared
+ if (mLastOpsHourSinceBoot != 0) {
+ for (long hour = mLastOpsHourSinceBoot + 1; hour <= numHoursSinceBoot; hour++) {
+ mNumOps[(int) (hour % 24)] = 0;
+ }
+ }
}
}
- @Override
- public void onNullBinding(ComponentName name) {
- Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
+ /**
+ * Add a new operation.
+ *
+ * @param currentTime Current elapsed time since boot in ns
+ */
+ void addOp(long currentTime) {
+ synchronized (mLock) {
+ long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
- sEventLogger.log(new SoundTriggerLogger.StringEvent(name + " for model "
- + mPuuid + " returned a null binding"));
+ mNumOps[(int) (numHoursSinceBoot % 24)]++;
+ mLastOpsHourSinceBoot = numHoursSinceBoot;
+ }
+ }
- synchronized (mRemoteServiceLock) {
- disconnectLocked();
+ /**
+ * Get the total operations added in the last 24 hours.
+ *
+ * @return The total number of operations added in the last 24 hours
+ */
+ int getOpsAdded() {
+ synchronized (mLock) {
+ int totalOperationsInLastDay = 0;
+ for (int i = 0; i < 24; i++) {
+ totalOperationsInLastDay += mNumOps[i];
+ }
+
+ return totalOperationsInLastDay;
}
}
}
- public final class LocalSoundTriggerService extends SoundTriggerInternal {
- private final Context mContext;
- private SoundTriggerHelper mSoundTriggerHelper;
-
- LocalSoundTriggerService(Context context) {
- mContext = context;
+ /**
+ * A single operation run in a {@link RemoteSoundTriggerDetectionService}.
+ *
+ * <p>Once the remote service is connected either setup + execute or setup + stop is executed.
+ */
+ private static class Operation {
+ private interface ExecuteOp {
+ void run(int opId, ISoundTriggerDetectionService service) throws RemoteException;
}
- synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
- mSoundTriggerHelper = helper;
+ private final @Nullable Runnable mSetupOp;
+ private final @NonNull ExecuteOp mExecuteOp;
+ private final @Nullable Runnable mDropOp;
+
+ private Operation(@Nullable Runnable setupOp, @NonNull ExecuteOp executeOp,
+ @Nullable Runnable cancelOp) {
+ mSetupOp = setupOp;
+ mExecuteOp = executeOp;
+ mDropOp = cancelOp;
}
- @Override
- public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
- IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel, listener,
- recognitionConfig);
+ private void setup() {
+ if (mSetupOp != null) {
+ mSetupOp.run();
+ }
}
- @Override
- public synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
+ void run(int opId, @NonNull ISoundTriggerDetectionService service) throws RemoteException {
+ setup();
+ mExecuteOp.run(opId, service);
}
- @Override
- public ModuleProperties getModuleProperties() {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.getModuleProperties();
+ void drop() {
+ setup();
+
+ if (mDropOp != null) {
+ mDropOp.run();
+ }
}
+ }
- @Override
- public int setParameter(int keyphraseId, @ModelParams int modelParam, int value) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.setKeyphraseParameter(keyphraseId, modelParam, value);
+ public final class LocalSoundTriggerService implements SoundTriggerInternal {
+ private final Context mContext;
+ LocalSoundTriggerService(Context context) {
+ mContext = context;
}
- @Override
- public int getParameter(int keyphraseId, @ModelParams int modelParam) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.getKeyphraseParameter(keyphraseId, modelParam);
+ private class SessionImpl implements Session {
+ private final @NonNull SoundTriggerHelper mSoundTriggerHelper;
+
+ private SessionImpl(
+ @NonNull SoundTriggerHelper soundTriggerHelper) {
+ mSoundTriggerHelper = soundTriggerHelper;
+ }
+
+ @Override
+ public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
+ IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
+ return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel,
+ listener,
+ recognitionConfig);
+ }
+
+ @Override
+ public synchronized int stopRecognition(int keyphraseId,
+ IRecognitionStatusCallback listener) {
+ return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
+ }
+
+ @Override
+ public ModuleProperties getModuleProperties() {
+ return mSoundTriggerHelper.getModuleProperties();
+ }
+
+ @Override
+ public int setParameter(int keyphraseId, @ModelParams int modelParam, int value) {
+ return mSoundTriggerHelper.setKeyphraseParameter(keyphraseId, modelParam, value);
+ }
+
+ @Override
+ public int getParameter(int keyphraseId, @ModelParams int modelParam) {
+ return mSoundTriggerHelper.getKeyphraseParameter(keyphraseId, modelParam);
+ }
+
+ @Override
+ @Nullable
+ public ModelParamRange queryParameter(int keyphraseId, @ModelParams int modelParam) {
+ return mSoundTriggerHelper.queryKeyphraseParameter(keyphraseId, modelParam);
+ }
+
+ @Override
+ public int unloadKeyphraseModel(int keyphraseId) {
+ return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mSoundTriggerHelper.dump(fd, pw, args);
+ }
}
@Override
- @Nullable
- public ModelParamRange queryParameter(int keyphraseId, @ModelParams int modelParam) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.queryKeyphraseParameter(keyphraseId, modelParam);
+ public Session attachAsOriginator(@NonNull Identity originatorIdentity) {
+ try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
+ originatorIdentity)) {
+ return new SessionImpl(newSoundTriggerHelper());
+ }
}
@Override
- public int unloadKeyphraseModel(int keyphraseId) {
- if (!isInitialized()) throw new UnsupportedOperationException();
- return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
+ public Session attachAsMiddleman(@NonNull Identity middlemanIdentity,
+ @NonNull Identity originatorIdentity) {
+ try (SafeCloseable ignored = PermissionUtil.establishIdentityIndirect(mContext,
+ SOUNDTRIGGER_DELEGATE_IDENTITY, middlemanIdentity, originatorIdentity)) {
+ return new SessionImpl(newSoundTriggerHelper());
+ }
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!isInitialized()) return;
- mSoundTriggerHelper.dump(fd, pw, args);
// log
sEventLogger.dump(pw);
@@ -1503,18 +1537,6 @@ public class SoundTriggerService extends SystemService {
// stats
mSoundModelStatTracker.dump(pw);
}
-
- private synchronized boolean isInitialized() {
- if (mSoundTriggerHelper == null ) {
- Slog.e(TAG, "SoundTriggerHelper not initialized.");
-
- sEventLogger.log(new SoundTriggerLogger.StringEvent(
- "SoundTriggerHelper not initialized."));
-
- return false;
- }
- return true;
- }
}
private void enforceCallingPermission(String permission) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index a2215ceed36a..f4fc185d3b5c 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -23,6 +23,7 @@ import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.role.OnRoleHoldersChangedListener;
import android.app.role.RoleManager;
@@ -48,6 +49,11 @@ import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModelParamRange;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.Identity;
+import android.media.permission.IdentityContext;
+import android.media.permission.PermissionUtil;
+import android.media.permission.SafeCloseable;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -69,6 +75,7 @@ import android.service.voice.VoiceInteractionServiceInfo;
import android.service.voice.VoiceInteractionSession;
import android.speech.RecognitionService;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
@@ -78,6 +85,7 @@ import com.android.internal.app.IVoiceActionCheckCallback;
import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.internal.app.IVoiceInteractionSessionListener;
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
+import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
@@ -93,7 +101,10 @@ import com.android.server.wm.ActivityTaskManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.LinkedList;
import java.util.List;
+import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -111,7 +122,8 @@ public class VoiceInteractionManagerService extends SystemService {
final ActivityManagerInternal mAmInternal;
final ActivityTaskManagerInternal mAtmInternal;
final UserManagerInternal mUserManagerInternal;
- final ArraySet<Integer> mLoadedKeyphraseIds = new ArraySet<>();
+ final ArrayMap<Integer, VoiceInteractionManagerServiceStub.SoundTriggerSession>
+ mLoadedKeyphraseIds = new ArrayMap<>();
ShortcutServiceInternal mShortcutServiceInternal;
SoundTriggerInternal mSoundTriggerInternal;
@@ -236,12 +248,28 @@ public class VoiceInteractionManagerService extends SystemService {
private boolean mTemporarilyDisabled;
private final boolean mEnableService;
+ private final List<WeakReference<SoundTriggerSession>> mSessions = new LinkedList<>();
VoiceInteractionManagerServiceStub() {
mEnableService = shouldEnableService(mContext);
new RoleObserver(mContext.getMainExecutor());
}
+ @Override
+ public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator(
+ @NonNull Identity originatorIdentity) {
+ Objects.requireNonNull(originatorIdentity);
+ try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
+ originatorIdentity)) {
+ SoundTriggerSession session = new SoundTriggerSession(
+ mSoundTriggerInternal.attachAsOriginator(IdentityContext.getNonNull()));
+ synchronized (mSessions) {
+ mSessions.add(new WeakReference<>(session));
+ }
+ return session;
+ }
+ }
+
// TODO: VI Make sure the caller is the current user or profile
void startLocalVoiceInteraction(final IBinder token, Bundle options) {
if (mImpl == null) return;
@@ -1014,12 +1042,15 @@ public class VoiceInteractionManagerService extends SystemService {
final long caller = Binder.clearCallingIdentity();
boolean deleted = false;
try {
- int unloadStatus = mSoundTriggerInternal.unloadKeyphraseModel(keyphraseId);
- if (unloadStatus != SoundTriggerInternal.STATUS_OK) {
- Slog.w(TAG, "Unable to unload keyphrase sound model:" + unloadStatus);
+ SoundTriggerSession session = mLoadedKeyphraseIds.get(keyphraseId);
+ if (session != null) {
+ int unloadStatus = session.unloadKeyphraseModel(keyphraseId);
+ if (unloadStatus != SoundTriggerInternal.STATUS_OK) {
+ Slog.w(TAG, "Unable to unload keyphrase sound model:" + unloadStatus);
+ }
+ deleted = mDbHelper.deleteKeyphraseSoundModel(keyphraseId, callingUserId,
+ bcp47Locale);
}
- deleted = mDbHelper.deleteKeyphraseSoundModel(
- keyphraseId, callingUserId, bcp47Locale);
return deleted ? SoundTriggerInternal.STATUS_OK : SoundTriggerInternal.STATUS_ERROR;
} finally {
if (deleted) {
@@ -1092,132 +1123,145 @@ public class VoiceInteractionManagerService extends SystemService {
return null;
}
- @Override
- public ModuleProperties getDspModuleProperties() {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ class SoundTriggerSession extends IVoiceInteractionSoundTriggerSession.Stub {
+ final SoundTriggerInternal.Session mSession;
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerInternal.getModuleProperties();
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
+ SoundTriggerSession(
+ SoundTriggerInternal.Session session) {
+ mSession = session;
}
- }
- @Override
- public int startRecognition(int keyphraseId, String bcp47Locale,
- IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ @Override
+ public ModuleProperties getDspModuleProperties() {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
- if (callback == null || recognitionConfig == null || bcp47Locale == null) {
- throw new IllegalArgumentException("Illegal argument(s) in startRecognition");
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSession.getModuleProperties();
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
}
- final int callingUserId = UserHandle.getCallingUserId();
- final long caller = Binder.clearCallingIdentity();
- try {
- KeyphraseSoundModel soundModel =
- mDbHelper.getKeyphraseSoundModel(keyphraseId, callingUserId, bcp47Locale);
- if (soundModel == null
- || soundModel.getUuid() == null
- || soundModel.getKeyphrases() == null) {
- Slog.w(TAG, "No matching sound model found in startRecognition");
- return SoundTriggerInternal.STATUS_ERROR;
- } else {
- // Regardless of the status of the start recognition, we need to make sure
- // that we unload this model if needed later.
- synchronized (this) {
- mLoadedKeyphraseIds.add(keyphraseId);
+ @Override
+ public int startRecognition(int keyphraseId, String bcp47Locale,
+ IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
+
+ if (callback == null || recognitionConfig == null || bcp47Locale == null) {
+ throw new IllegalArgumentException("Illegal argument(s) in startRecognition");
}
- return mSoundTriggerInternal.startRecognition(
- keyphraseId, soundModel, callback, recognitionConfig);
}
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
- }
- @Override
- public int stopRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ final int callingUserId = UserHandle.getCallingUserId();
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ KeyphraseSoundModel soundModel =
+ mDbHelper.getKeyphraseSoundModel(keyphraseId, callingUserId, bcp47Locale);
+ if (soundModel == null
+ || soundModel.getUuid() == null
+ || soundModel.getKeyphrases() == null) {
+ Slog.w(TAG, "No matching sound model found in startRecognition");
+ return SoundTriggerInternal.STATUS_ERROR;
+ } else {
+ // Regardless of the status of the start recognition, we need to make sure
+ // that we unload this model if needed later.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ mLoadedKeyphraseIds.put(keyphraseId, this);
+ }
+ return mSession.startRecognition(
+ keyphraseId, soundModel, callback, recognitionConfig);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerInternal.stopRecognition(keyphraseId, callback);
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
- }
+ @Override
+ public int stopRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
+ }
- @Override
- public int setParameter(int keyphraseId, @ModelParams int modelParam, int value) {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSession.stopRecognition(keyphraseId, callback);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerInternal.setParameter(keyphraseId, modelParam, value);
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
- }
+ @Override
+ public int setParameter(int keyphraseId, @ModelParams int modelParam, int value) {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
+ }
- @Override
- public int getParameter(int keyphraseId, @ModelParams int modelParam) {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSession.setParameter(keyphraseId, modelParam, value);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerInternal.getParameter(keyphraseId, modelParam);
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
- }
+ @Override
+ public int getParameter(int keyphraseId, @ModelParams int modelParam) {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
+ }
- @Override
- @Nullable
- public ModelParamRange queryParameter(int keyphraseId, @ModelParams int modelParam) {
- // Allow the call if this is the current voice interaction service.
- synchronized (this) {
- enforceIsCurrentVoiceInteractionService();
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSession.getParameter(keyphraseId, modelParam);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerInternal.queryParameter(keyphraseId, modelParam);
- } finally {
- Binder.restoreCallingIdentity(caller);
+ @Override
+ @Nullable
+ public ModelParamRange queryParameter(int keyphraseId, @ModelParams int modelParam) {
+ // Allow the call if this is the current voice interaction service.
+ synchronized (VoiceInteractionManagerServiceStub.this) {
+ enforceIsCurrentVoiceInteractionService();
+ }
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSession.queryParameter(keyphraseId, modelParam);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
}
- }
- private synchronized void unloadAllKeyphraseModels() {
- for (int i = 0; i < mLoadedKeyphraseIds.size(); i++) {
+ private int unloadKeyphraseModel(int keyphraseId) {
final long caller = Binder.clearCallingIdentity();
try {
- int status = mSoundTriggerInternal.unloadKeyphraseModel(
- mLoadedKeyphraseIds.valueAt(i));
- if (status != SoundTriggerInternal.STATUS_OK) {
- Slog.w(TAG, "Failed to unload keyphrase " + mLoadedKeyphraseIds.valueAt(i)
- + ":" + status);
- }
+ return mSession.unloadKeyphraseModel(keyphraseId);
} finally {
Binder.restoreCallingIdentity(caller);
}
}
+ }
+
+ private synchronized void unloadAllKeyphraseModels() {
+ for (int i = 0; i < mLoadedKeyphraseIds.size(); i++) {
+ int id = mLoadedKeyphraseIds.keyAt(i);
+ SoundTriggerSession session = mLoadedKeyphraseIds.valueAt(i);
+ int status = session.unloadKeyphraseModel(id);
+ if (status != SoundTriggerInternal.STATUS_OK) {
+ Slog.w(TAG, "Failed to unload keyphrase " + id + ":" + status);
+ }
+ }
mLoadedKeyphraseIds.clear();
}
@@ -1420,7 +1464,24 @@ public class VoiceInteractionManagerService extends SystemService {
mImpl.dumpLocked(fd, pw, args);
}
}
+
mSoundTriggerInternal.dump(fd, pw, args);
+
+ // Dump all sessions.
+ synchronized (mSessions) {
+ ListIterator<WeakReference<SoundTriggerSession>> iter =
+ mSessions.listIterator();
+ while (iter.hasNext()) {
+ SoundTriggerSession session = iter.next().get();
+ if (session != null) {
+ session.dump(fd, args);
+ } else {
+ // Session is obsolete, now is a good chance to remove it from the list.
+ iter.remove();
+ }
+ }
+ }
+
}
@Override
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java
index 9324ba0b8b72..380e29984c63 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java
@@ -23,7 +23,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
@@ -31,6 +30,7 @@ import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaPlayer;
import android.media.soundtrigger.SoundTriggerDetector;
+import android.media.soundtrigger.SoundTriggerManager;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
@@ -232,8 +232,8 @@ public class SoundTriggerTestService extends Service {
public AudioTrack captureAudioTrack;
}
- private GenericSoundModel createNewSoundModel(ModelInfo modelInfo) {
- return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid,
+ private SoundTriggerManager.Model createNewSoundModel(ModelInfo modelInfo) {
+ return SoundTriggerManager.Model.create(modelInfo.modelUuid, modelInfo.vendorUuid,
modelInfo.modelData);
}
@@ -246,16 +246,16 @@ public class SoundTriggerTestService extends Service {
postMessage("Loading model: " + modelInfo.name);
- GenericSoundModel soundModel = createNewSoundModel(modelInfo);
+ SoundTriggerManager.Model soundModel = createNewSoundModel(modelInfo);
boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(soundModel);
if (status) {
postToast("Successfully loaded " + modelInfo.name + ", UUID="
- + soundModel.getUuid());
+ + soundModel.getModelUuid());
setModelState(modelInfo, "Loaded");
} else {
postErrorToast("Failed to load " + modelInfo.name + ", UUID="
- + soundModel.getUuid() + "!");
+ + soundModel.getModelUuid() + "!");
setModelState(modelInfo, "Failed to load");
}
}
@@ -269,7 +269,7 @@ public class SoundTriggerTestService extends Service {
postMessage("Unloading model: " + modelInfo.name);
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
+ SoundTriggerManager.Model soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
postErrorToast("Sound model not found for " + modelInfo.name + "!");
return;
@@ -278,11 +278,11 @@ public class SoundTriggerTestService extends Service {
boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
if (status) {
postToast("Successfully unloaded " + modelInfo.name + ", UUID="
- + soundModel.getUuid());
+ + soundModel.getModelUuid());
setModelState(modelInfo, "Unloaded");
} else {
postErrorToast("Failed to unload " +
- modelInfo.name + ", UUID=" + soundModel.getUuid() + "!");
+ modelInfo.name + ", UUID=" + soundModel.getModelUuid() + "!");
setModelState(modelInfo, "Failed to unload");
}
}
@@ -294,12 +294,12 @@ public class SoundTriggerTestService extends Service {
return;
}
postMessage("Reloading model: " + modelInfo.name);
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
+ SoundTriggerManager.Model soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
postErrorToast("Sound model not found for " + modelInfo.name + "!");
return;
}
- GenericSoundModel updated = createNewSoundModel(modelInfo);
+ SoundTriggerManager.Model updated = createNewSoundModel(modelInfo);
boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated);
if (status) {
postToast("Successfully reloaded " + modelInfo.name + ", UUID="
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
index 6b89b62bb1e3..cfe8c855ac81 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
@@ -18,7 +18,6 @@ package com.android.test.soundtrigger;
import android.annotation.Nullable;
import android.content.Context;
-import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.media.soundtrigger.SoundTriggerDetector;
import android.media.soundtrigger.SoundTriggerManager;
import android.os.RemoteException;
@@ -37,13 +36,10 @@ import java.util.UUID;
public class SoundTriggerUtil {
private static final String TAG = "SoundTriggerTestUtil";
- private final ISoundTriggerService mSoundTriggerService;
private final SoundTriggerManager mSoundTriggerManager;
private final Context mContext;
public SoundTriggerUtil(Context context) {
- mSoundTriggerService = ISoundTriggerService.Stub.asInterface(
- ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE));
mSoundTriggerManager = (SoundTriggerManager) context.getSystemService(
Context.SOUND_TRIGGER_SERVICE);
mContext = context;
@@ -55,15 +51,11 @@ public class SoundTriggerUtil {
*
* @param soundModel The sound model to add/update.
*/
- public boolean addOrUpdateSoundModel(GenericSoundModel soundModel) {
- try {
- if (soundModel == null) {
- throw new RuntimeException("Bad sound model");
- }
- mSoundTriggerService.updateSoundModel(soundModel);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in updateSoundModel", e);
+ public boolean addOrUpdateSoundModel(SoundTriggerManager.Model soundModel) {
+ if (soundModel == null) {
+ throw new RuntimeException("Bad sound model");
}
+ mSoundTriggerManager.updateModel(soundModel);
return true;
}
@@ -71,19 +63,15 @@ public class SoundTriggerUtil {
* Gets the sound model for the given keyphrase, null if none exists.
* If a sound model for a given keyphrase exists, and it needs to be updated,
* it should be obtained using this method, updated and then passed in to
- * {@link #addOrUpdateSoundModel(GenericSoundModel)} without changing the IDs.
+ * {@link #addOrUpdateSoundModel(SoundTriggerManager.Model)} without changing the IDs.
*
* @param modelId The model ID to look-up the sound model for.
* @return The sound model if one was found, null otherwise.
*/
@Nullable
- public GenericSoundModel getSoundModel(UUID modelId) {
- GenericSoundModel model = null;
- try {
- model = mSoundTriggerService.getSoundModel(new ParcelUuid(modelId));
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in updateKeyphraseSoundModel");
- }
+ public SoundTriggerManager.Model getSoundModel(UUID modelId) {
+ SoundTriggerManager.Model model = null;
+ model = mSoundTriggerManager.getModel(modelId);
if (model == null) {
Log.w(TAG, "No models present for the given keyphrase ID");
@@ -100,12 +88,7 @@ public class SoundTriggerUtil {
* @return {@code true} if the call succeeds, {@code false} otherwise.
*/
public boolean deleteSoundModel(UUID modelId) {
- try {
- mSoundTriggerService.deleteSoundModel(new ParcelUuid(modelId));
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in deleteSoundModel");
- return false;
- }
+ mSoundTriggerManager.deleteModel(modelId);
return true;
}