SoundTriggerHelper: improve in call behavior

Fix 3 issues in SoundTriggerHelper when a call is active:
1) Do not actually load a sound model when recognition start is requested
but disabled because of and active call or any other reason. Previously
we were loading the sound model even if actual start was deferred until
enabled.
2) Allow to start recognition while disabled: the recognition start
request is stored as pending and will be executed when enabled again.
3) Process call state change with enough delay when call ends to allow
time for the HAL to process the call disconnect notification before re
enabling pending recognitions.

Also remove assumption on valid model handle values.

Test: repro steps in b/152723355
Bug: 152723355
Change-Id: Id46ae8c285426ed2ace8a6d80478fecb98f4ec33
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index 6c0f2b0..edbdd4e 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -40,6 +40,9 @@
 import android.hardware.soundtrigger.SoundTriggerModule;
 import android.os.Binder;
 import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.PowerManager;
 import android.os.PowerManager.ServiceType;
 import android.os.RemoteException;
@@ -113,6 +116,13 @@
 
     private PowerSaveModeListener mPowerSaveModeListener;
 
+    // Handler to process call state changes will delay to allow time for the audio
+    // and sound trigger HALs to process the end of call notifications
+    // before we re enable pending recognition requests.
+    private final Handler mHandler;
+    private static final int MSG_CALL_STATE_CHANGED = 0;
+    private static final int CALL_INACTIVE_MSG_DELAY_MS = 1000;
+
     SoundTriggerHelper(Context context) {
         ArrayList <ModuleProperties> modules = new ArrayList<>();
         int status = SoundTrigger.listModules(modules);
@@ -130,6 +140,31 @@
             // TODO: Figure out how to determine which module corresponds to the DSP hardware.
             mModuleProperties = modules.get(0);
         }
+
+        Looper looper = Looper.myLooper();
+        if (looper == null) {
+            looper = Looper.getMainLooper();
+        }
+        if (looper != null) {
+            mHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case MSG_CALL_STATE_CHANGED:
+                            synchronized (mLock) {
+                                onCallStateChangedLocked(
+                                        TelephonyManager.CALL_STATE_OFFHOOK == msg.arg1);
+                            }
+                            break;
+                        default:
+                            Slog.e(TAG, "unknown message in handler:" + msg.what);
+                            break;
+                    }
+                }
+            };
+        } else {
+            mHandler = null;
+        }
     }
 
     /**
@@ -227,6 +262,37 @@
         return status;
     }
 
+    private int prepareForRecognition(ModelData modelData) {
+        if (mModule == null) {
+            mModule = SoundTrigger.attachModule(mModuleProperties.getId(), this, null);
+            if (mModule == null) {
+                Slog.w(TAG, "prepareForRecognition: cannot attach to sound trigger module");
+                return STATUS_ERROR;
+            }
+        }
+        // Load the model if it is not loaded.
+        if (!modelData.isModelLoaded()) {
+            // Before we try and load this model, we should first make sure that any other
+            // models that don't have an active recognition/dead callback are unloaded. Since
+            // there is a finite limit on the number of models that the hardware may be able to
+            // have loaded, we want to make sure there's room for our model.
+            stopAndUnloadDeadModelsLocked();
+            int[] handle = new int[] { 0 };
+            int status = mModule.loadSoundModel(modelData.getSoundModel(), handle);
+            if (status != SoundTrigger.STATUS_OK) {
+                Slog.w(TAG, "prepareForRecognition: loadSoundModel failed with status: " + status);
+                return status;
+            }
+            modelData.setHandle(handle[0]);
+            modelData.setLoaded();
+            if (DBG) {
+                Slog.d(TAG, "prepareForRecognition: Sound model loaded with handle:" + handle[0]);
+            }
+        }
+        return STATUS_OK;
+    }
+
+
     /**
      * Starts recognition for the given sound model. A single routine for both keyphrase and
      * generic sound models.
@@ -248,12 +314,17 @@
                 Slog.w(TAG, "Attempting startRecognition without the capability");
                 return STATUS_ERROR;
             }
-            if (mModule == null) {
-                mModule = SoundTrigger.attachModule(mModuleProperties.getId(), this, null);
-                if (mModule == null) {
-                    Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
-                    return STATUS_ERROR;
+
+            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();
             }
 
             // If the existing SoundModel is different (for the same UUID for Generic and same
@@ -287,48 +358,25 @@
                 }
             }
 
-            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()) {
-                // Before we try and load this model, we should first make sure that any other
-                // models that don't have an active recognition/dead callback are unloaded. Since
-                // there is a finite limit on the number of models that the hardware may be able to
-                // have loaded, we want to make sure there's room for our model.
-                stopAndUnloadDeadModelsLocked();
-                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);
 
-            int status = startRecognitionLocked(modelData,
+            if (!isRecognitionAllowed()) {
+                initializeTelephonyAndPowerStateListeners();
+                return STATUS_OK;
+            }
+
+            int status = prepareForRecognition(modelData);
+            if (status != STATUS_OK) {
+                Slog.w(TAG, "startRecognition failed to prepare model for recognition");
+                return status;
+            }
+            status = startRecognitionLocked(modelData,
                     false /* Don't notify for synchronous calls */);
 
-                                // Initialize power save, call active state monitoring logic.
+            // Initialize power save, call active state monitoring logic.
             if (status == STATUS_OK) {
                 initializeTelephonyAndPowerStateListeners();
             }
@@ -398,7 +446,8 @@
             if (DBG) {
                 Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId + ", callback =" +
                         callback.asBinder());
-                Slog.d(TAG, "current callback=" + (modelData == null ? "null" :
+                Slog.d(TAG, "current callback="
+                        + ((modelData == null || modelData.getCallback() == null) ? "null" :
                             modelData.getCallback().asBinder()));
             }
             int status = stopRecognition(modelData, callback);
@@ -507,8 +556,8 @@
         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()) {
+            if (mModule == null || modelData == null || !modelData.isModelLoaded()
+                    || !modelData.isKeyphraseModel()) {
                 return STATUS_ERROR;
             }
 
@@ -943,6 +992,10 @@
             return STATUS_OK;
         }
         if (start) {
+            int status = prepareForRecognition(model);
+            if (status != STATUS_OK) {
+                return status;
+            }
             return startRecognitionLocked(model, notify);
         } else {
             return stopRecognitionLocked(model, notify);
@@ -992,8 +1045,15 @@
         @Override
         public void onCallStateChanged(int state, String arg1) {
             if (DBG) Slog.d(TAG, "onCallStateChanged: " + state);
-            synchronized (mLock) {
-                onCallStateChangedLocked(TelephonyManager.CALL_STATE_OFFHOOK == state);
+
+            if (mHandler != null) {
+                synchronized (mLock) {
+                    mHandler.removeMessages(MSG_CALL_STATE_CHANGED);
+                    Message msg = mHandler.obtainMessage(MSG_CALL_STATE_CHANGED, state, 0);
+                    mHandler.sendMessageDelayed(
+                            msg, (TelephonyManager.CALL_STATE_OFFHOOK == state) ? 0
+                                    : CALL_INACTIVE_MSG_DELAY_MS);
+                }
             }
         }
     }
@@ -1216,9 +1276,8 @@
     // 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) {
+        if (callback == null || !modelData.isModelLoaded() || config == null) {
             // Nothing to do here.
             Slog.w(TAG, "startRecognition: Bad data passed in.");
             MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
@@ -1235,7 +1294,7 @@
         if (mModule == null) {
             return STATUS_ERROR;
         }
-        int status = mModule.startRecognition(handle, config);
+        int status = mModule.startRecognition(modelData.getHandle(), config);
         if (status != SoundTrigger.STATUS_OK) {
             Slog.w(TAG, "startRecognition failed with " + status);
             MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
@@ -1376,7 +1435,7 @@
 
         // Model handle is an integer used by the HAL as an identifier for sound
         // models.
-        private int mModelHandle = INVALID_VALUE;
+        private int mModelHandle;
 
         // The SoundModel instance, one of KeyphraseSoundModel or GenericSoundModel.
         private SoundModel mSoundModel = null;
@@ -1436,7 +1495,6 @@
 
         synchronized void clearState() {
             mModelState = MODEL_NOTLOADED;
-            mModelHandle = INVALID_VALUE;
             mRecognitionConfig = null;
             mRequested = false;
             mCallback = null;