Fix synchronization issues in AlwaysOnHotwordDetector

- Remove unnecessary recognition status from AlwaysOnHotwordDetector

- Remove unnecessary recognition started callback from IRecognitionStatusCallback

- Fix a bug around the fact that we weren't picking up enrollment at runtime because
we were storing the availability at instantiation time.

- Handle 0-length arrays in SoundTrigger classes while parceling/unparceling

- Fix issue in SoundTrigger helper where we were not comparing binders for start/stop calls

- Unload the previous model when starting a new recognition

- Add more debug logging

Change-Id: Icc56d7f3dd1ffa49a8cfeea49080e3ab4d342c32
diff --git a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl
index 5738909..038d7ef 100644
--- a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl
+++ b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl
@@ -29,10 +29,6 @@
      */
     void onDetected(in byte[] data);
     /**
-     * Called when the detection for the associated keyphrase starts.
-     */
-    void onDetectionStarted();
-    /**
      * Called when the detection for the associated keyphrase stops.
      */
     void onDetectionStopped();
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 7b0a678..9a5cd9b 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -247,7 +247,7 @@
             String text = in.readString();
             int[] users = null;
             int numUsers = in.readInt();
-            if (numUsers > 0) {
+            if (numUsers >= 0) {
                 users = new int[numUsers];
                 in.readIntArray(users);
             }
@@ -264,7 +264,7 @@
                 dest.writeInt(users.length);
                 dest.writeIntArray(users);
             } else {
-                dest.writeInt(0);
+                dest.writeInt(-1);
             }
         }
 
@@ -349,7 +349,7 @@
             UUID uuid = UUID.fromString(in.readString());
             byte[] data = null;
             int dataLength = in.readInt();
-            if (dataLength > 0) {
+            if (dataLength >= 0) {
                 data = new byte[dataLength];
                 in.readByteArray(data);
             }
@@ -369,10 +369,16 @@
                 dest.writeInt(data.length);
                 dest.writeByteArray(data);
             } else {
-                dest.writeInt(0);
+                dest.writeInt(-1);
             }
             dest.writeTypedArray(keyphrases, 0);
         }
+
+        @Override
+        public String toString() {
+            return "KeyphraseSoundModel [keyphrases=" + Arrays.toString(keyphrases) + ", uuid="
+                    + uuid + ", type=" + type + ", data? " + (data != null) + "]";
+        }
     }
 
     /**
@@ -471,7 +477,7 @@
                     in.createTypedArray(KeyphraseRecognitionExtra.CREATOR);
             byte[] data = null;
             int dataLength = in.readInt();
-            if (dataLength > 0) {
+            if (dataLength >= 0) {
                 data = new byte[dataLength];
                 in.readByteArray(data);
             }
@@ -486,7 +492,7 @@
                 dest.writeInt(data.length);
                 dest.writeByteArray(data);
             } else {
-                dest.writeInt(0);
+                dest.writeInt(-1);
             }
         }
 
@@ -494,6 +500,12 @@
         public int describeContents() {
             return 0;
         }
+
+        @Override
+        public String toString() {
+            return "RecognitionConfig [captureRequested=" + captureRequested + ", keyphrases="
+                    + Arrays.toString(keyphrases) + ", data? " + (data != null) + "]";
+        }
     }
 
     /**
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index c4ed8c5..ad30f44 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -27,6 +27,8 @@
 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.os.Handler;
+import android.os.Message;
 import android.os.RemoteException;
 import android.util.Slog;
 
@@ -72,18 +74,6 @@
     public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
     public static final int STATUS_OK = SoundTrigger.STATUS_OK;
 
-    //---- Keyphrase recognition status ----//
-    /** Indicates that recognition is not available. */
-    public static final int RECOGNITION_STATUS_NOT_AVAILABLE = 0x01;
-    /** Indicates that recognition has not been requested. */
-    public static final int RECOGNITION_STATUS_NOT_REQUESTED = 0x02;
-    /** Indicates that recognition has been requested. */
-    public static final int RECOGNITION_STATUS_REQUESTED = 0x04;
-    /** Indicates that recognition has been temporarily disabled. */
-    public static final int RECOGNITION_STATUS_DISABLED_TEMPORARILY = 0x08;
-    /** Indicates that recognition is currently active . */
-    public static final int RECOGNITION_STATUS_ACTIVE = 0x10;
-
     //-- Flags for startRecogntion    ----//
     /** Empty flag for {@link #startRecognition(int)}. */
     public static final int RECOGNITION_FLAG_NONE = 0;
@@ -96,15 +86,22 @@
     //---- Recognition mode flags ----//
     // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.
 
-    /** Simple recognition of the key phrase. Returned by {@link #getRecognitionStatus()} */
+    /**
+     * Simple recognition of the key phrase. Returned by {@link #getSupportedRecognitionModes()}
+     */
     public static final int RECOGNITION_MODE_VOICE_TRIGGER
             = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
-    /** Trigger only if one user is identified. Returned by {@link #getRecognitionStatus()} */
+    /**
+     * Trigger only if one user is identified. Returned by {@link #getSupportedRecognitionModes()}
+     */
     public static final int RECOGNITION_MODE_USER_IDENTIFICATION
             = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
 
     static final String TAG = "AlwaysOnHotwordDetector";
 
+    private static final int MSG_HOTWORD_DETECTED = 1;
+    private static final int MSG_DETECTION_STOPPED = 2;
+
     private final String mText;
     private final String mLocale;
     /**
@@ -118,12 +115,11 @@
      */
     private final KeyphraseSoundModel mEnrolledSoundModel;
     private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
-    private final int mAvailability;
     private final IVoiceInteractionService mVoiceInteractionService;
     private final IVoiceInteractionManagerService mModelManagementService;
     private final SoundTriggerListener mInternalCallback;
-
-    private int mRecognitionState;
+    private final Callback mExternalCallback;
+    private final boolean mDisabled;
 
     /**
      * Callbacks for always-on hotword detection.
@@ -137,10 +133,6 @@
          */
         void onDetected(byte[] data);
         /**
-         * Called when the detection for the associated keyphrase starts.
-         */
-        void onDetectionStarted();
-        /**
          * Called when the detection for the associated keyphrase stops.
          */
         void onDetectionStopped();
@@ -163,7 +155,8 @@
         mLocale = locale;
         mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
         mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
-        mInternalCallback = new SoundTriggerListener(callback);
+        mExternalCallback = callback;
+        mInternalCallback = new SoundTriggerListener(new MyHandler());
         mVoiceInteractionService = voiceInteractionService;
         mModelManagementService = modelManagementService;
         if (mKeyphraseMetadata != null) {
@@ -171,7 +164,9 @@
         } else {
             mEnrolledSoundModel = null;
         }
-        mAvailability = internalGetAvailability();
+        int initialAvailability = internalGetAvailabilityLocked();
+        mDisabled = (initialAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE)
+                || (initialAvailability == KEYPHRASE_UNSUPPORTED);
     }
 
     /**
@@ -186,7 +181,9 @@
      *         {@link #KEYPHRASE_ENROLLED}.
      */
     public int getAvailability() {
-        return mAvailability;
+        synchronized (this) {
+            return internalGetAvailabilityLocked();
+        }
     }
 
     /**
@@ -197,8 +194,13 @@
      *         before calling this method to avoid this exception.
      */
     public int getSupportedRecognitionModes() {
-        if (mAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE
-                || mAvailability == KEYPHRASE_UNSUPPORTED) {
+        synchronized (this) {
+            return getSupportedRecognitionModesLocked();
+        }
+    }
+
+    private int getSupportedRecognitionModesLocked() {
+        if (mDisabled) {
             throw new UnsupportedOperationException(
                     "Getting supported recognition modes for the keyphrase is not supported");
         }
@@ -207,17 +209,6 @@
     }
 
     /**
-     * Gets the status of the recognition.
-     * @return A flag comprised of {@link #RECOGNITION_STATUS_NOT_AVAILABLE},
-     *         {@link #RECOGNITION_STATUS_NOT_REQUESTED}, {@link #RECOGNITION_STATUS_REQUESTED},
-     *         {@link #RECOGNITION_STATUS_DISABLED_TEMPORARILY} and
-     *         {@link #RECOGNITION_STATUS_ACTIVE}.
-     */
-    public int getRecognitionStatus() {
-        return mRecognitionState;
-    }
-
-    /**
      * Starts recognition for the associated keyphrase.
      *
      * @param recognitionFlags The flags to control the recognition properties.
@@ -229,16 +220,19 @@
      *         before calling this method to avoid this exception.
      */
     public int startRecognition(int recognitionFlags) {
-        if (mAvailability != KEYPHRASE_ENROLLED
-                || (mRecognitionState&RECOGNITION_STATUS_NOT_AVAILABLE) != 0) {
+        synchronized (this) {
+            return startRecognitionLocked(recognitionFlags);
+        }
+    }
+
+    private int startRecognitionLocked(int recognitionFlags) {
+        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
             throw new UnsupportedOperationException(
                     "Recognition for the given keyphrase is not supported");
         }
 
-        mRecognitionState &= RECOGNITION_STATUS_REQUESTED;
         KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
         // TODO: Do we need to do something about the confidence level here?
-        // TODO: Take in captureTriggerAudio as a method param here.
         recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id,
                 mKeyphraseMetadata.recognitionModeFlags, new ConfidenceLevel[0]);
         boolean captureTriggerAudio =
@@ -267,12 +261,17 @@
      *         before calling this method to avoid this exception.
      */
     public int stopRecognition() {
-        if (mAvailability != KEYPHRASE_ENROLLED) {
+        synchronized (this) {
+            return stopRecognitionLocked();
+        }
+    }
+
+    private synchronized int stopRecognitionLocked() {
+        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
             throw new UnsupportedOperationException(
                     "Recognition for the given keyphrase is not supported");
         }
 
-        mRecognitionState &= ~RECOGNITION_STATUS_NOT_REQUESTED;
         int code = STATUS_ERROR;
         try {
             code = mModelManagementService.stopRecognition(
@@ -299,8 +298,7 @@
      *         before calling this method to avoid this exception.
      */
     public Intent getManageIntent(int action) {
-        if (mAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE
-                || mAvailability == KEYPHRASE_UNSUPPORTED) {
+        if (mDisabled) {
             throw new UnsupportedOperationException(
                     "Managing the given keyphrase is not supported");
         }
@@ -313,7 +311,7 @@
         return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
     }
 
-    private int internalGetAvailability() {
+    private int internalGetAvailabilityLocked() {
         ModuleProperties dspModuleProperties = null;
         try {
             dspModuleProperties =
@@ -323,21 +321,16 @@
         }
         // No DSP available
         if (dspModuleProperties == null) {
-            mRecognitionState = RECOGNITION_STATUS_NOT_AVAILABLE;
             return KEYPHRASE_HARDWARE_UNAVAILABLE;
         }
         // No enrollment application supports this keyphrase/locale
         if (mKeyphraseMetadata == null) {
-            mRecognitionState = RECOGNITION_STATUS_NOT_AVAILABLE;
             return KEYPHRASE_UNSUPPORTED;
         }
         // This keyphrase hasn't been enrolled.
         if (mEnrolledSoundModel == null) {
-            mRecognitionState = RECOGNITION_STATUS_NOT_AVAILABLE;
             return KEYPHRASE_UNENROLLED;
         }
-        // Mark recognition as available
-        mRecognitionState &= ~RECOGNITION_STATUS_NOT_AVAILABLE;
         return KEYPHRASE_ENROLLED;
     }
 
@@ -358,7 +351,6 @@
                     continue;
                 }
                 for (Keyphrase keyphrase : soundModel.keyphrases) {
-                    // TODO: Check the user handle here to only load a model for the current user.
                     if (keyphrase.id == keyphraseId) {
                         return soundModel;
                     }
@@ -372,28 +364,39 @@
 
     /** @hide */
     static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub {
-        private final Callback mCallback;
+        private final Handler mHandler;
 
-        public SoundTriggerListener(Callback callback) {
-            this.mCallback = callback;
+        public SoundTriggerListener(Handler handler) {
+            mHandler = handler;
         }
 
         @Override
         public void onDetected(byte[] data) {
-            Slog.i(TAG, "onKeyphraseSpoken");
-            mCallback.onDetected(data);
-        }
-
-        @Override
-        public void onDetectionStarted() {
-            // TODO: Set the RECOGNITION_STATUS_ACTIVE flag here.
-            mCallback.onDetectionStarted();
+            Slog.i(TAG, "onDetected");
+            Message message = Message.obtain(mHandler, MSG_HOTWORD_DETECTED);
+            message.obj = data;
+            message.sendToTarget();
         }
 
         @Override
         public void onDetectionStopped() {
-            // TODO: Unset the RECOGNITION_STATUS_ACTIVE flag here.
-            mCallback.onDetectionStopped();
+            Slog.i(TAG, "onDetectionStopped");
+            mHandler.sendEmptyMessage(MSG_DETECTION_STOPPED);
+        }
+    }
+
+    class MyHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_HOTWORD_DETECTED:
+                    mExternalCallback.onDetected((byte[]) msg.obj);
+                    break;
+                case MSG_DETECTION_STOPPED:
+                    mExternalCallback.onDetectionStopped();
+                default:
+                    super.handleMessage(msg);
+            }
         }
     }
 }