blob: 40687b01ceec8d55263d865220f7b96eff8d2b8d [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 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;
// TODO: Since the voice layer currently only handles one recognition
// we simplify things by assuming one listener here too.
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;
// 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;
// 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;
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);
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());
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.
*
* @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 = getOrCreateGenericModelDataLocked(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.setLoaded();
Slog.d(TAG, "Generic sound model loaded with handle:" + handle[0]);
}
modelData.setCallback(callback);
modelData.setRecognitionConfig(recognitionConfig);
// Don't notify for synchronous calls.
return startGenericRecognitionLocked(modelData, false);
}
}
/**
* Starts recognition for the given keyphraseId.
*
* @param keyphraseId The identifier of the keyphrase for which
* the recognition is to be started.
* @param soundModel The sound model to use for recognition.
* @param listener The listener for the recognition events related to the given keyphrase.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int startKeyphraseRecognition(int keyphraseId,
KeyphraseSoundModel soundModel,
IRecognitionStatusCallback listener,
RecognitionConfig recognitionConfig) {
if (soundModel == null || listener == null || recognitionConfig == null) {
return STATUS_ERROR;
}
synchronized (mLock) {
if (DBG) {
Slog.d(TAG, "startKeyphraseRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", listener=" + listener.asBinder()
+ ", recognitionConfig=" + recognitionConfig);
Slog.d(TAG, "moduleProperties=" + mModuleProperties);
Slog.d(TAG, "current listener="
+ (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
Slog.d(TAG, "current SoundModel handle=" + mCurrentKeyphraseModelHandle);
Slog.d(TAG, "current SoundModel UUID="
+ (mCurrentSoundModel == null ? null : mCurrentSoundModel.uuid));
}
if (!mRecognitionRunning) {
initializeTelephonyAndPowerStateListeners();
}
if (mModuleProperties == null) {
Slog.w(TAG, "Attempting startKeyphraseRecognition without the capability");
return STATUS_ERROR;
}
if (mModule == null) {
mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
if (mModule == null) {
Slog.w(TAG, "startKeyphraseRecognition cannot attach to sound trigger module");
return STATUS_ERROR;
}
}
// 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 (mCurrentKeyphraseModelHandle != INVALID_VALUE
&& !soundModel.equals(mCurrentSoundModel)) {
Slog.w(TAG, "Unloading previous sound model");
int status = mModule.unloadSoundModel(mCurrentKeyphraseModelHandle);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadSoundModel call failed with " + status);
}
internalClearKeyphraseSoundModelLocked();
mKeyphraseStarted = false;
}
// If the previous recognition was by a different listener,
// Notify them that it was stopped.
if (mKeyphraseListener != null && mKeyphraseListener.asBinder() != listener.asBinder()) {
Slog.w(TAG, "Canceling previous recognition");
try {
mKeyphraseListener.onError(STATUS_ERROR);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetectionStopped", e);
}
mKeyphraseListener = null;
}
// Load the sound model if the current one is null.
int soundModelHandle = mCurrentKeyphraseModelHandle;
if (mCurrentKeyphraseModelHandle == INVALID_VALUE
|| mCurrentSoundModel == null) {
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;
}
soundModelHandle = handle[0];
} else {
if (DBG) Slog.d(TAG, "Reusing previously loaded sound model");
}
// Start the recognition.
mRequested = true;
mKeyphraseId = keyphraseId;
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.
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.isModelStarted()) {
// startGenericRecognition hasn't been called or it failed.
Slog.w(TAG, "Attempting stopGenericRecognition without a successful" +
" startGenericRecognition");
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 stopGenericRecognition for another recognition");
return STATUS_ERROR;
}
int status = stopGenericRecognitionLocked(modelData,
false /* don't notify for synchronous calls */);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopGenericRecognition failed: " + status);
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();
if (!computeRecognitionRunningLocked()) {
internalClearGlobalStateLocked();
}
return status;
}
}
/**
* Stops recognition for the given {@link Keyphrase} if a recognition is
* currently active.
*
* @param keyphraseId The identifier of the keyphrase for which
* the recognition is to be stopped.
* @param listener The listener for the recognition events related to the given keyphrase.
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
if (listener == null) {
return STATUS_ERROR;
}
synchronized (mLock) {
if (DBG) {
Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId
+ ", listener=" + listener.asBinder());
Slog.d(TAG, "current listener="
+ (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
}
if (mModuleProperties == null || mModule == null) {
Slog.w(TAG, "Attempting stopRecognition without the capability");
return STATUS_ERROR;
}
if (mKeyphraseListener == null) {
// startRecognition hasn't been called or it failed.
Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
return STATUS_ERROR;
}
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");
return STATUS_ERROR;
}
// Stop recognition if it's the current one, ignore otherwise.
mRequested = false;
int status = updateRecognitionLocked(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.
internalClearKeyphraseStateLocked();
internalClearGlobalStateLocked();
return status;
}
}
/**
* Stops all recognitions active currently and clears the internal state.
*/
void stopAllRecognitions() {
synchronized (mLock) {
if (mModuleProperties == null || mModule == null) {
return;
}
// Stop Keyphrase recognition if one exists.
if (mCurrentKeyphraseModelHandle != INVALID_VALUE) {
mRequested = false;
int status = updateRecognitionLocked(
false /* don't notify for synchronous calls */);
internalClearKeyphraseStateLocked();
}
// Stop all generic recognition models.
for (ModelData model : mGenericModelDataMap.values()) {
if (model.isModelStarted()) {
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 mModuleProperties;
}
int unloadKeyphraseSoundModel(int keyphraseId) {
if (mModule == null || mCurrentKeyphraseModelHandle == INVALID_VALUE) {
return STATUS_ERROR;
}
if (mKeyphraseId != keyphraseId) {
Slog.w(TAG, "Given sound model is not the one loaded.");
return STATUS_ERROR;
}
synchronized (mLock) {
// Stop recognition if it's the current one.
mRequested = false;
int status = updateRecognitionLocked(false /* don't notify */);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "Stop recognition failed for keyphrase ID:" + status);
}
status = mModule.unloadSoundModel(mCurrentKeyphraseModelHandle);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadKeyphraseSoundModel call failed with " + status);
}
internalClearKeyphraseSoundModelLocked();
return status;
}
}
int unloadGenericSoundModel(UUID modelId) {
if (modelId == null || mModule == null) {
return STATUS_ERROR;
}
synchronized (mLock) {
ModelData modelData = mGenericModelDataMap.get(modelId);
if (modelData == null) {
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 = stopGenericRecognitionLocked(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.");
}
mGenericModelDataMap.remove(modelId);
if (DBG) dumpGenericModelStateLocked();
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) {
// Fire aborts/failures to all listeners since it's not tied to a keyphrase.
case SoundTrigger.RECOGNITION_STATUS_ABORT:
onRecognitionAbortLocked();
break;
case SoundTrigger.RECOGNITION_STATUS_FAILURE:
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) {
if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) {
return;
}
ModelData model = getModelDataForLocked(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.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;
}
// 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) {
Slog.w(TAG, "Invalid sound model event!");
return;
}
if (DBG) Slog.d(TAG, "onSoundModelUpdate: " + event);
synchronized (mLock) {
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!!");
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;
updateRecognitionLocked(true /* notify */);
}
private void onPowerSaveModeChangedLocked(boolean isPowerSaveMode) {
if (mIsPowerSaveMode == isPowerSaveMode) {
return;
}
mIsPowerSaveMode = isPowerSaveMode;
updateRecognitionLocked(true /* notify */);
}
private void onSoundModelUpdatedLocked(SoundModelEvent event) {
// TODO: Handle sound model update here.
}
private void onServiceStateChangedLocked(boolean disabled) {
if (disabled == mServiceDisabled) {
return;
}
mServiceDisabled = disabled;
updateRecognitionLocked(true /* notify */);
}
private void onRecognitionAbortLocked() {
Slog.w(TAG, "Recognition aborted");
// If abort has been called, the hardware has already stopped recognition, so we shouldn't
// call it again when we process the state change.
mRecognitionAborted = true;
}
private void onRecognitionFailureLocked() {
Slog.w(TAG, "Recognition failure");
try {
if (mKeyphraseListener != null) {
mKeyphraseListener.onError(STATUS_ERROR);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
internalClearKeyphraseStateLocked();
internalClearGlobalStateLocked();
}
}
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) {
Slog.w(TAG, "Invalid keyphrase recognition event!");
return;
}
// TODO: Handle more than one keyphrase extras.
if (mKeyphraseId != keyphraseExtras[0].id) {
Slog.w(TAG, "received onRecognition event for a different keyphrase");
return;
}
try {
if (mKeyphraseListener != null) {
mKeyphraseListener.onKeyphraseDetected((KeyphraseRecognitionEvent) event);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onKeyphraseDetected", e);
}
mKeyphraseStarted = false;
mRequested = mRecognitionConfig.allowMultipleTriggers;
// TODO: Remove this block if the lower layer supports multiple triggers.
if (mRequested) {
updateRecognitionLocked(true /* notify */);
}
}
private void onServiceDiedLocked() {
try {
if (mKeyphraseListener != null) {
mKeyphraseListener.onError(SoundTrigger.STATUS_DEAD_OBJECT);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
internalClearKeyphraseSoundModelLocked();
internalClearKeyphraseStateLocked();
internalClearGenericModelStateLocked();
internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
mModule = null;
}
}
}
private int updateRecognitionLocked(boolean notify) {
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 == mKeyphraseStarted) {
// No-op.
return STATUS_OK;
}
// See if the recognition needs to be started.
if (start) {
// Start recognition.
int status = mModule.startRecognition(mCurrentKeyphraseModelHandle,
mRecognitionConfig);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "startKeyphraseRecognition failed with " + status);
// Notify of error if needed.
if (notify) {
try {
mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
mKeyphraseStarted = true;
// Notify of resume if needed.
if (notify) {
try {
mKeyphraseListener.onRecognitionResumed();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
}
}
}
return status;
} else {
// Stop recognition (only if we haven't been aborted).
int status = STATUS_OK;
if (!mRecognitionAborted) {
status = mModule.stopRecognition(mCurrentKeyphraseModelHandle);
} else {
mRecognitionAborted = false;
}
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "stopRecognition call failed with " + status);
if (notify) {
try {
mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
mKeyphraseStarted = false;
// Notify of pause if needed.
if (notify) {
try {
mKeyphraseListener.onRecognitionPaused();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
}
}
}
return status;
}
}
// 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);
// Unregister from power save mode changes.
if (mPowerSaveModeListener != null) {
mContext.unregisterReceiver(mPowerSaveModeListener);
mPowerSaveModeListener = null;
}
}
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;
}
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(" keyphrase ID="); pw.println(mKeyphraseId);
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(mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder());
pw.print(" requested="); pw.println(mRequested);
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 getOrCreateGenericModelDataLocked(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 getModelDataForLocked(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, "startGenericRecognition 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 {
Slog.i(TAG, "startRecognition successful.");
modelData.setStarted();
// Notify of resume if needed.
if (notify) {
try {
callback.onRecognitionResumed();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
}
}
}
if (DBG) dumpGenericModelStateLocked();
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);
}
}
}
if (DBG) dumpGenericModelStateLocked();
return status;
}
private void dumpGenericModelStateLocked() {
for (UUID modelId : mGenericModelDataMap.keySet()) {
ModelData modelData = mGenericModelDataMap.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;
}
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.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;
// 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;
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);
}
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;
}
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;
}
String stateToString() {
switch(mModelState) {
case MODEL_NOTLOADED: return "NOT_LOADED";
case MODEL_LOADED: return "LOADED";
case MODEL_STARTED: return "STARTED";
}
return "Unknown state";
}
public String toString() {
return "Handle: " + mModelHandle + "ModelState: " + stateToString();
}
}
}