SoundTriggerHelper changes for GenericSoundModels.
- Refactoring SoundTriggerHelper to handle generic sound models.
- Ability to store multiple models, callback and state information.
- Separate out initialization to be done per voice model, per any model
and per generic model.
- Minor change to the API exposed -- removing the Handler from the
createSoundTriggerDetector call.
- Added callback processing for onRecognitionEvent().
- Added logic for stopAll().
- Changes to the SoundTriggerTestApp to start/stop recognition.
- Multiple models (3).
- Ability to start/stop/load/unload individual models.
Bug: 22860713
Bug: 27222043
Change-Id: Ie5d811babb956bead653fb560a43f1e549ed11bd
diff --git a/api/system-current.txt b/api/system-current.txt
index e4437aa..6509ac8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -24281,19 +24281,26 @@
package android.media.soundtrigger {
public final class SoundTriggerDetector {
- method public boolean startRecognition();
+ method public boolean startRecognition(int);
method public boolean stopRecognition();
+ field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
+ field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
}
- public abstract class SoundTriggerDetector.Callback {
+ public static abstract class SoundTriggerDetector.Callback {
ctor public SoundTriggerDetector.Callback();
method public abstract void onAvailabilityChanged(int);
- method public abstract void onDetected();
+ method public abstract void onDetected(android.media.soundtrigger.SoundTriggerDetector.EventPayload);
method public abstract void onError();
method public abstract void onRecognitionPaused();
method public abstract void onRecognitionResumed();
}
+ public static class SoundTriggerDetector.EventPayload {
+ method public android.media.AudioFormat getCaptureAudioFormat();
+ method public byte[] getTriggerAudio();
+ }
+
public final class SoundTriggerManager {
method public android.media.soundtrigger.SoundTriggerDetector createSoundTriggerDetector(java.util.UUID, android.media.soundtrigger.SoundTriggerDetector.Callback, android.os.Handler);
method public void deleteModel(java.util.UUID);
diff --git a/core/java/com/android/internal/app/ISoundTriggerService.aidl b/core/java/com/android/internal/app/ISoundTriggerService.aidl
index 9de4a6c..f4c18c3 100644
--- a/core/java/com/android/internal/app/ISoundTriggerService.aidl
+++ b/core/java/com/android/internal/app/ISoundTriggerService.aidl
@@ -33,10 +33,11 @@
void deleteSoundModel(in ParcelUuid soundModelId);
- void startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
+ int startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback,
+ in SoundTrigger.RecognitionConfig config);
/**
* Stops recognition.
*/
- void stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
+ int stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
}
diff --git a/media/java/android/media/soundtrigger/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
index 707db06..8f022db 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerDetector.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
@@ -16,12 +16,17 @@
package android.media.soundtrigger;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.AudioFormat;
import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Slog;
@@ -29,6 +34,8 @@
import com.android.internal.app.ISoundTriggerService;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
/**
@@ -45,6 +52,12 @@
private static final boolean DBG = false;
private static final String TAG = "SoundTriggerDetector";
+ private static final int MSG_AVAILABILITY_CHANGED = 1;
+ private static final int MSG_SOUND_TRIGGER_DETECTED = 2;
+ private static final int MSG_DETECTION_ERROR = 3;
+ private static final int MSG_DETECTION_PAUSE = 4;
+ private static final int MSG_DETECTION_RESUME = 5;
+
private final Object mLock = new Object();
private final ISoundTriggerService mSoundTriggerService;
@@ -53,7 +66,121 @@
private final Handler mHandler;
private final RecognitionCallback mRecognitionCallback;
- public abstract class Callback {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {
+ RECOGNITION_FLAG_NONE,
+ RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
+ RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
+ })
+ public @interface RecognitionFlags {}
+
+ /**
+ * Empty flag for {@link #startRecognition(int)}.
+ *
+ * @hide
+ */
+ public static final int RECOGNITION_FLAG_NONE = 0;
+
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the trigger audio for hotword needs to be captured.
+ */
+ public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
+
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the recognition should keep going on even after the
+ * model triggers.
+ * If this flag is specified, it's possible to get multiple
+ * triggers after a call to {@link #startRecognition(int)}, if the model
+ * triggers multiple times.
+ * When this isn't specified, the default behavior is to stop recognition once the
+ * trigger happenss, till the caller starts recognition again.
+ */
+ public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2;
+
+ /**
+ * Additional payload for {@link Callback#onDetected}.
+ */
+ public static class EventPayload {
+ private final boolean mTriggerAvailable;
+
+ // Indicates if {@code captureSession} can be used to continue capturing more audio
+ // from the DSP hardware.
+ private final boolean mCaptureAvailable;
+ // The session to use when attempting to capture more audio from the DSP hardware.
+ private final int mCaptureSession;
+ private final AudioFormat mAudioFormat;
+ // Raw data associated with the event.
+ // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
+ private final byte[] mData;
+
+ private EventPayload(boolean triggerAvailable, boolean captureAvailable,
+ AudioFormat audioFormat, int captureSession, byte[] data) {
+ mTriggerAvailable = triggerAvailable;
+ mCaptureAvailable = captureAvailable;
+ mCaptureSession = captureSession;
+ mAudioFormat = audioFormat;
+ mData = data;
+ }
+
+ /**
+ * Gets the format of the audio obtained using {@link #getTriggerAudio()}.
+ * May be null if there's no audio present.
+ */
+ @Nullable
+ public AudioFormat getCaptureAudioFormat() {
+ return mAudioFormat;
+ }
+
+ /**
+ * Gets the raw audio that triggered the keyphrase.
+ * This may be null if the trigger audio isn't available.
+ * If non-null, the format of the audio can be obtained by calling
+ * {@link #getCaptureAudioFormat()}.
+ *
+ * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
+ */
+ @Nullable
+ public byte[] getTriggerAudio() {
+ if (mTriggerAvailable) {
+ return mData;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the session ID to start a capture from the DSP.
+ * This may be null if streaming capture isn't possible.
+ * If non-null, the format of the audio that can be captured can be
+ * obtained using {@link #getCaptureAudioFormat()}.
+ *
+ * TODO: Candidate for Public API when the API to start capture with a session ID
+ * is made public.
+ *
+ * TODO: Add this to {@link #getCaptureAudioFormat()}:
+ * "Gets the format of the audio obtained using {@link #getTriggerAudio()}
+ * or {@link #getCaptureSession()}. May be null if no audio can be obtained
+ * for either the trigger or a streaming session."
+ *
+ * TODO: Should this return a known invalid value instead?
+ *
+ * @hide
+ */
+ @Nullable
+ public Integer getCaptureSession() {
+ if (mCaptureAvailable) {
+ return mCaptureSession;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public static abstract class Callback {
/**
* Called when the availability of the sound model changes.
*/
@@ -63,7 +190,7 @@
* Called when the sound model has triggered (such as when it matched a
* given sound pattern).
*/
- public abstract void onDetected();
+ public abstract void onDetected(@NonNull EventPayload eventPayload);
/**
* Called when the detection fails due to an error.
@@ -95,9 +222,9 @@
mSoundModelId = soundModelId;
mCallback = callback;
if (handler == null) {
- mHandler = new Handler();
+ mHandler = new MyHandler();
} else {
- mHandler = handler;
+ mHandler = new MyHandler(handler.getLooper());
}
mRecognitionCallback = new RecognitionCallback();
}
@@ -107,13 +234,19 @@
* {@link Callback}.
* @return Indicates whether the call succeeded or not.
*/
- public boolean startRecognition() {
+ public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
if (DBG) {
Slog.d(TAG, "startRecognition()");
}
+ boolean captureTriggerAudio =
+ (recognitionFlags & RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
+
+ boolean allowMultipleTriggers =
+ (recognitionFlags & RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
try {
mSoundTriggerService.startRecognition(new ParcelUuid(mSoundModelId),
- mRecognitionCallback);
+ mRecognitionCallback, new RecognitionConfig(captureTriggerAudio,
+ allowMultipleTriggers, null, null));
} catch (RemoteException e) {
return false;
}
@@ -144,17 +277,25 @@
/**
* Callback that handles events from the lower sound trigger layer.
+ *
+ * Note that these callbacks will be called synchronously from the SoundTriggerService
+ * layer and thus should do minimal work (such as sending a message on a handler to do
+ * the real work).
* @hide
*/
- private static class RecognitionCallback extends
- IRecognitionStatusCallback.Stub {
+ private class RecognitionCallback extends IRecognitionStatusCallback.Stub {
/**
* @hide
*/
@Override
public void onDetected(SoundTrigger.RecognitionEvent event) {
- Slog.e(TAG, "onDetected()" + event);
+ Slog.d(TAG, "onDetected()" + event);
+ Message.obtain(mHandler,
+ MSG_SOUND_TRIGGER_DETECTED,
+ new EventPayload(event.triggerInData, event.captureAvailable,
+ event.captureFormat, event.captureSession, event.data))
+ .sendToTarget();
}
/**
@@ -162,7 +303,8 @@
*/
@Override
public void onError(int status) {
- Slog.e(TAG, "onError()" + status);
+ Slog.d(TAG, "onError()" + status);
+ mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
}
/**
@@ -170,7 +312,8 @@
*/
@Override
public void onRecognitionPaused() {
- Slog.e(TAG, "onRecognitionPaused()");
+ Slog.d(TAG, "onRecognitionPaused()");
+ mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
}
/**
@@ -178,7 +321,44 @@
*/
@Override
public void onRecognitionResumed() {
- Slog.e(TAG, "onRecognitionResumed()");
+ Slog.d(TAG, "onRecognitionResumed()");
+ mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
+ }
+ }
+
+ private class MyHandler extends Handler {
+
+ MyHandler() {
+ super();
+ }
+
+ MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Received message: " + msg.what + " for NULL callback.");
+ return;
+ }
+ switch (msg.what) {
+ case MSG_SOUND_TRIGGER_DETECTED:
+ mCallback.onDetected((EventPayload) msg.obj);
+ break;
+ case MSG_DETECTION_ERROR:
+ mCallback.onError();
+ break;
+ case MSG_DETECTION_PAUSE:
+ mCallback.onRecognitionPaused();
+ break;
+ case MSG_DETECTION_RESUME:
+ mCallback.onRecognitionResumed();
+ break;
+ default:
+ super.handleMessage(msg);
+
+ }
}
}
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
index 18a5d59..f7cd6a3 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
@@ -54,6 +54,7 @@
private static final String CREATE_TABLE_ST_SOUND_MODEL = "CREATE TABLE "
+ GenericSoundModelContract.TABLE + "("
+ GenericSoundModelContract.KEY_MODEL_UUID + " TEXT PRIMARY KEY,"
+ + GenericSoundModelContract.KEY_VENDOR_UUID + " TEXT,"
+ GenericSoundModelContract.KEY_DATA + " BLOB" + " )";
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index 354075e..cde47bd 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -16,12 +16,16 @@
package com.android.server.soundtrigger;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.GenericRecognitionEvent;
+import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
@@ -29,6 +33,7 @@
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
import android.hardware.soundtrigger.SoundTrigger.SoundModelEvent;
import android.hardware.soundtrigger.SoundTriggerModule;
import android.os.PowerManager;
@@ -40,9 +45,16 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.UUID;
/**
- * Helper for {@link SoundTrigger} APIs.
+ * Helper for {@link SoundTrigger} APIs. Supports two types of models:
+ * (i) A voice model which is exported via the {@link VoiceInteractionService}. There can only be
+ * a single voice model running on the DSP at any given time.
+ *
+ * (ii) Generic sound-trigger models: Supports multiple of these.
+ *
* Currently this just acts as an abstraction over all SoundTrigger API calls.
*
* @hide
@@ -62,7 +74,7 @@
private static final int INVALID_VALUE = Integer.MIN_VALUE;
/** The {@link ModuleProperties} for the system, or null if none exists. */
- final ModuleProperties moduleProperties;
+ final ModuleProperties mModuleProperties;
/** The properties for the DSP module */
private SoundTriggerModule mModule;
@@ -72,21 +84,36 @@
private final PhoneStateListener mPhoneStateListener;
private final PowerManager mPowerManager;
- // TODO: Since many layers currently only deal with one recognition
+ // TODO: Since the voice layer currently only handles one recognition
// we simplify things by assuming one listener here too.
- private IRecognitionStatusCallback mActiveListener;
+ private IRecognitionStatusCallback mKeyphraseListener;
+
+ // The SoundTriggerManager layer handles multiple generic recognition models. We store the
+ // ModelData here in a hashmap.
+ private final HashMap<UUID, ModelData> mGenericModelDataMap;
+
+ // Note: KeyphraseId is not really used.
private int mKeyphraseId = INVALID_VALUE;
- private int mCurrentSoundModelHandle = INVALID_VALUE;
+
+ // Current voice sound model handle. We only allow one voice model to run at any given time.
+ private int mCurrentKeyphraseModelHandle = INVALID_VALUE;
private KeyphraseSoundModel mCurrentSoundModel = null;
// FIXME: Ideally this should not be stored if allowMultipleTriggers happens at a lower layer.
private RecognitionConfig mRecognitionConfig = null;
+
+ // Whether we are requesting recognition to start.
private boolean mRequested = false;
private boolean mCallActive = false;
private boolean mIsPowerSaveMode = false;
// Indicates if the native sound trigger service is disabled or not.
// This is an indirect indication of the microphone being open in some other application.
private boolean mServiceDisabled = false;
- private boolean mStarted = false;
+
+ // Whether we have ANY recognition (keyphrase or generic) running.
+ private boolean mRecognitionRunning = false;
+
+ // Keeps track of whether the keyphrase recognition is running.
+ private boolean mKeyphraseStarted = false;
private boolean mRecognitionAborted = false;
private PowerSaveModeListener mPowerSaveModeListener;
@@ -96,14 +123,87 @@
mContext = context;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mGenericModelDataMap = new HashMap<UUID, ModelData>();
mPhoneStateListener = new MyCallStateListener();
if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size());
- moduleProperties = null;
+ mModuleProperties = null;
mModule = null;
} else {
// TODO: Figure out how to determine which module corresponds to the DSP hardware.
- moduleProperties = modules.get(0);
+ mModuleProperties = modules.get(0);
+ }
+ }
+
+ /**
+ * Starts recognition for the given generic sound model ID.
+ *
+ * @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 startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
+ IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
+ if (soundModel == null || callback == null || recognitionConfig == null) {
+ Slog.w(TAG, "Passed in bad data to startGenericRecognition().");
+ return STATUS_ERROR;
+ }
+
+ synchronized (mLock) {
+
+ if (mModuleProperties == null) {
+ Slog.w(TAG, "Attempting startRecognition without the capability");
+ return STATUS_ERROR;
+ }
+
+ if (mModule == null) {
+ mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
+ if (mModule == null) {
+ Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
+ return STATUS_ERROR;
+ }
+ }
+
+ // Initialize power save, call active state monitoring logic.
+ if (!mRecognitionRunning) {
+ initializeTelephonyAndPowerStateListeners();
+ }
+
+ // Fetch a ModelData instance from the hash map. Creates a new one if none
+ // exists.
+ ModelData modelData = getOrCreateGenericModelData(modelId);
+
+ IRecognitionStatusCallback oldCallback = modelData.getCallback();
+ if (oldCallback != null) {
+ Slog.w(TAG, "Canceling previous recognition for model id: " + modelId);
+ try {
+ oldCallback.onError(STATUS_ERROR);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onDetectionStopped", e);
+ }
+ modelData.clearCallback();
+ }
+
+ // Load the model if its not loaded.
+ if (!modelData.isModelLoaded()) {
+ // Load the model
+ int[] handle = new int[] { INVALID_VALUE };
+ int status = mModule.loadSoundModel(soundModel, handle);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "loadSoundModel call failed with " + status);
+ return status;
+ }
+ if (handle[0] == INVALID_VALUE) {
+ Slog.w(TAG, "loadSoundModel call returned invalid sound model handle");
+ return STATUS_ERROR;
+ }
+ modelData.setHandle(handle[0]);
+ }
+ modelData.setCallback(callback);
+ modelData.setRecognitionConfig(recognitionConfig);
+
+ // Don't notify for synchronous calls.
+ return startGenericRecognitionLocked(modelData, false);
}
}
@@ -116,7 +216,7 @@
* @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,
+ int startKeyphraseRecognition(int keyphraseId,
KeyphraseSoundModel soundModel,
IRecognitionStatusCallback listener,
RecognitionConfig recognitionConfig) {
@@ -129,36 +229,24 @@
Slog.d(TAG, "startRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", listener=" + listener.asBinder()
+ ", recognitionConfig=" + recognitionConfig);
- Slog.d(TAG, "moduleProperties=" + moduleProperties);
+ Slog.d(TAG, "moduleProperties=" + mModuleProperties);
Slog.d(TAG, "current listener="
- + (mActiveListener == null ? "null" : mActiveListener.asBinder()));
- Slog.d(TAG, "current SoundModel handle=" + mCurrentSoundModelHandle);
+ + (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
+ Slog.d(TAG, "current SoundModel handle=" + mCurrentKeyphraseModelHandle);
Slog.d(TAG, "current SoundModel UUID="
+ (mCurrentSoundModel == null ? null : mCurrentSoundModel.uuid));
}
- if (!mStarted) {
- // Get the current call state synchronously for the first recognition.
- mCallActive = mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
- // Register for call state changes when the first call to start recognition occurs.
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
-
- // Register for power saver mode changes when the first call to start recognition
- // occurs.
- if (mPowerSaveModeListener == null) {
- mPowerSaveModeListener = new PowerSaveModeListener();
- mContext.registerReceiver(mPowerSaveModeListener,
- new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
- }
- mIsPowerSaveMode = mPowerManager.isPowerSaveMode();
+ if (!mRecognitionRunning) {
+ initializeTelephonyAndPowerStateListeners();
}
- if (moduleProperties == null) {
+ if (mModuleProperties == null) {
Slog.w(TAG, "Attempting startRecognition without the capability");
return STATUS_ERROR;
}
if (mModule == null) {
- mModule = SoundTrigger.attachModule(moduleProperties.id, this, null);
+ mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
if (mModule == null) {
Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
return STATUS_ERROR;
@@ -168,32 +256,32 @@
// Unload the previous model if the current one isn't invalid
// and, it's not the same as the new one.
// This helps use cache and reuse the model and just start/stop it when necessary.
- if (mCurrentSoundModelHandle != INVALID_VALUE
+ if (mCurrentKeyphraseModelHandle != INVALID_VALUE
&& !soundModel.equals(mCurrentSoundModel)) {
Slog.w(TAG, "Unloading previous sound model");
- int status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
+ int status = mModule.unloadSoundModel(mCurrentKeyphraseModelHandle);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadSoundModel call failed with " + status);
}
- internalClearSoundModelLocked();
- mStarted = false;
+ internalClearKeyphraseSoundModelLocked();
+ mKeyphraseStarted = false;
}
// If the previous recognition was by a different listener,
// Notify them that it was stopped.
- if (mActiveListener != null && mActiveListener.asBinder() != listener.asBinder()) {
+ if (mKeyphraseListener != null && mKeyphraseListener.asBinder() != listener.asBinder()) {
Slog.w(TAG, "Canceling previous recognition");
try {
- mActiveListener.onError(STATUS_ERROR);
+ mKeyphraseListener.onError(STATUS_ERROR);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetectionStopped", e);
}
- mActiveListener = null;
+ mKeyphraseListener = null;
}
// Load the sound model if the current one is null.
- int soundModelHandle = mCurrentSoundModelHandle;
- if (mCurrentSoundModelHandle == INVALID_VALUE
+ int soundModelHandle = mCurrentKeyphraseModelHandle;
+ if (mCurrentKeyphraseModelHandle == INVALID_VALUE
|| mCurrentSoundModel == null) {
int[] handle = new int[] { INVALID_VALUE };
int status = mModule.loadSoundModel(soundModel, handle);
@@ -213,18 +301,81 @@
// Start the recognition.
mRequested = true;
mKeyphraseId = keyphraseId;
- mCurrentSoundModelHandle = soundModelHandle;
+ mCurrentKeyphraseModelHandle = soundModelHandle;
mCurrentSoundModel = soundModel;
mRecognitionConfig = recognitionConfig;
// Register the new listener. This replaces the old one.
// There can only be a maximum of one active listener at any given time.
- mActiveListener = listener;
+ mKeyphraseListener = listener;
return updateRecognitionLocked(false /* don't notify for synchronous calls */);
}
}
/**
+ * Stops recognition for the given generic sound model.
+ *
+ * @param modelId The identifier of the generic sound model for which
+ * the recognition is to be stopped.
+ * @param listener The listener for the recognition events related to the given sound model.
+ *
+ * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ */
+ int stopGenericRecognition(UUID modelId, IRecognitionStatusCallback listener) {
+ if (listener == null) {
+ return STATUS_ERROR;
+ }
+
+ synchronized (mLock) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData == null) {
+ Slog.w(TAG, "Attempting stopRecognition on invalid model with id:" + modelId);
+ return STATUS_ERROR;
+ }
+
+ IRecognitionStatusCallback currentCallback = modelData.getCallback();
+ if (DBG) {
+ Slog.d(TAG, "stopRecognition for modelId=" + modelId
+ + ", listener=" + listener.asBinder());
+ Slog.d(TAG, "current callback ="
+ + (currentCallback == null ? "null" : currentCallback.asBinder()));
+ }
+
+ if (mModuleProperties == null || mModule == null) {
+ Slog.w(TAG, "Attempting stopRecognition without the capability");
+ return STATUS_ERROR;
+ }
+
+ if (currentCallback == null || !modelData.modelStarted()) {
+ // startRecognition hasn't been called or it failed.
+ Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
+ return STATUS_ERROR;
+ }
+ if (currentCallback.asBinder() != listener.asBinder()) {
+ // We don't allow a different listener to stop the recognition than the one
+ // that started it.
+ Slog.w(TAG, "Attempting stopRecognition for another recognition");
+ return STATUS_ERROR;
+ }
+
+ int status = stopGenericRecognitionLocked(modelData, false /* don't notify for synchronous calls */);
+ if (status != SoundTrigger.STATUS_OK) {
+ return status;
+ }
+
+ // We leave the sound model loaded but not started, this helps us when we start
+ // back.
+ // Also clear the internal state once the recognition has been stopped.
+ modelData.clearState();
+ modelData.clearCallback();
+ if (!computeRecognitionRunning()) {
+ internalClearGlobalStateLocked();
+ }
+ return status;
+ }
+ }
+
+ /**
* Stops recognition for the given {@link Keyphrase} if a recognition is
* currently active.
*
@@ -234,7 +385,7 @@
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
+ int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
if (listener == null) {
return STATUS_ERROR;
}
@@ -244,20 +395,20 @@
Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId
+ ", listener=" + listener.asBinder());
Slog.d(TAG, "current listener="
- + (mActiveListener == null ? "null" : mActiveListener.asBinder()));
+ + (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
}
- if (moduleProperties == null || mModule == null) {
+ if (mModuleProperties == null || mModule == null) {
Slog.w(TAG, "Attempting stopRecognition without the capability");
return STATUS_ERROR;
}
- if (mActiveListener == null) {
+ if (mKeyphraseListener == null) {
// startRecognition hasn't been called or it failed.
Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
return STATUS_ERROR;
}
- if (mActiveListener.asBinder() != listener.asBinder()) {
+ if (mKeyphraseListener.asBinder() != listener.asBinder()) {
// We don't allow a different listener to stop the recognition than the one
// that started it.
Slog.w(TAG, "Attempting stopRecognition for another recognition");
@@ -274,7 +425,8 @@
// We leave the sound model loaded but not started, this helps us when we start
// back.
// Also clear the internal state once the recognition has been stopped.
- internalClearStateLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGlobalStateLocked();
return status;
}
}
@@ -284,38 +436,56 @@
*/
void stopAllRecognitions() {
synchronized (mLock) {
- if (moduleProperties == null || mModule == null) {
+ if (mModuleProperties == null || mModule == null) {
return;
}
- if (mCurrentSoundModelHandle == INVALID_VALUE) {
- return;
+ // Stop Keyphrase recognition if one exists.
+ if (mCurrentKeyphraseModelHandle != INVALID_VALUE) {
+
+ mRequested = false;
+ int status = updateRecognitionLocked(
+ false /* don't notify for synchronous calls */);
+ internalClearKeyphraseStateLocked();
}
- mRequested = false;
- int status = updateRecognitionLocked(false /* don't notify for synchronous calls */);
- internalClearStateLocked();
+ // Stop all generic recognition models.
+ for (ModelData model : mGenericModelDataMap.values()) {
+ if (model.modelStarted()) {
+ int status = stopGenericRecognitionLocked(model,
+ false /* do not notify for synchronous calls */);
+ if (status != STATUS_OK) {
+ // What else can we do if there is an error here.
+ Slog.w(TAG, "Error stopping generic model: " + model.getHandle());
+ }
+ model.clearState();
+ model.clearCallback();
+ }
+ }
+ internalClearGlobalStateLocked();
}
}
public ModuleProperties getModuleProperties() {
- return moduleProperties;
+ return mModuleProperties;
}
//---- SoundTrigger.StatusListener methods
@Override
public void onRecognition(RecognitionEvent event) {
- if (event == null || !(event instanceof KeyphraseRecognitionEvent)) {
- Slog.w(TAG, "Invalid recognition event!");
+ if (event == null) {
+ Slog.w(TAG, "Null recognition event!");
+ return;
+ }
+
+ if (!(event instanceof KeyphraseRecognitionEvent) &&
+ !(event instanceof GenericRecognitionEvent)) {
+ Slog.w(TAG, "Invalid recognition event type (not one of generic or keyphrase) !");
return;
}
if (DBG) Slog.d(TAG, "onRecognition: " + event);
synchronized (mLock) {
- if (mActiveListener == null) {
- Slog.w(TAG, "received onRecognition event without any listener for it");
- return;
- }
switch (event.status) {
// Fire aborts/failures to all listeners since it's not tied to a keyphrase.
case SoundTrigger.RECOGNITION_STATUS_ABORT:
@@ -325,12 +495,60 @@
onRecognitionFailureLocked();
break;
case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
- onRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
+
+ if (isKeyphraseRecognitionEvent(event)) {
+ onKeyphraseRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
+ } else {
+ onGenericRecognitionSuccessLocked((GenericRecognitionEvent) event);
+ }
+
break;
}
}
}
+ private boolean isKeyphraseRecognitionEvent(RecognitionEvent event) {
+ return mCurrentKeyphraseModelHandle == event.soundModelHandle;
+ }
+
+ private void onGenericRecognitionSuccessLocked(GenericRecognitionEvent event) {
+ if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) {
+ return;
+ }
+ ModelData model = getModelDataFor(event.soundModelHandle);
+ if (model == null) {
+ Slog.w(TAG, "Generic recognition event: Model does not exist for handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ IRecognitionStatusCallback callback = model.getCallback();
+ if (callback == null) {
+ Slog.w(TAG, "Generic recognition event: Null callback for model handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ try {
+ callback.onDetected((GenericRecognitionEvent) event);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onDetected", e);
+ }
+
+ model.setStopped();
+ RecognitionConfig config = model.getRecognitionConfig();
+ if (config == null) {
+ Slog.w(TAG, "Generic recognition event: Null RecognitionConfig for model handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ // TODO: Remove this block if the lower layer supports multiple triggers.
+ if (config.allowMultipleTriggers) {
+ startGenericRecognitionLocked(model, true /* notify */);
+ }
+ }
+
@Override
public void onSoundModelUpdate(SoundModelEvent event) {
if (event == null) {
@@ -399,18 +617,25 @@
private void onRecognitionFailureLocked() {
Slog.w(TAG, "Recognition failure");
try {
- if (mActiveListener != null) {
- mActiveListener.onError(STATUS_ERROR);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onError(STATUS_ERROR);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
- internalClearStateLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGlobalStateLocked();
}
}
- private void onRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
+ private void onKeyphraseRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
Slog.i(TAG, "Recognition success");
+
+ if (mKeyphraseListener == null) {
+ Slog.w(TAG, "received onRecognition event without any listener for it");
+ return;
+ }
+
KeyphraseRecognitionExtra[] keyphraseExtras =
((KeyphraseRecognitionEvent) event).keyphraseExtras;
if (keyphraseExtras == null || keyphraseExtras.length == 0) {
@@ -424,14 +649,14 @@
}
try {
- if (mActiveListener != null) {
- mActiveListener.onDetected((KeyphraseRecognitionEvent) event);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onDetected((KeyphraseRecognitionEvent) event);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetected", e);
}
- mStarted = false;
+ mKeyphraseStarted = false;
mRequested = mRecognitionConfig.allowMultipleTriggers;
// TODO: Remove this block if the lower layer supports multiple triggers.
if (mRequested) {
@@ -441,14 +666,16 @@
private void onServiceDiedLocked() {
try {
- if (mActiveListener != null) {
- mActiveListener.onError(SoundTrigger.STATUS_DEAD_OBJECT);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onError(SoundTrigger.STATUS_DEAD_OBJECT);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
- internalClearSoundModelLocked();
- internalClearStateLocked();
+ internalClearKeyphraseSoundModelLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGenericModelStateLocked();
+ internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
mModule = null;
@@ -457,14 +684,14 @@
}
private int updateRecognitionLocked(boolean notify) {
- if (mModule == null || moduleProperties == null
- || mCurrentSoundModelHandle == INVALID_VALUE || mActiveListener == null) {
+ if (mModule == null || mModuleProperties == null
+ || mCurrentKeyphraseModelHandle == INVALID_VALUE || mKeyphraseListener == null) {
// Nothing to do here.
return STATUS_OK;
}
boolean start = mRequested && !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
- if (start == mStarted) {
+ if (start == mKeyphraseStarted) {
// No-op.
return STATUS_OK;
}
@@ -472,23 +699,24 @@
// See if the recognition needs to be started.
if (start) {
// Start recognition.
- int status = mModule.startRecognition(mCurrentSoundModelHandle, mRecognitionConfig);
+ int status = mModule.startRecognition(mCurrentKeyphraseModelHandle,
+ mRecognitionConfig);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "startRecognition failed with " + status);
// Notify of error if needed.
if (notify) {
try {
- mActiveListener.onError(status);
+ mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
- mStarted = true;
+ mKeyphraseStarted = true;
// Notify of resume if needed.
if (notify) {
try {
- mActiveListener.onRecognitionResumed();
+ mKeyphraseListener.onRecognitionResumed();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
}
@@ -499,7 +727,7 @@
// Stop recognition (only if we haven't been aborted).
int status = STATUS_OK;
if (!mRecognitionAborted) {
- status = mModule.stopRecognition(mCurrentSoundModelHandle);
+ status = mModule.stopRecognition(mCurrentKeyphraseModelHandle);
} else {
mRecognitionAborted = false;
}
@@ -507,17 +735,17 @@
Slog.w(TAG, "stopRecognition call failed with " + status);
if (notify) {
try {
- mActiveListener.onError(status);
+ mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
- mStarted = false;
+ mKeyphraseStarted = false;
// Notify of pause if needed.
if (notify) {
try {
- mActiveListener.onRecognitionPaused();
+ mKeyphraseListener.onRecognitionPaused();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
}
@@ -527,14 +755,11 @@
}
}
- private void internalClearStateLocked() {
- mStarted = false;
- mRequested = false;
-
- mKeyphraseId = INVALID_VALUE;
- mRecognitionConfig = null;
- mActiveListener = null;
-
+ // internalClearGlobalStateLocked() gets split into two routines. Cleanup that is
+ // specific to keyphrase sound models named as internalClearKeyphraseStateLocked() and
+ // internalClearGlobalStateLocked() for global state. The global cleanup routine will be used
+ // by the cleanup happening with the generic sound models.
+ private void internalClearGlobalStateLocked() {
// Unregister from call state changes.
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
@@ -545,8 +770,27 @@
}
}
- private void internalClearSoundModelLocked() {
- mCurrentSoundModelHandle = INVALID_VALUE;
+ private void internalClearKeyphraseStateLocked() {
+ mKeyphraseStarted = false;
+ mRequested = false;
+
+ mKeyphraseId = INVALID_VALUE;
+ mRecognitionConfig = null;
+ mKeyphraseListener = null;
+ }
+
+ private void internalClearGenericModelStateLocked() {
+ for (UUID modelId : mGenericModelDataMap.keySet()) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ modelData.clearState();
+ modelData.clearCallback();
+ }
+ }
+
+ // This routine is a replacement for internalClearSoundModelLocked(). However, we
+ // should see why this should be different from internalClearKeyphraseStateLocked().
+ private void internalClearKeyphraseSoundModelLocked() {
+ mCurrentKeyphraseModelHandle = INVALID_VALUE;
mCurrentSoundModel = null;
}
@@ -577,19 +821,251 @@
void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mLock) {
pw.print(" module properties=");
- pw.println(moduleProperties == null ? "null" : moduleProperties);
+ pw.println(mModuleProperties == null ? "null" : mModuleProperties);
pw.print(" keyphrase ID="); pw.println(mKeyphraseId);
- pw.print(" sound model handle="); pw.println(mCurrentSoundModelHandle);
+ pw.print(" sound model handle="); pw.println(mCurrentKeyphraseModelHandle);
pw.print(" sound model UUID=");
pw.println(mCurrentSoundModel == null ? "null" : mCurrentSoundModel.uuid);
pw.print(" current listener=");
- pw.println(mActiveListener == null ? "null" : mActiveListener.asBinder());
+ pw.println(mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder());
pw.print(" requested="); pw.println(mRequested);
- pw.print(" started="); pw.println(mStarted);
+ pw.print(" started="); pw.println(mKeyphraseStarted);
pw.print(" call active="); pw.println(mCallActive);
pw.print(" power save mode active="); pw.println(mIsPowerSaveMode);
pw.print(" service disabled="); pw.println(mServiceDisabled);
}
}
+
+ private void initializeTelephonyAndPowerStateListeners() {
+ // Get the current call state synchronously for the first recognition.
+ mCallActive = mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
+
+ // Register for call state changes when the first call to start recognition occurs.
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ // Register for power saver mode changes when the first call to start recognition
+ // occurs.
+ if (mPowerSaveModeListener == null) {
+ mPowerSaveModeListener = new PowerSaveModeListener();
+ mContext.registerReceiver(mPowerSaveModeListener,
+ new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
+ }
+ mIsPowerSaveMode = mPowerManager.isPowerSaveMode();
+ }
+
+ private ModelData getOrCreateGenericModelData(UUID modelId) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData == null) {
+ modelData = new ModelData(modelId);
+ modelData.setTypeGeneric();
+ mGenericModelDataMap.put(modelId, modelData);
+ }
+ return modelData;
+ }
+
+ // Instead of maintaining a second hashmap of modelHandle -> ModelData, we just
+ // iterate through to find the right object (since we don't expect 100s of models
+ // to be stored).
+ private ModelData getModelDataFor(int modelHandle) {
+ // Fetch ModelData object corresponding to the model handle.
+ for (ModelData model : mGenericModelDataMap.values()) {
+ if (model.getHandle() == modelHandle) {
+ return model;
+ }
+ }
+ return null;
+ }
+
+ // Whether we are allowed to run any recognition at all. The conditions that let us run
+ // a recognition include: no active phone call or not being in a power save mode. Also,
+ // the native service should be enabled.
+ private boolean isRecognitionAllowed() {
+ return !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
+ }
+
+ private int startGenericRecognitionLocked(ModelData modelData, boolean notify) {
+ IRecognitionStatusCallback callback = modelData.getCallback();
+ int handle = modelData.getHandle();
+ RecognitionConfig config = modelData.getRecognitionConfig();
+ if (callback == null || handle == INVALID_VALUE || config == null) {
+ // Nothing to do here.
+ Slog.w(TAG, "startGenericRecognition: Bad data passed in.");
+ return STATUS_ERROR;
+ }
+
+ if (!isRecognitionAllowed()) {
+ // Nothing to do here.
+ Slog.w(TAG, "startGenericRecognition requested but not allowed.");
+ return STATUS_OK;
+ }
+
+ int status = mModule.startRecognition(handle, config);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "startRecognition failed with " + status);
+ // Notify of error if needed.
+ if (notify) {
+ try {
+ callback.onError(status);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onError", e);
+ }
+ }
+ } else {
+ modelData.setStarted();
+ // Notify of resume if needed.
+ if (notify) {
+ try {
+ callback.onRecognitionResumed();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
+ }
+ }
+ }
+ return status;
+ }
+
+ private int stopGenericRecognitionLocked(ModelData modelData, boolean notify) {
+ IRecognitionStatusCallback callback = modelData.getCallback();
+
+ // Stop recognition (only if we haven't been aborted).
+ int status = mModule.stopRecognition(modelData.getHandle());
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "stopRecognition call failed with " + status);
+ if (notify) {
+ try {
+ callback.onError(status);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onError", e);
+ }
+ }
+ } else {
+ modelData.setStopped();
+ // Notify of pause if needed.
+ if (notify) {
+ try {
+ callback.onRecognitionPaused();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
+ }
+ }
+ }
+ return status;
+ }
+
+ // Computes whether we have any recognition running at all (voice or generic). Sets
+ // the mRecognitionRunning variable with the result.
+ private boolean computeRecognitionRunning() {
+ synchronized (mLock) {
+ if (mModuleProperties == null || mModule == null) {
+ mRecognitionRunning = false;
+ return mRecognitionRunning;
+ }
+ if (mKeyphraseListener != null &&
+ mKeyphraseStarted &&
+ mCurrentKeyphraseModelHandle != INVALID_VALUE &&
+ mCurrentSoundModel != null) {
+ mRecognitionRunning = true;
+ return mRecognitionRunning;
+ }
+ for (UUID modelId : mGenericModelDataMap.keySet()) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData.modelStarted()) {
+ mRecognitionRunning = true;
+ return mRecognitionRunning;
+ }
+ }
+ mRecognitionRunning = false;
+ }
+ return mRecognitionRunning;
+ }
+
+ // This class encapsulates the callbacks, state, handles and any other information that
+ // represents a model.
+ private static class ModelData {
+ // Model not loaded (and hence not started).
+ static final int MODEL_NOTLOADED = 0;
+
+ // Loaded implies model was successfully loaded. Model not started yet.
+ static final int MODEL_LOADED = 1;
+
+ // Started implies model was successfully loaded and start was called.
+ static final int MODEL_STARTED = 2;
+
+ // One of MODEL_NOTLOADED, MODEL_LOADED, MODEL_STARTED (which implies loaded).
+ private int mModelState;
+
+ private UUID mModelId;
+
+ // One of SoundModel.TYPE_GENERIC or SoundModel.TYPE_KEYPHRASE. Initially set
+ // to SoundModel.TYPE_UNKNOWN;
+ private int mModelType = SoundModel.TYPE_UNKNOWN;
+ private IRecognitionStatusCallback mCallback = null;
+ private SoundModel mSoundModel = null;
+ private RecognitionConfig mRecognitionConfig = null;
+
+
+ // Model handle is an integer used by the HAL as an identifier for sound
+ // models.
+ private int mModelHandle = INVALID_VALUE;
+
+ ModelData(UUID modelId) {
+ mModelId = modelId;
+ }
+
+ synchronized void setTypeGeneric() {
+ mModelType = SoundModel.TYPE_GENERIC_SOUND;
+ }
+
+ synchronized void setCallback(IRecognitionStatusCallback callback) {
+ mCallback = callback;
+ }
+
+ synchronized IRecognitionStatusCallback getCallback() {
+ return mCallback;
+ }
+
+ synchronized boolean isModelLoaded() {
+ return (mModelState == MODEL_LOADED || mModelState == MODEL_STARTED) &&
+ mSoundModel != null;
+ }
+
+ synchronized void setStarted() {
+ mModelState = MODEL_STARTED;
+ }
+
+ synchronized void setStopped() {
+ mModelState = MODEL_LOADED;
+ }
+
+ synchronized boolean modelStarted() {
+ return mModelState == MODEL_STARTED;
+ }
+
+ synchronized void clearState() {
+ mModelState = MODEL_NOTLOADED;
+ mSoundModel = null;
+ mModelHandle = INVALID_VALUE;
+ }
+
+ synchronized void clearCallback() {
+ mCallback = null;
+ }
+
+ synchronized void setHandle(int handle) {
+ mModelHandle = handle;
+ }
+
+ synchronized void setRecognitionConfig(RecognitionConfig config) {
+ mRecognitionConfig = config;
+ }
+
+ synchronized int getHandle() {
+ return mModelHandle;
+ }
+
+ synchronized RecognitionConfig getRecognitionConfig() {
+ return mRecognitionConfig;
+ }
+ }
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 682f4a4..251f314 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -15,6 +15,7 @@
*/
package com.android.server.soundtrigger;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -47,13 +48,14 @@
* @hide
*/
public class SoundTriggerService extends SystemService {
- static final String TAG = "SoundTriggerService";
- static final boolean DEBUG = false;
+ private static final String TAG = "SoundTriggerService";
+ private static final boolean DEBUG = true;
final Context mContext;
private final SoundTriggerServiceStub mServiceStub;
private final LocalSoundTriggerService mLocalSoundTriggerService;
private SoundTriggerDbHelper mDbHelper;
+ private SoundTriggerHelper mSoundTriggerHelper;
public SoundTriggerService(Context context) {
super(context);
@@ -71,7 +73,8 @@
@Override
public void onBootPhase(int phase) {
if (PHASE_SYSTEM_SERVICES_READY == phase) {
- mLocalSoundTriggerService.initSoundTriggerHelper();
+ initSoundTriggerHelper();
+ mLocalSoundTriggerService.setSoundTriggerHelper(mSoundTriggerHelper);
} else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
mDbHelper = new SoundTriggerDbHelper(mContext);
}
@@ -85,6 +88,20 @@
public void onSwitchUser(int userHandle) {
}
+ private synchronized void initSoundTriggerHelper() {
+ if (mSoundTriggerHelper == null) {
+ mSoundTriggerHelper = new SoundTriggerHelper(mContext);
+ }
+ }
+
+ private synchronized boolean isInitialized() {
+ if (mSoundTriggerHelper == null ) {
+ Slog.e(TAG, "SoundTriggerHelper not initialized.");
+ return false;
+ }
+ return true;
+ }
+
class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
@@ -102,19 +119,32 @@
}
@Override
- public void startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
+ public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
+ RecognitionConfig config) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
if (DEBUG) {
Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
}
+ if (!isInitialized()) return STATUS_ERROR;
+
+ GenericSoundModel model = getSoundModel(parcelUuid);
+ if (model == null) {
+ Slog.e(TAG, "Null model in database for id: " + parcelUuid);
+ return STATUS_ERROR;
+ }
+
+ return mSoundTriggerHelper.startGenericRecognition(parcelUuid.getUuid(), model,
+ callback, config);
}
@Override
- public void stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
+ public int stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
if (DEBUG) {
Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
}
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
}
@Override
@@ -123,10 +153,8 @@
if (DEBUG) {
Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
}
- SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(soundModelId.getUuid());
- if (model == null) {
- Slog.e(TAG, "Null model in database.");
- }
+ SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
+ soundModelId.getUuid());
return model;
}
@@ -157,38 +185,49 @@
mContext = context;
}
- void initSoundTriggerHelper() {
- if (mSoundTriggerHelper == null) {
- mSoundTriggerHelper = new SoundTriggerHelper(mContext);
- }
+ synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
+ mSoundTriggerHelper = helper;
}
@Override
public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
- return mSoundTriggerHelper.startRecognition(keyphraseId, soundModel, listener,
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel, listener,
recognitionConfig);
}
@Override
- public int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
- return mSoundTriggerHelper.stopRecognition(keyphraseId, listener);
+ public synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
}
@Override
public void stopAllRecognitions() {
+ if (!isInitialized()) return;
mSoundTriggerHelper.stopAllRecognitions();
}
@Override
public ModuleProperties getModuleProperties() {
+ if (!isInitialized()) return null;
return mSoundTriggerHelper.getModuleProperties();
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!isInitialized()) return;
mSoundTriggerHelper.dump(fd, pw, args);
}
+
+ private synchronized boolean isInitialized() {
+ if (mSoundTriggerHelper == null ) {
+ Slog.e(TAG, "SoundTriggerHelper not initialized.");
+ return false;
+ }
+ return true;
+ }
}
private void enforceCallingPermission(String permission) {
diff --git a/tests/SoundTriggerTestApp/Android.mk b/tests/SoundTriggerTestApp/Android.mk
index 7bcab5e..c327b09 100644
--- a/tests/SoundTriggerTestApp/Android.mk
+++ b/tests/SoundTriggerTestApp/Android.mk
@@ -8,5 +8,6 @@
LOCAL_MODULE_TAGS := optional
LOCAL_PRIVILEGED_MODULE := true
+LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
diff --git a/tests/SoundTriggerTestApp/AndroidManifest.xml b/tests/SoundTriggerTestApp/AndroidManifest.xml
index 40619da..a72b3dd 100644
--- a/tests/SoundTriggerTestApp/AndroidManifest.xml
+++ b/tests/SoundTriggerTestApp/AndroidManifest.xml
@@ -2,16 +2,22 @@
package="com.android.test.soundtrigger">
<uses-permission android:name="android.permission.MANAGE_SOUND_TRIGGER" />
- <application
- android:permission="android.permission.MANAGE_SOUND_TRIGGER">
+ <application>
<activity
android:name="TestSoundTriggerActivity"
android:label="SoundTrigger Test Application"
- android:theme="@android:style/Theme.Material.Light.Voice">
+ android:theme="@android:style/Theme.Material">
+ <!--
<intent-filter>
<action android:name="com.android.intent.action.MANAGE_SOUND_TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
+ -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
</activity>
</application>
</manifest>
diff --git a/tests/SoundTriggerTestApp/res/layout/main.xml b/tests/SoundTriggerTestApp/res/layout/main.xml
index 9d2b9d9..5ecc770 100644
--- a/tests/SoundTriggerTestApp/res/layout/main.xml
+++ b/tests/SoundTriggerTestApp/res/layout/main.xml
@@ -18,6 +18,11 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:orientation="vertical"
+ >
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
>
<Button
@@ -37,7 +42,57 @@
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:text="@string/start_recog"
+ android:onClick="onStartRecognitionButtonClicked"
+ android:padding="20dp" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/stop_recog"
+ android:onClick="onStopRecognitionButtonClicked"
+ android:padding="20dp" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:text="@string/unenroll"
android:onClick="onUnEnrollButtonClicked"
android:padding="20dp" />
-</LinearLayout>
\ No newline at end of file
+
+</LinearLayout>
+
+<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="20dp"
+ android:orientation="vertical">
+ <RadioButton android:id="@+id/model_one"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_one"
+ android:onClick="onRadioButtonClicked"/>
+ <RadioButton android:id="@+id/model_two"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_two"
+ android:onClick="onRadioButtonClicked"/>
+ <RadioButton android:id="@+id/model_three"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_three"
+ android:onClick="onRadioButtonClicked"/>
+</RadioGroup>
+
+ <TextView
+ android:id="@+id/console"
+ android:gravity="left"
+ android:paddingTop="20pt"
+ android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:maxLines="40"
+ android:textSize="14dp"
+ android:scrollbars = "vertical"
+ android:text="@string/none">
+ </TextView>
+</LinearLayout>
diff --git a/tests/SoundTriggerTestApp/res/values/strings.xml b/tests/SoundTriggerTestApp/res/values/strings.xml
index 07bac2a..5f0fb1d 100644
--- a/tests/SoundTriggerTestApp/res/values/strings.xml
+++ b/tests/SoundTriggerTestApp/res/values/strings.xml
@@ -16,7 +16,13 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="enroll">Enroll</string>
- <string name="reenroll">Re-enroll</string>
- <string name="unenroll">Un-enroll</string>
-</resources>
\ No newline at end of file
+ <string name="enroll">Load</string>
+ <string name="reenroll">Re-load</string>
+ <string name="unenroll">Un-load</string>
+ <string name="start_recog">Start</string>
+ <string name="stop_recog">Stop</string>
+ <string name="model_one">Model One</string>
+ <string name="model_two">Model Two</string>
+ <string name="model_three">Model Three</string>
+ <string name="none">Debug messages appear here:</string>
+</resources>
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
index 4702835..1c95c25 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.media.soundtrigger.SoundTriggerDetector;
import android.media.soundtrigger.SoundTriggerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -28,6 +29,7 @@
import com.android.internal.app.ISoundTriggerService;
+import java.lang.RuntimeException;
import java.util.UUID;
/**
@@ -56,6 +58,9 @@
*/
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);
@@ -112,4 +117,10 @@
public void deleteSoundModelUsingManager(UUID modelId) {
mSoundTriggerManager.deleteModel(modelId);
}
+
+ public SoundTriggerDetector createSoundTriggerDetector(UUID modelId,
+ SoundTriggerDetector.Callback callback) {
+ return mSoundTriggerManager.createSoundTriggerDetector(modelId, callback, null);
+ }
+
}
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
index 966179b..96a6966 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
@@ -22,11 +22,17 @@
import android.app.Activity;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.media.AudioFormat;
+import android.media.soundtrigger.SoundTriggerDetector;
import android.media.soundtrigger.SoundTriggerManager;
+import android.text.Editable;
+import android.text.method.ScrollingMovementMethod;
import android.os.Bundle;
import android.os.UserManager;
import android.util.Log;
import android.view.View;
+import android.widget.RadioButton;
+import android.widget.TextView;
import android.widget.Toast;
public class TestSoundTriggerActivity extends Activity {
@@ -35,42 +41,75 @@
private SoundTriggerUtil mSoundTriggerUtil;
private Random mRandom;
- private UUID mModelUuid = UUID.randomUUID();
+ private UUID mModelUuid1 = UUID.randomUUID();
private UUID mModelUuid2 = UUID.randomUUID();
+ private UUID mModelUuid3 = UUID.randomUUID();
private UUID mVendorUuid = UUID.randomUUID();
+ private SoundTriggerDetector mDetector1 = null;
+ private SoundTriggerDetector mDetector2 = null;
+ private SoundTriggerDetector mDetector3 = null;
+
+ private TextView mDebugView = null;
+ private int mSelectedModelId = 1;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DBG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
+ mDebugView = (TextView) findViewById(R.id.console);
+ mDebugView.setText(mDebugView.getText(), TextView.BufferType.EDITABLE);
+ mDebugView.setMovementMethod(new ScrollingMovementMethod());
mSoundTriggerUtil = new SoundTriggerUtil(this);
mRandom = new Random();
}
+ private void postMessage(String msg) {
+ Log.i(TAG, "Posted: " + msg);
+ ((Editable) mDebugView.getText()).append(msg + "\n");
+ }
+
+ private UUID getSelectedUuid() {
+ if (mSelectedModelId == 2) return mModelUuid2;
+ if (mSelectedModelId == 3) return mModelUuid3;
+ return mModelUuid1; // Default.
+ }
+
+ private void setDetector(SoundTriggerDetector detector) {
+ if (mSelectedModelId == 2) mDetector2 = detector;
+ if (mSelectedModelId == 3) mDetector3 = detector;
+ mDetector1 = detector;
+ }
+
+ private SoundTriggerDetector getDetector() {
+ if (mSelectedModelId == 2) return mDetector2;
+ if (mSelectedModelId == 3) return mDetector3;
+ return mDetector1;
+ }
+
/**
* Called when the user clicks the enroll button.
* Performs a fresh enrollment.
*/
public void onEnrollButtonClicked(View v) {
+ postMessage("Loading model: " + mSelectedModelId);
// Generate a fake model to push.
byte[] data = new byte[1024];
mRandom.nextBytes(data);
- GenericSoundModel model = new GenericSoundModel(mModelUuid, mVendorUuid, data);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel model = new GenericSoundModel(modelUuid, mVendorUuid, data);
boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(model);
if (status) {
Toast.makeText(
- this, "Successfully created sound trigger model UUID=" + mModelUuid, Toast.LENGTH_SHORT)
- .show();
+ this, "Successfully created sound trigger model UUID=" + modelUuid,
+ Toast.LENGTH_SHORT).show();
} else {
- Toast.makeText(this, "Failed to enroll!!!" + mModelUuid, Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, "Failed to enroll!!!" + modelUuid, Toast.LENGTH_SHORT).show();
}
// Test the SoundManager API.
- SoundTriggerManager.Model tmpModel = SoundTriggerManager.Model.create(mModelUuid2,
- mVendorUuid, data);
- mSoundTriggerUtil.addOrUpdateSoundModel(tmpModel);
}
/**
@@ -78,12 +117,14 @@
* Clears the enrollment information for the user.
*/
public void onUnEnrollButtonClicked(View v) {
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid);
+ postMessage("Unloading model: " + mSelectedModelId);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
return;
}
- boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid);
+ boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid1);
if (status) {
Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid,
Toast.LENGTH_SHORT)
@@ -91,7 +132,6 @@
} else {
Toast.makeText(this, "Failed to delete sound model!!!", Toast.LENGTH_SHORT).show();
}
- mSoundTriggerUtil.deleteSoundModelUsingManager(mModelUuid2);
}
/**
@@ -99,7 +139,9 @@
* Uses the previously enrolled sound model and makes changes to it before pushing it back.
*/
public void onReEnrollButtonClicked(View v) {
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid);
+ postMessage("Re-loading model: " + mSelectedModelId);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
return;
@@ -118,4 +160,86 @@
Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show();
}
}
+
+ public void onStartRecognitionButtonClicked(View v) {
+ UUID modelUuid = getSelectedUuid();
+ SoundTriggerDetector detector = getDetector();
+ if (detector == null) {
+ Log.i(TAG, "Created an instance of the SoundTriggerDetector.");
+ detector = mSoundTriggerUtil.createSoundTriggerDetector(modelUuid,
+ new DetectorCallback());
+ setDetector(detector);
+ }
+ postMessage("Triggering start recognition for model: " + mSelectedModelId);
+ if (!detector.startRecognition(
+ SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
+ Log.e(TAG, "Fast failure attempting to start recognition.");
+ }
+ }
+
+ public void onStopRecognitionButtonClicked(View v) {
+ SoundTriggerDetector detector = getDetector();
+ if (detector == null) {
+ Log.e(TAG, "Stop called on null detector.");
+ return;
+ }
+ postMessage("Triggering stop recognition for model: " + mSelectedModelId);
+ if (!detector.stopRecognition()) {
+ Log.e(TAG, "Fast failure attempting to stop recognition.");
+ }
+ }
+
+ public void onRadioButtonClicked(View view) {
+ // Is the button now checked?
+ boolean checked = ((RadioButton) view).isChecked();
+ // Check which radio button was clicked
+ switch(view.getId()) {
+ case R.id.model_one:
+ if (checked) mSelectedModelId = 1;
+ postMessage("Selected model one.");
+ break;
+ case R.id.model_two:
+ if (checked) mSelectedModelId = 2;
+ postMessage("Selected model two.");
+ break;
+ case R.id.model_three:
+ if (checked) mSelectedModelId = 3;
+ postMessage("Selected model three.");
+ break;
+ }
+ }
+
+ // Implementation of SoundTriggerDetector.Callback.
+ public class DetectorCallback extends SoundTriggerDetector.Callback {
+ public void onAvailabilityChanged(int status) {
+ postMessage("Availability changed to: " + status);
+ }
+
+ public void onDetected(SoundTriggerDetector.EventPayload event) {
+ postMessage("onDetected(): " + eventPayloadToString(event));
+ }
+
+ public void onError() {
+ postMessage("onError()");
+ }
+
+ public void onRecognitionPaused() {
+ postMessage("onRecognitionPaused()");
+ }
+
+ public void onRecognitionResumed() {
+ postMessage("onRecognitionResumed()");
+ }
+ }
+
+ private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
+ String result = "EventPayload(";
+ AudioFormat format = event.getCaptureAudioFormat();
+ result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
+ byte[] triggerAudio = event.getTriggerAudio();
+ result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length);
+ result = result + "CaptureSession: " + event.getCaptureSession();
+ result += " )";
+ return result;
+ }
}