blob: 3779d8720a9d99c5a1de7f5257ace16ac593584a [file] [log] [blame]
/**
* 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.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;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
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;
import android.os.RemoteException;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Slog;
import com.android.internal.logging.MetricsLogger;
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. 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
*/
public class SoundTriggerHelper implements SoundTrigger.StatusListener {
static final String TAG = "SoundTriggerHelper";
static final boolean DBG = false;
/**
* Return codes for {@link #startRecognition(int, KeyphraseSoundModel,
* IRecognitionStatusCallback, RecognitionConfig)},
* {@link #stopRecognition(int, IRecognitionStatusCallback)}
*/
public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
public static final int STATUS_OK = SoundTrigger.STATUS_OK;
private static final int INVALID_VALUE = Integer.MIN_VALUE;
/** The {@link ModuleProperties} for the system, or null if none exists. */
final ModuleProperties mModuleProperties;
/** The properties for the DSP module */
private SoundTriggerModule mModule;
private final Object mLock = new Object();
private final Context mContext;
private final TelephonyManager mTelephonyManager;
private final PhoneStateListener mPhoneStateListener;
private final PowerManager mPowerManager;
// The SoundTriggerManager layer handles multiple recognition models of type generic and
// keyphrase. We store the ModelData here in a hashmap.
private final HashMap<UUID, ModelData> mModelDataMap;
// An index of keyphrase sound models so that we can reach them easily. We support indexing
// keyphrase sound models with a keyphrase ID. Sound model with the same keyphrase ID will
// replace an existing model, thus there is a 1:1 mapping from keyphrase ID to a voice
// sound model.
private HashMap<Integer, UUID> mKeyphraseUuidMap;
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;
// Whether we have ANY recognition (keyphrase or generic) running.
private boolean mRecognitionRunning = false;
private PowerSaveModeListener mPowerSaveModeListener;
SoundTriggerHelper(Context context) {
ArrayList <ModuleProperties> modules = new ArrayList<>();
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;
mModule = null;
} else {
// TODO: Figure out how to determine which module corresponds to the DSP hardware.
mModuleProperties = modules.get(0);
}
}
/**
* Starts recognition for the given generic sound model ID. This is a wrapper around {@link
* startRecognition()}.
*
* @param modelId UUID of the sound model.
* @param soundModel The generic sound model to use for recognition.
* @param callback Callack for the recognition events related to the given keyphrase.
* @param recognitionConfig Instance of RecognitionConfig containing the parameters for the
* recognition.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
MetricsLogger.count(mContext, "sth_start_recognition", 1);
if (modelId == null || soundModel == null || callback == null ||
recognitionConfig == null) {
Slog.w(TAG, "Passed in bad data to startGenericRecognition().");
return STATUS_ERROR;
}
synchronized (mLock) {
ModelData modelData = getOrCreateGenericModelDataLocked(modelId);
if (modelData == null) {
Slog.w(TAG, "Irrecoverable error occurred, check UUID / sound model data.");
return STATUS_ERROR;
}
return startRecognition(soundModel, modelData, callback, recognitionConfig,
INVALID_VALUE /* keyphraseId */);
}
}
/**
* 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 callback The callback for the recognition events related to the given keyphrase.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int startKeyphraseRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_start_recognition", 1);
if (soundModel == null || callback == null || recognitionConfig == null) {
return STATUS_ERROR;
}
if (DBG) {
Slog.d(TAG, "startKeyphraseRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", callback=" + callback.asBinder()
+ ", recognitionConfig=" + recognitionConfig);
Slog.d(TAG, "moduleProperties=" + mModuleProperties);
dumpModelStateLocked();
}
ModelData model = getKeyphraseModelDataLocked(keyphraseId);
if (model != null && !model.isKeyphraseModel()) {
Slog.e(TAG, "Generic model with same UUID exists.");
return STATUS_ERROR;
}
// Process existing model first.
if (model != null && !model.getModelId().equals(soundModel.uuid)) {
// The existing model has a different UUID, should be replaced.
int status = cleanUpExistingKeyphraseModel(model);
if (status != STATUS_OK) {
return status;
}
removeKeyphraseModelLocked(keyphraseId);
model = null;
}
// We need to create a new one: either no previous models existed for given keyphrase id
// or the existing model had a different UUID and was cleaned up.
if (model == null) {
model = createKeyphraseModelDataLocked(soundModel.uuid, keyphraseId);
}
return startRecognition(soundModel, model, callback, recognitionConfig,
keyphraseId);
}
}
private int cleanUpExistingKeyphraseModel(ModelData modelData) {
// Stop and clean up a previous ModelData if one exists. This usually is used when the
// previous model has a different UUID for the same keyphrase ID.
int status = tryStopAndUnloadLocked(modelData, true /* stop */, true /* unload */);
if (status != STATUS_OK) {
Slog.w(TAG, "Unable to stop or unload previous model: " +
modelData.toString());
}
return status;
}
/**
* Starts recognition for the given sound model. A single routine for both keyphrase and
* generic sound models.
*
* @param soundModel The sound model to use for recognition.
* @param modelData Instance of {@link #ModelData} for the given model.
* @param callback Callback for the recognition events related to the given keyphrase.
* @param recognitionConfig Instance of {@link RecognitionConfig} containing the parameters
* @param keyphraseId Keyphrase ID for keyphrase models only. Pass in INVALID_VALUE for other
* models.
* for the recognition.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int startRecognition(SoundModel soundModel, ModelData modelData,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
int keyphraseId) {
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();
}
// If the existing SoundModel is different (for the same UUID for Generic and same
// keyphrase ID for voice), ensure that it is unloaded and stopped before proceeding.
// This works for both keyphrase and generic models. This logic also ensures that a
// previously loaded (or started) model is appropriately stopped. Since this is a
// generalization of the previous logic with a single keyphrase model, we should have
// no regression with the previous version of this code as was given in the
// startKeyphrase() routine.
if (modelData.getSoundModel() != null) {
boolean stopModel = false; // Stop the model after checking that it is started.
boolean unloadModel = false;
if (modelData.getSoundModel().equals(soundModel) && modelData.isModelStarted()) {
// The model has not changed, but the previous model is "started".
// Stop the previously running model.
stopModel = true;
unloadModel = false; // No need to unload if the model hasn't changed.
} else if (!modelData.getSoundModel().equals(soundModel)) {
// We have a different model for this UUID. Stop and unload if needed. This
// helps maintain the singleton restriction for keyphrase sound models.
stopModel = modelData.isModelStarted();
unloadModel = modelData.isModelLoaded();
}
if (stopModel || unloadModel) {
int status = tryStopAndUnloadLocked(modelData, stopModel, unloadModel);
if (status != STATUS_OK) {
Slog.w(TAG, "Unable to stop or unload previous model: " +
modelData.toString());
return status;
}
}
}
IRecognitionStatusCallback oldCallback = modelData.getCallback();
if (oldCallback != null && oldCallback.asBinder() != callback.asBinder()) {
Slog.w(TAG, "Canceling previous recognition for model id: " +
modelData.getModelId());
try {
oldCallback.onError(STATUS_ERROR);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetectionStopped", e);
}
modelData.clearCallback();
}
// Load the model if it is 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.setLoaded();
Slog.d(TAG, "Sound model loaded with handle:" + handle[0]);
}
modelData.setCallback(callback);
modelData.setRequested(true);
modelData.setRecognitionConfig(recognitionConfig);
modelData.setSoundModel(soundModel);
return startRecognitionLocked(modelData,
false /* Don't notify for synchronous calls */);
}
}
/**
* Stops recognition for the given generic sound model. This is a wrapper for {@link
* #stopRecognition}.
*
* @param modelId The identifier of the generic sound model for which
* the recognition is to be stopped.
* @param callback The callback 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 callback) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_stop_recognition", 1);
if (callback == null || modelId == null) {
Slog.e(TAG, "Null callbackreceived for stopGenericRecognition() for modelid:" +
modelId);
return STATUS_ERROR;
}
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null || !modelData.isGenericModel()) {
Slog.w(TAG, "Attempting stopRecognition on invalid model with id:" + modelId);
return STATUS_ERROR;
}
int status = stopRecognition(modelData, callback);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopGenericRecognition failed: " + status);
}
return status;
}
}
/**
* Stops recognition for the given {@link Keyphrase} if a recognition is
* currently active. This is a wrapper for {@link #stopRecognition()}.
*
* @param keyphraseId The identifier of the keyphrase for which
* the recognition is to be stopped.
* @param callback The callback for the recognition events related to the given keyphrase.
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_stop_recognition", 1);
if (callback == null) {
Slog.e(TAG, "Null callback received for stopKeyphraseRecognition() for keyphraseId:" +
keyphraseId);
return STATUS_ERROR;
}
ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
if (modelData == null || !modelData.isKeyphraseModel()) {
Slog.e(TAG, "No model exists for given keyphrase Id.");
return STATUS_ERROR;
}
if (DBG) {
Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId + ", callback =" +
callback.asBinder());
Slog.d(TAG, "current callback=" + (modelData == null ? "null" :
modelData.getCallback().asBinder()));
}
int status = stopRecognition(modelData, callback);
if (status != SoundTrigger.STATUS_OK) {
return status;
}
return status;
}
}
/**
* Stops recognition for the given ModelData instance.
*
* @param modelData Instance of {@link #ModelData} sound model.
* @param callback The callback for the recognition events related to the given keyphrase.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
private int stopRecognition(ModelData modelData, IRecognitionStatusCallback callback) {
synchronized (mLock) {
if (callback == null) {
return STATUS_ERROR;
}
if (mModuleProperties == null || mModule == null) {
Slog.w(TAG, "Attempting stopRecognition without the capability");
return STATUS_ERROR;
}
IRecognitionStatusCallback currentCallback = modelData.getCallback();
if (modelData == null || currentCallback == null ||
(!modelData.isRequested() && !modelData.isModelStarted())) {
// startGenericRecognition hasn't been called or it failed.
Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
return STATUS_ERROR;
}
if (currentCallback.asBinder() != callback.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;
}
// Request stop recognition via the update() method.
modelData.setRequested(false);
int status = updateRecognitionLocked(modelData, isRecognitionAllowed(),
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.setLoaded();
modelData.clearCallback();
modelData.setRecognitionConfig(null);
if (!computeRecognitionRunningLocked()) {
internalClearGlobalStateLocked();
}
return status;
}
}
// Stop a previously started model if it was started. Optionally, unload if the previous model
// is stale and is about to be replaced.
// Needs to be called with the mLock held.
private int tryStopAndUnloadLocked(ModelData modelData, boolean stopModel,
boolean unloadModel) {
int status = STATUS_OK;
if (modelData.isModelNotLoaded()) {
return status;
}
if (stopModel && modelData.isModelStarted()) {
status = stopRecognitionLocked(modelData,
false /* don't notify for synchronous calls */);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopRecognition failed: " + status);
return status;
}
}
if (unloadModel && modelData.isModelLoaded()) {
Slog.d(TAG, "Unloading previously loaded stale model.");
status = mModule.unloadSoundModel(modelData.getHandle());
MetricsLogger.count(mContext, "sth_unloading_stale_model", 1);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadSoundModel call failed with " + status);
} else {
// Clear the ModelData state if successful.
modelData.clearState();
}
}
return status;
}
public ModuleProperties getModuleProperties() {
return mModuleProperties;
}
int unloadKeyphraseSoundModel(int keyphraseId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_unload_keyphrase_sound_model", 1);
ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
if (mModule == null || modelData == null || modelData.getHandle() == INVALID_VALUE ||
!modelData.isKeyphraseModel()) {
return STATUS_ERROR;
}
// Stop recognition if it's the current one.
modelData.setRequested(false);
int status = updateRecognitionLocked(modelData, isRecognitionAllowed(),
false /* don't notify */);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "Stop recognition failed for keyphrase ID:" + status);
}
status = mModule.unloadSoundModel(modelData.getHandle());
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadKeyphraseSoundModel call failed with " + status);
}
// Remove it from existence.
removeKeyphraseModelLocked(keyphraseId);
return status;
}
}
int unloadGenericSoundModel(UUID modelId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_unload_generic_sound_model", 1);
if (modelId == null || mModule == null) {
return STATUS_ERROR;
}
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null || !modelData.isGenericModel()) {
Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" +
modelId);
return STATUS_ERROR;
}
if (!modelData.isModelLoaded()) {
// Nothing to do here.
Slog.i(TAG, "Unload: Given generic model is not loaded:" + modelId);
return STATUS_OK;
}
if (modelData.isModelStarted()) {
int status = stopRecognitionLocked(modelData,
false /* don't notify for synchronous calls */);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopGenericRecognition failed: " + status);
}
}
int status = mModule.unloadSoundModel(modelData.getHandle());
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadGenericSoundModel() call failed with " + status);
Slog.w(TAG, "unloadGenericSoundModel() force-marking model as unloaded.");
}
// Remove it from existence.
mModelDataMap.remove(modelId);
if (DBG) dumpModelStateLocked();
return status;
}
}
//---- SoundTrigger.StatusListener methods
@Override
public void onRecognition(RecognitionEvent 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) {
switch (event.status) {
case SoundTrigger.RECOGNITION_STATUS_ABORT:
onRecognitionAbortLocked(event);
break;
case SoundTrigger.RECOGNITION_STATUS_FAILURE:
// Fire failures to all listeners since it's not tied to a keyphrase.
onRecognitionFailureLocked();
break;
case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
if (isKeyphraseRecognitionEvent(event)) {
onKeyphraseRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
} else {
onGenericRecognitionSuccessLocked((GenericRecognitionEvent) event);
}
break;
}
}
}
private boolean isKeyphraseRecognitionEvent(RecognitionEvent event) {
return event instanceof KeyphraseRecognitionEvent;
}
private void onGenericRecognitionSuccessLocked(GenericRecognitionEvent event) {
MetricsLogger.count(mContext, "sth_generic_recognition_event", 1);
if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) {
return;
}
ModelData model = getModelDataForLocked(event.soundModelHandle);
if (model == null || !model.isGenericModel()) {
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.onGenericSoundTriggerDetected((GenericRecognitionEvent) event);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onGenericSoundTriggerDetected", e);
}
model.setStopped();
RecognitionConfig config = model.getRecognitionConfig();
if (config == null) {
Slog.w(TAG, "Generic recognition event: Null RecognitionConfig for model handle: " +
event.soundModelHandle);
return;
}
model.setRequested(config.allowMultipleTriggers);
// TODO: Remove this block if the lower layer supports multiple triggers.
if (model.isRequested()) {
updateRecognitionLocked(model, isRecognitionAllowed() /* isAllowed */,
true /* notify */);
}
}
@Override
public void onSoundModelUpdate(SoundModelEvent event) {
if (event == null) {
Slog.w(TAG, "Invalid sound model event!");
return;
}
if (DBG) Slog.d(TAG, "onSoundModelUpdate: " + event);
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_sound_model_updated", 1);
onSoundModelUpdatedLocked(event);
}
}
@Override
public void onServiceStateChange(int state) {
if (DBG) Slog.d(TAG, "onServiceStateChange, state: " + state);
synchronized (mLock) {
onServiceStateChangedLocked(SoundTrigger.SERVICE_STATE_DISABLED == state);
}
}
@Override
public void onServiceDied() {
Slog.e(TAG, "onServiceDied!!");
MetricsLogger.count(mContext, "sth_service_died", 1);
synchronized (mLock) {
onServiceDiedLocked();
}
}
private void onCallStateChangedLocked(boolean callActive) {
if (mCallActive == callActive) {
// We consider multiple call states as being active
// so we check if something really changed or not here.
return;
}
mCallActive = callActive;
updateAllRecognitionsLocked(true /* notify */);
}
private void onPowerSaveModeChangedLocked(boolean isPowerSaveMode) {
if (mIsPowerSaveMode == isPowerSaveMode) {
return;
}
mIsPowerSaveMode = isPowerSaveMode;
updateAllRecognitionsLocked(true /* notify */);
}
private void onSoundModelUpdatedLocked(SoundModelEvent event) {
// TODO: Handle sound model update here.
}
private void onServiceStateChangedLocked(boolean disabled) {
if (disabled == mServiceDisabled) {
return;
}
mServiceDisabled = disabled;
updateAllRecognitionsLocked(true /* notify */);
}
private void onRecognitionAbortLocked(RecognitionEvent event) {
Slog.w(TAG, "Recognition aborted");
MetricsLogger.count(mContext, "sth_recognition_aborted", 1);
ModelData modelData = getModelDataForLocked(event.soundModelHandle);
if (modelData != null) {
modelData.setStopped();
}
}
private void onRecognitionFailureLocked() {
Slog.w(TAG, "Recognition failure");
MetricsLogger.count(mContext, "sth_recognition_failure_event", 1);
try {
sendErrorCallbacksToAll(STATUS_ERROR);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
internalClearModelStateLocked();
internalClearGlobalStateLocked();
}
}
private int getKeyphraseIdFromEvent(KeyphraseRecognitionEvent event) {
if (event == null) {
Slog.w(TAG, "Null RecognitionEvent received.");
return INVALID_VALUE;
}
KeyphraseRecognitionExtra[] keyphraseExtras =
((KeyphraseRecognitionEvent) event).keyphraseExtras;
if (keyphraseExtras == null || keyphraseExtras.length == 0) {
Slog.w(TAG, "Invalid keyphrase recognition event!");
return INVALID_VALUE;
}
// TODO: Handle more than one keyphrase extras.
return keyphraseExtras[0].id;
}
private void onKeyphraseRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
Slog.i(TAG, "Recognition success");
MetricsLogger.count(mContext, "sth_keyphrase_recognition_event", 1);
int keyphraseId = getKeyphraseIdFromEvent(event);
ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
if (modelData == null || !modelData.isKeyphraseModel()) {
Slog.e(TAG, "Keyphase model data does not exist for ID:" + keyphraseId);
return;
}
if (modelData.getCallback() == null) {
Slog.w(TAG, "Received onRecognition event without callback for keyphrase model.");
return;
}
try {
modelData.getCallback().onKeyphraseDetected((KeyphraseRecognitionEvent) event);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onKeyphraseDetected", e);
}
modelData.setStopped();
RecognitionConfig config = modelData.getRecognitionConfig();
if (config != null) {
// Whether we should continue by starting this again.
modelData.setRequested(config.allowMultipleTriggers);
}
// TODO: Remove this block if the lower layer supports multiple triggers.
if (modelData.isRequested()) {
updateRecognitionLocked(modelData, isRecognitionAllowed(), true /* notify */);
}
}
private void updateAllRecognitionsLocked(boolean notify) {
boolean isAllowed = isRecognitionAllowed();
for (ModelData modelData : mModelDataMap.values()) {
updateRecognitionLocked(modelData, isAllowed, notify);
}
}
private int updateRecognitionLocked(ModelData model, boolean isAllowed,
boolean notify) {
boolean start = model.isRequested() && isAllowed;
if (start == model.isModelStarted()) {
// No-op.
return STATUS_OK;
}
if (start) {
return startRecognitionLocked(model, notify);
} else {
return stopRecognitionLocked(model, notify);
}
}
private void onServiceDiedLocked() {
try {
MetricsLogger.count(mContext, "sth_service_died", 1);
sendErrorCallbacksToAll(SoundTrigger.STATUS_DEAD_OBJECT);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
internalClearModelStateLocked();
internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
mModule = null;
}
}
}
// internalClearGlobalStateLocked() cleans up the telephony and power save listeners.
private void internalClearGlobalStateLocked() {
// Unregister from call state changes.
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
// Unregister from power save mode changes.
if (mPowerSaveModeListener != null) {
mContext.unregisterReceiver(mPowerSaveModeListener);
mPowerSaveModeListener = null;
}
}
// Clears state for all models (generic and keyphrase).
private void internalClearModelStateLocked() {
for (ModelData modelData : mModelDataMap.values()) {
modelData.clearState();
}
}
class MyCallStateListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String arg1) {
if (DBG) Slog.d(TAG, "onCallStateChanged: " + state);
synchronized (mLock) {
onCallStateChangedLocked(TelephonyManager.CALL_STATE_IDLE != state);
}
}
}
class PowerSaveModeListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
return;
}
boolean active = mPowerManager.isPowerSaveMode();
if (DBG) Slog.d(TAG, "onPowerSaveModeChanged: " + active);
synchronized (mLock) {
onPowerSaveModeChangedLocked(active);
}
}
}
void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mLock) {
pw.print(" module properties=");
pw.println(mModuleProperties == null ? "null" : mModuleProperties);
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();
}
// Sends an error callback to all models with a valid registered callback.
private void sendErrorCallbacksToAll(int errorCode) throws RemoteException {
for (ModelData modelData : mModelDataMap.values()) {
IRecognitionStatusCallback callback = modelData.getCallback();
if (callback != null) {
callback.onError(STATUS_ERROR);
}
}
}
private ModelData getOrCreateGenericModelDataLocked(UUID modelId) {
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null) {
modelData = ModelData.createGenericModelData(modelId);
mModelDataMap.put(modelId, modelData);
} else if (!modelData.isGenericModel()) {
Slog.e(TAG, "UUID already used for non-generic model.");
return null;
}
return modelData;
}
private void removeKeyphraseModelLocked(int keyphraseId) {
UUID uuid = mKeyphraseUuidMap.get(keyphraseId);
if (uuid == null) {
return;
}
mModelDataMap.remove(uuid);
mKeyphraseUuidMap.remove(keyphraseId);
}
private ModelData getKeyphraseModelDataLocked(int keyphraseId) {
UUID uuid = mKeyphraseUuidMap.get(keyphraseId);
if (uuid == null) {
return null;
}
return mModelDataMap.get(uuid);
}
// Use this to create a new ModelData entry for a keyphrase Id. It will overwrite existing
// mapping if one exists.
private ModelData createKeyphraseModelDataLocked(UUID modelId, int keyphraseId) {
mKeyphraseUuidMap.remove(keyphraseId);
mModelDataMap.remove(modelId);
mKeyphraseUuidMap.put(keyphraseId, modelId);
ModelData modelData = ModelData.createKeyphraseModelData(modelId);
mModelDataMap.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 getModelDataForLocked(int modelHandle) {
// Fetch ModelData object corresponding to the model handle.
for (ModelData model : mModelDataMap.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;
}
// A single routine that implements the start recognition logic for both generic and keyphrase
// models.
private int startRecognitionLocked(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, "startRecognition: Bad data passed in.");
MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
return STATUS_ERROR;
}
if (!isRecognitionAllowed()) {
// Nothing to do here.
Slog.w(TAG, "startRecognition requested but not allowed.");
MetricsLogger.count(mContext, "sth_start_recognition_not_allowed", 1);
return STATUS_OK;
}
int status = mModule.startRecognition(handle, config);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "startRecognition failed with " + status);
MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
// Notify of error if needed.
if (notify) {
try {
callback.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
Slog.i(TAG, "startRecognition successful.");
MetricsLogger.count(mContext, "sth_start_recognition_success", 1);
modelData.setStarted();
// Notify of resume if needed.
if (notify) {
try {
callback.onRecognitionResumed();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
}
}
}
if (DBG) {
Slog.d(TAG, "Model being started :" + modelData.toString());
}
return status;
}
private int stopRecognitionLocked(ModelData modelData, boolean notify) {
IRecognitionStatusCallback callback = modelData.getCallback();
// Stop recognition.
int status = STATUS_OK;
status = mModule.stopRecognition(modelData.getHandle());
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopRecognition call failed with " + status);
MetricsLogger.count(mContext, "sth_stop_recognition_error", 1);
if (notify) {
try {
callback.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
modelData.setStopped();
MetricsLogger.count(mContext, "sth_stop_recognition_success", 1);
// Notify of pause if needed.
if (notify) {
try {
callback.onRecognitionPaused();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
}
}
}
if (DBG) {
Slog.d(TAG, "Model being stopped :" + modelData.toString());
}
return status;
}
private void dumpModelStateLocked() {
for (UUID modelId : mModelDataMap.keySet()) {
ModelData modelData = mModelDataMap.get(modelId);
Slog.i(TAG, "Model :" + modelData.toString());
}
}
// Computes whether we have any recognition running at all (voice or generic). Sets
// the mRecognitionRunning variable with the result.
private boolean computeRecognitionRunningLocked() {
if (mModuleProperties == null || mModule == null) {
mRecognitionRunning = false;
return mRecognitionRunning;
}
for (ModelData modelData : mModelDataMap.values()) {
if (modelData.isModelStarted()) {
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;
// mRequested captures the explicit intent that a start was requested for this model. We
// continue to capture and retain this state even after the model gets started, so that we
// know when a model gets stopped due to "other" reasons, that we should start it again.
// This was the intended behavior of the "mRequested" variable in the previous version of
// this code that we are replicating here.
//
// The "other" reasons include power save, abort being called from the lower layer (due
// to concurrent capture not being supported) and phone call state. Once we recover from
// these transient disruptions, we would start such models again where mRequested == true.
// Thus, mRequested gets reset only when there is an explicit intent to stop the model
// coming from the SoundTriggerService layer that uses this class (and thus eventually
// from the app that manages this model).
private boolean mRequested = false;
// 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 RecognitionConfig mRecognitionConfig = null;
// Model handle is an integer used by the HAL as an identifier for sound
// models.
private int mModelHandle = INVALID_VALUE;
// The SoundModel instance, one of KeyphraseSoundModel or GenericSoundModel.
private SoundModel mSoundModel = null;
private ModelData(UUID modelId, int modelType) {
mModelId = modelId;
// Private constructor, since we require modelType to be one of TYPE_GENERIC,
// TYPE_KEYPHRASE or TYPE_UNKNOWN.
mModelType = modelType;
}
static ModelData createKeyphraseModelData(UUID modelId) {
return new ModelData(modelId, SoundModel.TYPE_KEYPHRASE);
}
static ModelData createGenericModelData(UUID modelId) {
return new ModelData(modelId, SoundModel.TYPE_GENERIC_SOUND);
}
// Note that most of the functionality in this Java class will not work for
// SoundModel.TYPE_UNKNOWN nevertheless we have it since lower layers support it.
static ModelData createModelDataOfUnknownType(UUID modelId) {
return new ModelData(modelId, SoundModel.TYPE_UNKNOWN);
}
synchronized void setCallback(IRecognitionStatusCallback callback) {
mCallback = callback;
}
synchronized IRecognitionStatusCallback getCallback() {
return mCallback;
}
synchronized boolean isModelLoaded() {
return (mModelState == MODEL_LOADED || mModelState == MODEL_STARTED);
}
synchronized boolean isModelNotLoaded() {
return mModelState == MODEL_NOTLOADED;
}
synchronized void setStarted() {
mModelState = MODEL_STARTED;
}
synchronized void setStopped() {
mModelState = MODEL_LOADED;
}
synchronized void setLoaded() {
mModelState = MODEL_LOADED;
}
synchronized boolean isModelStarted() {
return mModelState == MODEL_STARTED;
}
synchronized void clearState() {
mModelState = MODEL_NOTLOADED;
mModelHandle = INVALID_VALUE;
mRecognitionConfig = null;
mRequested = false;
mCallback = null;
}
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 UUID getModelId() {
return mModelId;
}
synchronized RecognitionConfig getRecognitionConfig() {
return mRecognitionConfig;
}
// Whether a start recognition was requested.
synchronized boolean isRequested() {
return mRequested;
}
synchronized void setRequested(boolean requested) {
mRequested = requested;
}
synchronized void setSoundModel(SoundModel soundModel) {
mSoundModel = soundModel;
}
synchronized SoundModel getSoundModel() {
return mSoundModel;
}
synchronized int getModelType() {
return mModelType;
}
synchronized boolean isKeyphraseModel() {
return mModelType == SoundModel.TYPE_KEYPHRASE;
}
synchronized boolean isGenericModel() {
return mModelType == SoundModel.TYPE_GENERIC_SOUND;
}
synchronized String stateToString() {
switch(mModelState) {
case MODEL_NOTLOADED: return "NOT_LOADED";
case MODEL_LOADED: return "LOADED";
case MODEL_STARTED: return "STARTED";
}
return "Unknown state";
}
synchronized String requestedToString() {
return "Requested: " + (mRequested ? "Yes" : "No");
}
synchronized String callbackToString() {
return "Callback: " + (mCallback != null ? mCallback.asBinder() : "null");
}
synchronized String uuidToString() {
return "UUID: " + mModelId;
}
synchronized public String toString() {
return "Handle: " + mModelHandle + "\n" +
"ModelState: " + stateToString() + "\n" +
requestedToString() + "\n" +
callbackToString() + "\n" +
uuidToString() + "\n" + modelTypeToString();
}
synchronized String modelTypeToString() {
String type = null;
switch (mModelType) {
case SoundModel.TYPE_GENERIC_SOUND: type = "Generic"; break;
case SoundModel.TYPE_UNKNOWN: type = "Unknown"; break;
case SoundModel.TYPE_KEYPHRASE: type = "Keyphrase"; break;
}
return "Model type: " + type + "\n";
}
}
}