AlwaysOnHotwordDetector needs to reflect enrollment changes

Add a callback for when any sound model change happens. This helps the VIS
to re-check the availability and either enroll the user, or start/stop recognition.

Also shut down any active recognition when VIS dies, or a different hotword detector instance is obtained from VIS.

Change-Id: I03f94e78c6ee307afe822a84aebc7e74c64de7b4
diff --git a/api/current.txt b/api/current.txt
index cb54a5f..863a88b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -27324,10 +27324,6 @@
     method public int getSupportedRecognitionModes();
     method public int startRecognition(int);
     method public int stopRecognition();
-    field public static final int KEYPHRASE_ENROLLED = 2; // 0x2
-    field public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
-    field public static final int KEYPHRASE_UNENROLLED = 1; // 0x1
-    field public static final int KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
     field public static final int MANAGE_ACTION_ENROLL = 0; // 0x0
     field public static final int MANAGE_ACTION_RE_ENROLL = 1; // 0x1
     field public static final int MANAGE_ACTION_UN_ENROLL = 2; // 0x2
@@ -27335,6 +27331,11 @@
     field public static final int RECOGNITION_FLAG_NONE = 0; // 0x0
     field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
     field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
+    field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
+    field public static final int STATE_INVALID = -3; // 0xfffffffd
+    field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
+    field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
+    field public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
     field public static final int STATUS_ERROR = -2147483648; // 0x80000000
     field public static final int STATUS_OK = 0; // 0x0
   }
@@ -27346,10 +27347,12 @@
 
   public class VoiceInteractionService extends android.app.Service {
     ctor public VoiceInteractionService();
-    method public final android.service.voice.AlwaysOnHotwordDetector getAlwaysOnHotwordDetector(java.lang.String, java.lang.String, android.service.voice.AlwaysOnHotwordDetector.Callback);
+    method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.lang.String, android.service.voice.AlwaysOnHotwordDetector.Callback);
     method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
     method public android.os.IBinder onBind(android.content.Intent);
     method public void onReady();
+    method public void onShutdown();
+    method public void onSoundModelsChanged();
     method public void startSession(android.os.Bundle);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService";
     field public static final java.lang.String SERVICE_META_DATA = "android.voice_interaction";
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index ad30f44..27a7b8e 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -41,26 +41,32 @@
  * always-on keyphrase detection APIs.
  */
 public class AlwaysOnHotwordDetector {
-    //---- States of Keyphrase availability ----//
+    //---- States of Keyphrase availability. Return codes for getAvailability() ----//
     /**
-     * Indicates that the given keyphrase is not available on the system because of the
-     * hardware configuration.
+     * Indicates that this hotword detector is no longer valid for any recognition
+     * and should not be used anymore.
      */
-    public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2;
+    public static final int STATE_INVALID = -3;
     /**
-     * Indicates that the given keyphrase is not supported.
+     * Indicates that recognition for the given keyphrase is not available on the system
+     * because of the hardware configuration.
      */
-    public static final int KEYPHRASE_UNSUPPORTED = -1;
+    public static final int STATE_HARDWARE_UNAVAILABLE = -2;
+    /**
+     * Indicates that recognition for the given keyphrase is not supported.
+     */
+    public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
     /**
      * Indicates that the given keyphrase is not enrolled.
      */
-    public static final int KEYPHRASE_UNENROLLED = 1;
+    public static final int STATE_KEYPHRASE_UNENROLLED = 1;
     /**
-     * Indicates that the given keyphrase is currently enrolled but not being actively listened for.
+     * Indicates that the given keyphrase is currently enrolled and it's possible to start
+     * recognition for it.
      */
-    public static final int KEYPHRASE_ENROLLED = 2;
+    public static final int STATE_KEYPHRASE_ENROLLED = 2;
 
-    // Keyphrase management actions ----//
+    // Keyphrase management actions. Used in getManageIntent() ----//
     /** Indicates that we need to enroll. */
     public static final int MANAGE_ACTION_ENROLL = 0;
     /** Indicates that we need to re-enroll. */
@@ -83,7 +89,7 @@
      */
     public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
 
-    //---- Recognition mode flags ----//
+    //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
     // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.
 
     /**
@@ -109,17 +115,20 @@
      * This may be null if this keyphrase isn't supported by the enrollment application.
      */
     private final KeyphraseMetadata mKeyphraseMetadata;
-    /**
-     * The sound model for the keyphrase, derived from the model management service
-     * (IVoiceInteractionManagerService). May be null if the keyphrase isn't enrolled yet.
-     */
-    private final KeyphraseSoundModel mEnrolledSoundModel;
     private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
     private final IVoiceInteractionService mVoiceInteractionService;
     private final IVoiceInteractionManagerService mModelManagementService;
     private final SoundTriggerListener mInternalCallback;
     private final Callback mExternalCallback;
     private final boolean mDisabled;
+    private final Object mLock = new Object();
+
+    /**
+     * The sound model for the keyphrase, derived from the model management service
+     * (IVoiceInteractionManagerService). May be null if the keyphrase isn't enrolled yet.
+     */
+    private KeyphraseSoundModel mEnrolledSoundModel;
+    private boolean mInvalidated;
 
     /**
      * Callbacks for always-on hotword detection.
@@ -151,6 +160,7 @@
             KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
             IVoiceInteractionService voiceInteractionService,
             IVoiceInteractionManagerService modelManagementService) {
+        mInvalidated = false;
         mText = text;
         mLocale = locale;
         mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
@@ -160,28 +170,34 @@
         mVoiceInteractionService = voiceInteractionService;
         mModelManagementService = modelManagementService;
         if (mKeyphraseMetadata != null) {
-            mEnrolledSoundModel = internalGetKeyphraseSoundModel(mKeyphraseMetadata.id);
-        } else {
-            mEnrolledSoundModel = null;
+            mEnrolledSoundModel = internalGetKeyphraseSoundModelLocked(mKeyphraseMetadata.id);
         }
         int initialAvailability = internalGetAvailabilityLocked();
-        mDisabled = (initialAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE)
-                || (initialAvailability == KEYPHRASE_UNSUPPORTED);
+        mDisabled = (initialAvailability == STATE_HARDWARE_UNAVAILABLE)
+                || (initialAvailability == STATE_KEYPHRASE_UNSUPPORTED);
     }
 
     /**
      * Gets the state of always-on hotword detection for the given keyphrase and locale
      * on this system.
      * Availability implies that the hardware on this system is capable of listening for
-     * the given keyphrase or not.
+     * the given keyphrase or not. <p/>
+     * If the return code is one of {@link #STATE_HARDWARE_UNAVAILABLE} or
+     * {@link #STATE_KEYPHRASE_UNSUPPORTED}, no further interaction should be performed with this
+     * detector. <br/>
+     * If the state is {@link #STATE_KEYPHRASE_UNENROLLED} the caller may choose to begin
+     * an enrollment flow for the keyphrase. <br/>
+     * For {@value #STATE_KEYPHRASE_ENROLLED} a recognition can be started as desired. <br/>
+     * If the return code is {@link #STATE_INVALID}, this detector is stale and must not be used.
+     * A new detector should be obtained and used.
      *
      * @return Indicates if always-on hotword detection is available for the given keyphrase.
-     *         The return code is one of {@link #KEYPHRASE_HARDWARE_UNAVAILABLE},
-     *         {@link #KEYPHRASE_UNSUPPORTED}, {@link #KEYPHRASE_UNENROLLED} or
-     *         {@link #KEYPHRASE_ENROLLED}.
+     *         The return code is one of {@link #STATE_HARDWARE_UNAVAILABLE},
+     *         {@link #STATE_KEYPHRASE_UNSUPPORTED}, {@link #STATE_KEYPHRASE_UNENROLLED},
+     *         {@link #STATE_KEYPHRASE_ENROLLED}, or {@link #STATE_INVALID}.
      */
     public int getAvailability() {
-        synchronized (this) {
+        synchronized (mLock) {
             return internalGetAvailabilityLocked();
         }
     }
@@ -194,7 +210,7 @@
      *         before calling this method to avoid this exception.
      */
     public int getSupportedRecognitionModes() {
-        synchronized (this) {
+        synchronized (mLock) {
             return getSupportedRecognitionModesLocked();
         }
     }
@@ -220,13 +236,13 @@
      *         before calling this method to avoid this exception.
      */
     public int startRecognition(int recognitionFlags) {
-        synchronized (this) {
+        synchronized (mLock) {
             return startRecognitionLocked(recognitionFlags);
         }
     }
 
     private int startRecognitionLocked(int recognitionFlags) {
-        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
+        if (internalGetAvailabilityLocked() != STATE_KEYPHRASE_ENROLLED) {
             throw new UnsupportedOperationException(
                     "Recognition for the given keyphrase is not supported");
         }
@@ -261,13 +277,13 @@
      *         before calling this method to avoid this exception.
      */
     public int stopRecognition() {
-        synchronized (this) {
+        synchronized (mLock) {
             return stopRecognitionLocked();
         }
     }
 
-    private synchronized int stopRecognitionLocked() {
-        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
+    private int stopRecognitionLocked() {
+        if (internalGetAvailabilityLocked() != STATE_KEYPHRASE_ENROLLED) {
             throw new UnsupportedOperationException(
                     "Recognition for the given keyphrase is not supported");
         }
@@ -312,6 +328,10 @@
     }
 
     private int internalGetAvailabilityLocked() {
+        if (mInvalidated) {
+            return STATE_INVALID;
+        }
+
         ModuleProperties dspModuleProperties = null;
         try {
             dspModuleProperties =
@@ -321,23 +341,51 @@
         }
         // No DSP available
         if (dspModuleProperties == null) {
-            return KEYPHRASE_HARDWARE_UNAVAILABLE;
+            return STATE_HARDWARE_UNAVAILABLE;
         }
         // No enrollment application supports this keyphrase/locale
         if (mKeyphraseMetadata == null) {
-            return KEYPHRASE_UNSUPPORTED;
+            return STATE_KEYPHRASE_UNSUPPORTED;
         }
+
         // This keyphrase hasn't been enrolled.
         if (mEnrolledSoundModel == null) {
-            return KEYPHRASE_UNENROLLED;
+            return STATE_KEYPHRASE_UNENROLLED;
         }
-        return KEYPHRASE_ENROLLED;
+        return STATE_KEYPHRASE_ENROLLED;
+    }
+
+    /**
+     * Invalidates this hotword detector so that any future calls to this result
+     * in an IllegalStateException.
+     *
+     * @hide
+     */
+    void invalidate() {
+        synchronized (mLock) {
+            mInvalidated = true;
+        }
+    }
+
+    /**
+     * Reloads the sound models from the service.
+     *
+     * @hide
+     */
+    void onSoundModelsChanged() {
+        synchronized (mLock) {
+            // TODO: This should stop the recognition if it was using an enrolled sound model
+            // that's no longer available.
+            if (mKeyphraseMetadata != null) {
+                mEnrolledSoundModel = internalGetKeyphraseSoundModelLocked(mKeyphraseMetadata.id);
+            }
+        }
     }
 
     /**
      * @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
      */
-    private KeyphraseSoundModel internalGetKeyphraseSoundModel(int keyphraseId) {
+    private KeyphraseSoundModel internalGetKeyphraseSoundModelLocked(int keyphraseId) {
         List<KeyphraseSoundModel> soundModels;
         try {
             soundModels = mModelManagementService
diff --git a/core/java/android/service/voice/IVoiceInteractionService.aidl b/core/java/android/service/voice/IVoiceInteractionService.aidl
index c9915a2..e8265a2 100644
--- a/core/java/android/service/voice/IVoiceInteractionService.aidl
+++ b/core/java/android/service/voice/IVoiceInteractionService.aidl
@@ -21,4 +21,6 @@
  */
 oneway interface IVoiceInteractionService {
     void ready();
+    void soundModelsChanged();
+    void shutdown();
 }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 982f43d..0c2ba26 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -28,8 +28,8 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-
 import android.provider.Settings;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractionManagerService;
 
@@ -70,15 +70,27 @@
         @Override public void ready() {
             mHandler.sendEmptyMessage(MSG_READY);
         }
+        @Override public void shutdown() {
+            mHandler.sendEmptyMessage(MSG_SHUTDOWN);
+        }
+        @Override public void soundModelsChanged() {
+            mHandler.sendEmptyMessage(MSG_SOUND_MODELS_CHANGED);
+        }
     };
 
     MyHandler mHandler;
 
     IVoiceInteractionManagerService mSystemService;
 
+    private final Object mLock = new Object();
+
     private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
 
+    private AlwaysOnHotwordDetector mHotwordDetector;
+
     static final int MSG_READY = 1;
+    static final int MSG_SHUTDOWN = 2;
+    static final int MSG_SOUND_MODELS_CHANGED = 3;
 
     class MyHandler extends Handler {
         @Override
@@ -87,6 +99,12 @@
                 case MSG_READY:
                     onReady();
                     break;
+                case MSG_SHUTDOWN:
+                    onShutdownInternal();
+                    break;
+                case MSG_SOUND_MODELS_CHANGED:
+                    onSoundModelsChangedInternal();
+                    break;
                 default:
                     super.handleMessage(msg);
             }
@@ -140,9 +158,10 @@
 
     /**
      * Called during service initialization to tell you when the system is ready
-     * to receive interaction from it.  You should generally do initialization here
-     * rather than in {@link #onCreate()}.  Methods such as {@link #startSession}
-     * and {@link #getAlwaysOnHotwordDetector} will not be operational until this point.
+     * to receive interaction from it. You should generally do initialization here
+     * rather than in {@link #onCreate()}. Methods such as {@link #startSession(Bundle)} and
+     * {@link #createAlwaysOnHotwordDetector(String, String, android.service.voice.AlwaysOnHotwordDetector.Callback)}
+     * will not be operational until this point.
      */
     public void onReady() {
         mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
@@ -150,22 +169,67 @@
         mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager());
     }
 
+    private void onShutdownInternal() {
+        onShutdown();
+        // Stop any active recognitions when shutting down.
+        // This ensures that if implementations forget to stop any active recognition,
+        // It's still guaranteed to have been stopped.
+        // This helps with cases where the voice interaction implementation is changed
+        // by the user.
+        safelyShutdownHotwordDetector();
+    }
+
     /**
+     * Called during service de-initialization to tell you when the system is shutting the
+     * service down.
+     */
+    public void onShutdown() {
+    }
+
+    private void onSoundModelsChangedInternal() {
+        synchronized (this) {
+            if (mHotwordDetector != null) {
+                // TODO: Stop recognition if a sound model that was being recognized gets deleted.
+                mHotwordDetector.onSoundModelsChanged();
+            }
+        }
+        onSoundModelsChanged();
+    }
+
+    /**
+     * Called when the sound models available for recognition change.
+     * This may be called if a new sound model is available or
+     * an existing one is updated or removed.
+     * Implementations must check the availability of the hotword detector if they own one
+     * by calling {@link AlwaysOnHotwordDetector#getAvailability()} before calling into it.
+     */
+    public void onSoundModelsChanged() {
+    }
+
+    /**
+     * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
+     * This instance must be retained and used by the client.
+     * Calling this a second time invalidates the previously created hotword detector
+     * which can no longer be used to manage recognition.
+     *
      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
      * @param locale The locale for which the enrollment needs to be performed.
      *        This is a Java locale, for example "en_US".
      * @param callback The callback to notify of detection events.
      * @return An always-on hotword detector for the given keyphrase and locale.
      */
-    public final AlwaysOnHotwordDetector getAlwaysOnHotwordDetector(
+    public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
             String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) {
         if (mSystemService == null) {
             throw new IllegalStateException("Not available until onReady() is called");
         }
-        // TODO: Cache instances and return the same one instead of creating a new interactor
-        // for the same keyphrase/locale combination.
-        return new AlwaysOnHotwordDetector(keyphrase, locale, callback,
-                mKeyphraseEnrollmentInfo, mInterface, mSystemService);
+        synchronized (mLock) {
+            // Allow only one concurrent recognition via the APIs.
+            safelyShutdownHotwordDetector();
+            mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback,
+                    mKeyphraseEnrollmentInfo, mInterface, mSystemService);
+        }
+        return mHotwordDetector;
     }
 
     /**
@@ -176,4 +240,18 @@
     protected final KeyphraseEnrollmentInfo getKeyphraseEnrollmentInfo() {
         return mKeyphraseEnrollmentInfo;
     }
+
+    private void safelyShutdownHotwordDetector() {
+        try {
+            synchronized (mLock) {
+                if (mHotwordDetector != null) {
+                    mHotwordDetector.stopRecognition();
+                    mHotwordDetector.invalidate();
+                    mHotwordDetector = null;
+                }
+            }
+        } catch (Exception ex) {
+            // Ignore.
+        }
+    }
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 2ce4971..7b2e4f1 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -290,19 +290,22 @@
 
                 final long caller = Binder.clearCallingIdentity();
                 try {
-                    // If the keyphrases are not present in the model, delete the model.
+                    boolean success = false;
                     if (model.keyphrases == null) {
-                        if (mDbHelper.deleteKeyphraseSoundModel(model.uuid)) {
-                            return SoundTriggerHelper.STATUS_OK;
-                        } else {
-                            return SoundTriggerHelper.STATUS_ERROR;
-                        }
+                        // If the keyphrases are not present in the model, delete the model.
+                        success = mDbHelper.deleteKeyphraseSoundModel(model.uuid);
                     } else {
-                        if (mDbHelper.addOrUpdateKeyphraseSoundModel(model)) {
-                            return SoundTriggerHelper.STATUS_OK;
-                        } else {
-                            return SoundTriggerHelper.STATUS_ERROR;
+                        // Else update the model.
+                        success = mDbHelper.addOrUpdateKeyphraseSoundModel(model);
+                    }
+                    if (success) {
+                        // Notify the voice interaction service of a change in sound models.
+                        if (mImpl != null && mImpl.mService != null) {
+                            mImpl.notifySoundModelsChangedLocked();
                         }
+                        return SoundTriggerHelper.STATUS_OK;
+                    } else {
+                        return SoundTriggerHelper.STATUS_ERROR;
                     }
                 } finally {
                     Binder.restoreCallingIdentity(caller);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 54af5d4..94f227c 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -96,6 +96,13 @@
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
+            try {
+                if (mService != null) {
+                    mService.shutdown();
+                }
+            } catch (RemoteException e) {
+                Slog.w(TAG, "RemoteException in shutdown", e);
+            }
             mService = null;
         }
     };
@@ -308,4 +315,15 @@
             mContext.unregisterReceiver(mBroadcastReceiver);
         }
     }
+
+    void notifySoundModelsChangedLocked() {
+        if (mService == null) {
+            Slog.w(TAG, "Not bound to voice interaction service " + mComponent);
+        }
+        try {
+            mService.soundModelsChanged();
+        } catch (RemoteException e) {
+            Slog.w(TAG, "RemoteException while calling soundModelsChanged", e);
+        }
+    }
 }
diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
index e74307f..e750bb6 100644
--- a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
+++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
@@ -50,30 +50,18 @@
         Log.i(TAG, "Keyphrase enrollment meta-data: "
                 + Arrays.toString(getKeyphraseEnrollmentInfo().listKeyphraseMetadata()));
 
-        mHotwordDetector = getAlwaysOnHotwordDetector("Hello There", "en-US", mHotwordCallback);
+        mHotwordDetector = createAlwaysOnHotwordDetector("Hello There", "en-US", mHotwordCallback);
+        testHotwordAvailabilityStates();
+    }
+
+    @Override
+    public void onSoundModelsChanged() {
         int availability = mHotwordDetector.getAvailability();
         Log.i(TAG, "Hotword availability = " + availability);
-
-        switch (availability) {
-            case AlwaysOnHotwordDetector.KEYPHRASE_HARDWARE_UNAVAILABLE:
-                Log.i(TAG, "KEYPHRASE_HARDWARE_UNAVAILABLE");
-                break;
-            case AlwaysOnHotwordDetector.KEYPHRASE_UNSUPPORTED:
-                Log.i(TAG, "KEYPHRASE_UNSUPPORTED");
-                break;
-            case AlwaysOnHotwordDetector.KEYPHRASE_UNENROLLED:
-                Log.i(TAG, "KEYPHRASE_UNENROLLED");
-                Intent enroll = mHotwordDetector.getManageIntent(
-                        AlwaysOnHotwordDetector.MANAGE_ACTION_ENROLL);
-                Log.i(TAG, "Need to enroll with " + enroll);
-                break;
-            case AlwaysOnHotwordDetector.KEYPHRASE_ENROLLED:
-                Log.i(TAG, "KEYPHRASE_ENROLLED");
-                int status = mHotwordDetector.startRecognition(
-                        AlwaysOnHotwordDetector.RECOGNITION_FLAG_NONE);
-                Log.i(TAG, "startRecognition status = " + status);
-                break;
+        if (availability == AlwaysOnHotwordDetector.STATE_INVALID) {
+            mHotwordDetector = createAlwaysOnHotwordDetector("Hello There", "en-US", mHotwordCallback);
         }
+        testHotwordAvailabilityStates();
     }
 
     @Override
@@ -84,4 +72,32 @@
         stopSelf(startId);
         return START_NOT_STICKY;
     }
+
+    private void testHotwordAvailabilityStates() {
+        int availability = mHotwordDetector.getAvailability();
+        Log.i(TAG, "Hotword availability = " + availability);
+        switch (availability) {
+            case AlwaysOnHotwordDetector.STATE_INVALID:
+                Log.i(TAG, "STATE_INVALID");
+                break;
+            case AlwaysOnHotwordDetector.STATE_HARDWARE_UNAVAILABLE:
+                Log.i(TAG, "STATE_HARDWARE_UNAVAILABLE");
+                break;
+            case AlwaysOnHotwordDetector.STATE_KEYPHRASE_UNSUPPORTED:
+                Log.i(TAG, "STATE_KEYPHRASE_UNSUPPORTED");
+                break;
+            case AlwaysOnHotwordDetector.STATE_KEYPHRASE_UNENROLLED:
+                Log.i(TAG, "STATE_KEYPHRASE_UNENROLLED");
+                Intent enroll = mHotwordDetector.getManageIntent(
+                        AlwaysOnHotwordDetector.MANAGE_ACTION_ENROLL);
+                Log.i(TAG, "Need to enroll with " + enroll);
+                break;
+            case AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED:
+                Log.i(TAG, "STATE_KEYPHRASE_ENROLLED");
+                int status = mHotwordDetector.startRecognition(
+                        AlwaysOnHotwordDetector.RECOGNITION_FLAG_NONE);
+                Log.i(TAG, "startRecognition status = " + status);
+                break;
+        }
+    }
 }