*/
package android.media.soundtrigger;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
+import android.app.PendingIntent;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.content.Context;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
+import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
+import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.os.Handler;
import android.os.ParcelUuid;
import android.os.RemoteException;
return mGenericSoundModel;
}
}
+
+
+ /**
+ * Default message type.
+ * @hide
+ */
+ public static final int FLAG_MESSAGE_TYPE_UNKNOWN = -1;
+ /**
+ * Contents of EXTRA_MESSAGE_TYPE extra for a RecognitionEvent.
+ * @hide
+ */
+ public static final int FLAG_MESSAGE_TYPE_RECOGNITION_EVENT = 0;
+ /**
+ * Contents of EXTRA_MESSAGE_TYPE extra for recognition error events.
+ * @hide
+ */
+ public static final int FLAG_MESSAGE_TYPE_RECOGNITION_ERROR = 1;
+ /**
+ * Contents of EXTRA_MESSAGE_TYPE extra for a recognition paused events.
+ * @hide
+ */
+ public static final int FLAG_MESSAGE_TYPE_RECOGNITION_PAUSED = 2;
+ /**
+ * Contents of EXTRA_MESSAGE_TYPE extra for recognition resumed events.
+ * @hide
+ */
+ public static final int FLAG_MESSAGE_TYPE_RECOGNITION_RESUMED = 3;
+
+ /**
+ * Extra key in the intent for the type of the message.
+ * @hide
+ */
+ public static final String EXTRA_MESSAGE_TYPE = "android.media.soundtrigger.MESSAGE_TYPE";
+ /**
+ * Extra key in the intent that holds the RecognitionEvent parcelable.
+ * @hide
+ */
+ public static final String EXTRA_RECOGNITION_EVENT = "android.media.soundtrigger.RECOGNITION_EVENT";
+ /**
+ * Extra key in the intent that holds the status in an error message.
+ * @hide
+ */
+ public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
+
+ /**
+ * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
+ * an error/the system service is restarted.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public int loadSoundModel(SoundModel soundModel) {
+ if (soundModel == null) {
+ return STATUS_ERROR;
+ }
+
+ try {
+ switch (soundModel.type) {
+ case SoundModel.TYPE_GENERIC_SOUND:
+ return mSoundTriggerService.loadGenericSoundModel(
+ (GenericSoundModel) soundModel);
+ case SoundModel.TYPE_KEYPHRASE:
+ return mSoundTriggerService.loadKeyphraseSoundModel(
+ (KeyphraseSoundModel) soundModel);
+ default:
+ Slog.e(TAG, "Unkown model type");
+ return STATUS_ERROR;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts recognition on the given model id. All events from the model will be sent to the
+ * PendingIntent.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public int startRecognition(UUID soundModelId, PendingIntent callbackIntent,
+ RecognitionConfig config) {
+ if (soundModelId == null || callbackIntent == null || config == null) {
+ return STATUS_ERROR;
+ }
+ try {
+ return mSoundTriggerService.startRecognitionForIntent(new ParcelUuid(soundModelId),
+ callbackIntent, config);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stops the given model's recognition.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public int stopRecognition(UUID soundModelId) {
+ if (soundModelId == null) {
+ return STATUS_ERROR;
+ }
+ try {
+ return mSoundTriggerService.stopRecognitionForIntent(new ParcelUuid(soundModelId));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes the given model from memory. Will also stop any pending recognitions.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public int unloadSoundModel(UUID soundModelId) {
+ if (soundModelId == null) {
+ return STATUS_ERROR;
+ }
+ try {
+ return mSoundTriggerService.unloadSoundModel(
+ new ParcelUuid(soundModelId));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns true if the given model has had detection started on it.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public boolean isRecognitionActive(UUID soundModelId) {
+ if (soundModelId == null) {
+ return false;
+ }
+ try {
+ return mSoundTriggerService.isRecognitionActive(
+ new ParcelUuid(soundModelId));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
package com.android.server.soundtrigger;
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_OK;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.Manifest;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.soundtrigger.SoundTriggerManager;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelUuid;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.util.Slog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.TreeMap;
import java.util.UUID;
/**
private static final boolean DEBUG = true;
final Context mContext;
+ private Object mLock;
private final SoundTriggerServiceStub mServiceStub;
private final LocalSoundTriggerService mLocalSoundTriggerService;
private SoundTriggerDbHelper mDbHelper;
private SoundTriggerHelper mSoundTriggerHelper;
+ private final TreeMap<UUID, SoundModel> mLoadedModels;
+ private final TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback> mIntentCallbacks;
+ private PowerManager.WakeLock mWakelock;
public SoundTriggerService(Context context) {
super(context);
mContext = context;
mServiceStub = new SoundTriggerServiceStub();
mLocalSoundTriggerService = new LocalSoundTriggerService(context);
+ mLoadedModels = new TreeMap<UUID, SoundModel>();
+ mIntentCallbacks = new TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback>();
+ mLock = new Object();
}
@Override
mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
}
+
+ @Override
+ public int loadGenericSoundModel(GenericSoundModel soundModel) {
+ enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return STATUS_ERROR;
+ if (soundModel == null || soundModel.uuid == null) {
+ Slog.e(TAG, "Invalid sound model");
+ return STATUS_ERROR;
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "loadGenericSoundModel(): id = " + soundModel.uuid);
+ }
+ synchronized (mLock) {
+ SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
+ // If the model we're loading is actually different than what we had loaded, we
+ // should unload that other model now. We don't care about return codes since we
+ // don't know if the other model is loaded.
+ if (oldModel != null && !oldModel.equals(soundModel)) {
+ mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
+ mIntentCallbacks.remove(soundModel.uuid);
+ }
+ mLoadedModels.put(soundModel.uuid, soundModel);
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public int loadKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
+ enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return STATUS_ERROR;
+ if (soundModel == null || soundModel.uuid == null) {
+ Slog.e(TAG, "Invalid sound model");
+ return STATUS_ERROR;
+ }
+ if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
+ Slog.e(TAG, "Only one keyphrase per model is currently supported.");
+ return STATUS_ERROR;
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "loadKeyphraseSoundModel(): id = " + soundModel.uuid);
+ }
+ synchronized (mLock) {
+ SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
+ // If the model we're loading is actually different than what we had loaded, we
+ // should unload that other model now. We don't care about return codes since we
+ // don't know if the other model is loaded.
+ if (oldModel != null && !oldModel.equals(soundModel)) {
+ mSoundTriggerHelper.unloadKeyphraseSoundModel(soundModel.keyphrases[0].id);
+ mIntentCallbacks.remove(soundModel.uuid);
+ }
+ mLoadedModels.put(soundModel.uuid, soundModel);
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public int startRecognitionForIntent(ParcelUuid soundModelId, PendingIntent callbackIntent,
+ SoundTrigger.RecognitionConfig config) {
+ enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return STATUS_ERROR;
+ if (DEBUG) {
+ Slog.i(TAG, "startRecognition(): id = " + soundModelId);
+ }
+
+ synchronized (mLock) {
+ SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
+ if (soundModel == null) {
+ Slog.e(TAG, soundModelId + " is not loaded");
+ return STATUS_ERROR;
+ }
+ LocalSoundTriggerRecognitionStatusCallback callback = mIntentCallbacks.get(
+ soundModelId.getUuid());
+ if (callback != null) {
+ Slog.e(TAG, soundModelId + " is already running");
+ return STATUS_ERROR;
+ }
+ callback = new LocalSoundTriggerRecognitionStatusCallback(soundModelId.getUuid(),
+ callbackIntent, config);
+ int ret;
+ switch (soundModel.type) {
+ case SoundModel.TYPE_KEYPHRASE: {
+ KeyphraseSoundModel keyphraseSoundModel = (KeyphraseSoundModel) soundModel;
+ ret = mSoundTriggerHelper.startKeyphraseRecognition(
+ keyphraseSoundModel.keyphrases[0].id, keyphraseSoundModel, callback,
+ config);
+ } break;
+ case SoundModel.TYPE_GENERIC_SOUND:
+ ret = mSoundTriggerHelper.startGenericRecognition(soundModel.uuid,
+ (GenericSoundModel) soundModel, callback, config);
+ break;
+ default:
+ Slog.e(TAG, "Unknown model type");
+ return STATUS_ERROR;
+ }
+
+ if (ret != STATUS_OK) {
+ Slog.e(TAG, "Failed to start model: " + ret);
+ return ret;
+ }
+ mIntentCallbacks.put(soundModelId.getUuid(), callback);
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public int stopRecognitionForIntent(ParcelUuid soundModelId) {
+ enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return STATUS_ERROR;
+ if (DEBUG) {
+ Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
+ }
+
+ synchronized (mLock) {
+ SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
+ if (soundModel == null) {
+ Slog.e(TAG, soundModelId + " is not loaded");
+ return STATUS_ERROR;
+ }
+ LocalSoundTriggerRecognitionStatusCallback callback = mIntentCallbacks.get(
+ soundModelId.getUuid());
+ if (callback == null) {
+ Slog.e(TAG, soundModelId + " is not running");
+ return STATUS_ERROR;
+ }
+ int ret;
+ switch (soundModel.type) {
+ case SoundModel.TYPE_KEYPHRASE:
+ ret = mSoundTriggerHelper.stopKeyphraseRecognition(
+ ((KeyphraseSoundModel)soundModel).keyphrases[0].id, callback);
+ break;
+ case SoundModel.TYPE_GENERIC_SOUND:
+ ret = mSoundTriggerHelper.stopGenericRecognition(soundModel.uuid, callback);
+ break;
+ default:
+ Slog.e(TAG, "Unknown model type");
+ return STATUS_ERROR;
+ }
+
+ if (ret != STATUS_OK) {
+ Slog.e(TAG, "Failed to stop model: " + ret);
+ return ret;
+ }
+ mIntentCallbacks.remove(soundModelId.getUuid());
+ }
+ return STATUS_OK;
+ }
+
+ @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);
+ }
+
+ synchronized (mLock) {
+ SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
+ if (soundModel == null) {
+ Slog.e(TAG, soundModelId + " is not loaded");
+ return STATUS_ERROR;
+ }
+ int ret;
+ switch (soundModel.type) {
+ case SoundModel.TYPE_KEYPHRASE:
+ ret = mSoundTriggerHelper.unloadKeyphraseSoundModel(
+ ((KeyphraseSoundModel)soundModel).keyphrases[0].id);
+ break;
+ case SoundModel.TYPE_GENERIC_SOUND:
+ ret = mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
+ break;
+ default:
+ Slog.e(TAG, "Unknown model type");
+ return STATUS_ERROR;
+ }
+ if (ret != STATUS_OK) {
+ Slog.e(TAG, "Failed to unload model");
+ return ret;
+ }
+ mLoadedModels.remove(soundModelId.getUuid());
+ return STATUS_OK;
+ }
+ }
+
+ @Override
+ public boolean isRecognitionActive(ParcelUuid parcelUuid) {
+ enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return false;
+ synchronized (mLock) {
+ LocalSoundTriggerRecognitionStatusCallback callback =
+ mIntentCallbacks.get(parcelUuid.getUuid());
+ if (callback == null) {
+ return false;
+ }
+ return mSoundTriggerHelper.isRecognitionRequested(parcelUuid.getUuid());
+ }
+ }
}
+ private final class LocalSoundTriggerRecognitionStatusCallback
+ extends IRecognitionStatusCallback.Stub {
+ private UUID mUuid;
+ private PendingIntent mCallbackIntent;
+ private RecognitionConfig mRecognitionConfig;
+
+ public LocalSoundTriggerRecognitionStatusCallback(UUID modelUuid,
+ PendingIntent callbackIntent,
+ RecognitionConfig config) {
+ mUuid = modelUuid;
+ mCallbackIntent = callbackIntent;
+ mRecognitionConfig = config;
+ }
+
+ @Override
+ public boolean pingBinder() {
+ return mCallbackIntent != null;
+ }
+
+ @Override
+ public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
+ if (mCallbackIntent == null) {
+ return;
+ }
+ grabWakeLock();
+
+ Slog.w(TAG, "Keyphrase sound trigger event: " + event);
+ Intent extras = new Intent();
+ extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
+ SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_EVENT);
+ extras.putExtra(SoundTriggerManager.EXTRA_RECOGNITION_EVENT, event);
+ try {
+ mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
+ if (!mRecognitionConfig.allowMultipleTriggers) {
+ removeCallback(/*releaseWakeLock=*/false);
+ }
+ } catch (PendingIntent.CanceledException e) {
+ removeCallback(/*releaseWakeLock=*/true);
+ }
+ }
+
+ @Override
+ public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
+ if (mCallbackIntent == null) {
+ return;
+ }
+ grabWakeLock();
+
+ Slog.w(TAG, "Generic sound trigger event: " + event);
+ Intent extras = new Intent();
+ extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
+ SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_EVENT);
+ extras.putExtra(SoundTriggerManager.EXTRA_RECOGNITION_EVENT, event);
+ try {
+ mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
+ if (!mRecognitionConfig.allowMultipleTriggers) {
+ removeCallback(/*releaseWakeLock=*/false);
+ }
+ } catch (PendingIntent.CanceledException e) {
+ removeCallback(/*releaseWakeLock=*/true);
+ }
+ }
+
+ @Override
+ public void onError(int status) {
+ if (mCallbackIntent == null) {
+ return;
+ }
+ grabWakeLock();
+
+ Slog.i(TAG, "onError: " + status);
+ Intent extras = new Intent();
+ extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
+ SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_ERROR);
+ extras.putExtra(SoundTriggerManager.EXTRA_STATUS, status);
+ try {
+ mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
+ // Remove the callback, but wait for the intent to finish before we let go of the
+ // wake lock
+ removeCallback(/*releaseWakeLock=*/false);
+ } catch (PendingIntent.CanceledException e) {
+ removeCallback(/*releaseWakeLock=*/true);
+ }
+ }
+
+ @Override
+ public void onRecognitionPaused() {
+ if (mCallbackIntent == null) {
+ return;
+ }
+ grabWakeLock();
+
+ Slog.i(TAG, "onRecognitionPaused");
+ Intent extras = new Intent();
+ extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
+ SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_PAUSED);
+ try {
+ mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
+ } catch (PendingIntent.CanceledException e) {
+ removeCallback(/*releaseWakeLock=*/true);
+ }
+ }
+
+ @Override
+ public void onRecognitionResumed() {
+ if (mCallbackIntent == null) {
+ return;
+ }
+ grabWakeLock();
+
+ Slog.i(TAG, "onRecognitionResumed");
+ Intent extras = new Intent();
+ extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
+ SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_RESUMED);
+ try {
+ mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
+ } catch (PendingIntent.CanceledException e) {
+ removeCallback(/*releaseWakeLock=*/true);
+ }
+ }
+
+ private void removeCallback(boolean releaseWakeLock) {
+ mCallbackIntent = null;
+ synchronized (mLock) {
+ mIntentCallbacks.remove(mUuid);
+ if (releaseWakeLock) {
+ mWakelock.release();
+ }
+ }
+ }
+ }
+
+ private void grabWakeLock() {
+ synchronized (mLock) {
+ if (mWakelock == null) {
+ PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
+ mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ }
+ mWakelock.acquire();
+ }
+ }
+
+ private PendingIntent.OnFinished mCallbackCompletedHandler = new PendingIntent.OnFinished() {
+ @Override
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ // We're only ever invoked when the callback is done, so release the lock.
+ synchronized (mLock) {
+ mWakelock.release();
+ }
+ }
+ };
+
public final class LocalSoundTriggerService extends SoundTriggerInternal {
private final Context mContext;
private SoundTriggerHelper mSoundTriggerHelper;