Always on hotword changes

Add model management API skeleton to VoiceInteractionManagerService
Add an "interactor" for all always-on APIs

- The VoiceInteractionService will get an interactor for the given
  keyphrase and locale.
- It can then check the availability and call methods to start and
  stop recognition on this interactor.

- Add a common class to deal with SoundTrigger APIs

- Cleanup the keyphrase representation:
  We now have separate representations for the keyphrase metadata and
  a keyphrase being used for recognition.
  This'll also help us to handle custom keyphrases in the
  future easily.
  This also ensures that for use within the framework,
  we rely on the ID of the KeyphraseInfo rather than comparing the
  text everytime.

Add a callback for the AlwaysOnHotwordDetector

This callback should be passed in by the VoiceInteractionService and is used to notify it
of recognition events.

Change-Id: I26252298773024f53a10cdd2af4404a4e6d74aae
diff --git a/api/current.txt b/api/current.txt
index d55cac6..558b7d1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26427,8 +26427,37 @@
 
 package android.service.voice {
 
+  public class AlwaysOnHotwordDetector {
+    method public int getAvailability();
+    method public android.content.Intent getManageIntent(int);
+    method public int getRecognitionStatus();
+    method public int startRecognition();
+    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
+    field public static final int RECOGNITION_ACTIVE = 2; // 0x2
+    field public static final int RECOGNITION_DISABLED_TEMPORARILY = -1; // 0xffffffff
+    field public static final int RECOGNITION_NOT_AVAILABLE = -3; // 0xfffffffd
+    field public static final int RECOGNITION_NOT_REQUESTED = -2; // 0xfffffffe
+    field public static final int RECOGNITION_REQUESTED = 1; // 0x1
+    field public static final int STATUS_ERROR = -2147483648; // 0x80000000
+    field public static final int STATUS_OK = 1; // 0x1
+  }
+
+  public static abstract interface AlwaysOnHotwordDetector.Callback {
+    method public abstract void onDetected();
+    method public abstract void onDetectionStarted();
+    method public abstract void onDetectionStopped();
+  }
+
   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 android.os.IBinder onBind(android.content.Intent);
     method public void startSession(android.os.Bundle);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService";
diff --git a/core/java/android/service/voice/DspInfo.java b/core/java/android/hardware/soundtrigger/DspInfo.java
similarity index 97%
rename from core/java/android/service/voice/DspInfo.java
rename to core/java/android/hardware/soundtrigger/DspInfo.java
index 0862309..517159d 100644
--- a/core/java/android/service/voice/DspInfo.java
+++ b/core/java/android/hardware/soundtrigger/DspInfo.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2014 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-package android.service.voice;
+package android.hardware.soundtrigger;
 
 import java.util.UUID;
 
 /**
  * Properties of the DSP hardware on the device.
+ *
  * @hide
  */
 public class DspInfo {
diff --git a/core/java/android/hardware/soundtrigger/Keyphrase.aidl b/core/java/android/hardware/soundtrigger/Keyphrase.aidl
new file mode 100644
index 0000000..d9853a7
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/Keyphrase.aidl
@@ -0,0 +1,4 @@
+package android.hardware.soundtrigger;
+
+// @hide
+parcelable Keyphrase;
\ No newline at end of file
diff --git a/core/java/android/hardware/soundtrigger/Keyphrase.java b/core/java/android/hardware/soundtrigger/Keyphrase.java
new file mode 100644
index 0000000..42fd350
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/Keyphrase.java
@@ -0,0 +1,101 @@
+/**
+ * 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 android.hardware.soundtrigger;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A Voice Keyphrase.
+ *
+ * @hide
+ */
+public class Keyphrase implements Parcelable {
+    /** A unique identifier for this keyphrase */
+    public final int id;
+    /** A hint text to display corresponding to this keyphrase, e.g. "Hello There". */
+    public final String hintText;
+    /** The locale of interest when using this Keyphrase. */
+    public String locale;
+
+    public static final Parcelable.Creator<Keyphrase> CREATOR
+            = new Parcelable.Creator<Keyphrase>() {
+        public Keyphrase createFromParcel(Parcel in) {
+            return Keyphrase.fromParcel(in);
+        }
+
+        public Keyphrase[] newArray(int size) {
+            return new Keyphrase[size];
+        }
+    };
+
+    private static Keyphrase fromParcel(Parcel in) {
+        return new Keyphrase(in.readInt(), in.readString(), in.readString());
+    }
+
+    public Keyphrase(int id, String hintText, String locale) {
+        this.id = id;
+        this.hintText = hintText;
+        this.locale = locale;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(id);
+        dest.writeString(hintText);
+        dest.writeString(locale);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((hintText == null) ? 0 : hintText.hashCode());
+        result = prime * result + id;
+        result = prime * result + ((locale == null) ? 0 : locale.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Keyphrase other = (Keyphrase) obj;
+        if (hintText == null) {
+            if (other.hintText != null)
+                return false;
+        } else if (!hintText.equals(other.hintText))
+            return false;
+        if (id != other.id)
+            return false;
+        if (locale == null) {
+            if (other.locale != null)
+                return false;
+        } else if (!locale.equals(other.locale))
+            return false;
+        return true;
+    }
+}
diff --git a/core/java/android/service/voice/KeyphraseEnrollmentInfo.java b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
similarity index 82%
rename from core/java/android/service/voice/KeyphraseEnrollmentInfo.java
rename to core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
index ebe41ce..2f5de6a 100644
--- a/core/java/android/service/voice/KeyphraseEnrollmentInfo.java
+++ b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2014 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.service.voice;
+package android.hardware.soundtrigger;
 
 import android.Manifest;
 import android.content.Intent;
@@ -24,6 +24,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.service.voice.AlwaysOnHotwordDetector;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.Xml;
@@ -34,7 +35,11 @@
 import java.io.IOException;
 import java.util.List;
 
-/** @hide */
+/**
+ * Enrollment information about the different available keyphrases.
+ *
+ * @hide
+ */
 public class KeyphraseEnrollmentInfo {
     private static final String TAG = "KeyphraseEnrollmentInfo";
     /**
@@ -53,10 +58,14 @@
     public static final String ACTION_MANAGE_VOICE_KEYPHRASES =
             "com.android.intent.action.MANAGE_VOICE_KEYPHRASES";
     /**
-     * Intent extra: The intent extra for un-enrolling a user for a particular keyphrase.
+     * Intent extra: The intent extra for the specific manage action that needs to be performed.
+     * Possible values are {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
+     * {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
+     * or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}.
      */
-    public static final String EXTRA_VOICE_KEYPHRASE_UNENROLL =
-            "com.android.intent.extra.VOICE_KEYPHRASE_UNENROLL";
+    public static final String EXTRA_VOICE_KEYPHRASE_ACTION =
+            "com.android.intent.extra.VOICE_KEYPHRASE_ACTION";
+
     /**
      * Intent extra: The hint text to be shown on the voice keyphrase management UI.
      */
@@ -68,7 +77,7 @@
     public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
             "com.android.intent.extra.VOICE_KEYPHRASE_LOCALE";
 
-    private KeyphraseInfo[] mKeyphrases;
+    private KeyphraseMetadata[] mKeyphrases;
     private String mEnrollmentPackage;
     private String mParseError;
 
@@ -156,8 +165,8 @@
                         && !searchKeyphraseSupportedLocales.isEmpty()) {
                     supportedLocales = searchKeyphraseSupportedLocales.split(",");
                 }
-                mKeyphrases = new KeyphraseInfo[1];
-                mKeyphrases[0] = new KeyphraseInfo(
+                mKeyphrases = new KeyphraseMetadata[1];
+                mKeyphrases[0] = new KeyphraseMetadata(
                         searchKeyphraseId, searchKeyphrase, supportedLocales);
             } else {
                 mParseError = "searchKeyphraseId not specified in meta-data";
@@ -188,7 +197,7 @@
      * @return An array of available keyphrases that can be enrolled on the system.
      *         It may be null if no keyphrases can be enrolled.
      */
-    public KeyphraseInfo[] getKeyphrases() {
+    public KeyphraseMetadata[] listKeyphraseMetadata() {
         return mKeyphrases;
     }
 
@@ -196,51 +205,56 @@
      * Returns an intent to launch an activity that manages the given keyphrase
      * for the locale.
      *
-     * @param enroll Indicates if the intent should enroll the user or un-enroll them.
+     * @param action The enrollment related action that this intent is supposed to perform.
+     *        This can be one of {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
+     *        {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
+     *        or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}
      * @param keyphrase The keyphrase that the user needs to be enrolled to.
      * @param locale The locale for which the enrollment needs to be performed.
+     *        This is a Java locale, for example "en_US".
      * @return An {@link Intent} to manage the keyphrase. This can be null if managing the
      *         given keyphrase/locale combination isn't possible.
      */
-    public Intent getManageKeyphraseIntent(boolean enroll, String keyphrase, String locale) {
+    public Intent getManageKeyphraseIntent(int action, String keyphrase, String locale) {
         if (mEnrollmentPackage == null || mEnrollmentPackage.isEmpty()) {
             Slog.w(TAG, "No enrollment application exists");
             return null;
         }
 
-        if (isKeyphraseEnrollmentSupported(keyphrase, locale)) {
+        if (getKeyphraseMetadata(keyphrase, locale) != null) {
             Intent intent = new Intent(ACTION_MANAGE_VOICE_KEYPHRASES)
                     .setPackage(mEnrollmentPackage)
                     .putExtra(EXTRA_VOICE_KEYPHRASE_HINT_TEXT, keyphrase)
-                    .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale);
-            if (!enroll) intent.putExtra(EXTRA_VOICE_KEYPHRASE_UNENROLL, true);
+                    .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale)
+                    .putExtra(EXTRA_VOICE_KEYPHRASE_ACTION, action);
             return intent;
         }
         return null;
     }
 
     /**
-     * Indicates if enrollment is supported for the given keyphrase & locale.
+     * Gets the {@link KeyphraseMetadata} for the given keyphrase and locale, null if any metadata
+     * isn't available for the given combination.
      *
      * @param keyphrase The keyphrase that the user needs to be enrolled to.
      * @param locale The locale for which the enrollment needs to be performed.
+     *        This is a Java locale, for example "en_US".
      * @return true, if an enrollment client supports the given keyphrase and the given locale.
      */
-    public boolean isKeyphraseEnrollmentSupported(String keyphrase, String locale) {
+    public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, String locale) {
         if (mKeyphrases == null || mKeyphrases.length == 0) {
             Slog.w(TAG, "Enrollment application doesn't support keyphrases");
-            return false;
+            return null;
         }
-        for (KeyphraseInfo keyphraseInfo : mKeyphrases) {
+        for (KeyphraseMetadata keyphraseMetadata : mKeyphrases) {
             // Check if the given keyphrase is supported in the locale provided by
             // the enrollment application.
-            String supportedKeyphrase = keyphraseInfo.keyphrase;
-            if (supportedKeyphrase.equalsIgnoreCase(keyphrase)
-                    && keyphraseInfo.supportedLocales.contains(locale)) {
-                return true;
+            if (keyphraseMetadata.supportsPhrase(keyphrase)
+                    && keyphraseMetadata.supportsLocale(locale)) {
+                return keyphraseMetadata;
             }
         }
-        Slog.w(TAG, "Enrollment application doesn't support the given keyphrase");
-        return false;
+        Slog.w(TAG, "Enrollment application doesn't support the given keyphrase/locale");
+        return null;
     }
 }
diff --git a/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
new file mode 100644
index 0000000..03a4939
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
@@ -0,0 +1,60 @@
+/**
+ * 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 android.hardware.soundtrigger;
+
+import android.util.ArraySet;
+
+/**
+ * A Voice Keyphrase metadata read from the enrollment application.
+ *
+ * @hide
+ */
+public class KeyphraseMetadata {
+    public final int id;
+    public final String keyphrase;
+    public final ArraySet<String> supportedLocales;
+
+    public KeyphraseMetadata(int id, String keyphrase, String[] supportedLocales) {
+        this.id = id;
+        this.keyphrase = keyphrase;
+        this.supportedLocales = new ArraySet<String>(supportedLocales.length);
+        for (String locale : supportedLocales) {
+            this.supportedLocales.add(locale);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "id=" + id + ", keyphrase=" + keyphrase + ", supported-locales=" + supportedLocales;
+    }
+
+    /**
+     * @return Indicates if we support the given phrase.
+     */
+    public boolean supportsPhrase(String phrase) {
+        // TODO(sansid): Come up with a scheme for custom keyphrases that should always match.
+        return keyphrase.equalsIgnoreCase(phrase);
+    }
+
+    /**
+     * @return Indicates if we support the given locale.
+     */
+    public boolean supportsLocale(String locale) {
+        // TODO(sansid): Come up with a scheme for keyphrases that are available in all locales.
+        return supportedLocales.contains(locale);
+    }
+}
diff --git a/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl
new file mode 100644
index 0000000..39b33cc
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl
@@ -0,0 +1,4 @@
+package android.hardware.soundtrigger;
+
+// @hide
+parcelable KeyphraseSoundModel;
\ No newline at end of file
diff --git a/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java
new file mode 100644
index 0000000..4ddba6a
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java
@@ -0,0 +1,68 @@
+package android.hardware.soundtrigger;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * A KeyphraseSoundModel is a sound model capable of detecting voice keyphrases.
+ * It contains data needed by the hardware to detect a given number of key phrases
+ * and the list of corresponding {@link Keyphrase}s.
+ *
+ * @hide
+ */
+public class KeyphraseSoundModel implements Parcelable {
+
+    /** Key phrases in this sound model */
+    public final Keyphrase[] keyphrases;
+    public final byte[] data;
+    public final UUID uuid;
+
+    public static final Parcelable.Creator<KeyphraseSoundModel> CREATOR
+            = new Parcelable.Creator<KeyphraseSoundModel>() {
+        public KeyphraseSoundModel createFromParcel(Parcel in) {
+            return KeyphraseSoundModel.fromParcel(in);
+        }
+
+        public KeyphraseSoundModel[] newArray(int size) {
+            return new KeyphraseSoundModel[size];
+        }
+    };
+
+    public KeyphraseSoundModel(UUID uuid, byte[] data,Keyphrase[] keyPhrases) {
+        this.uuid = uuid;
+        this.data = data;
+        this.keyphrases = keyPhrases;
+    }
+
+    private static KeyphraseSoundModel fromParcel(Parcel in) {
+        UUID uuid = UUID.fromString(in.readString());
+        int dataLength = in.readInt();
+        byte[] data = null;
+        if (dataLength > 0) {
+            data = new byte[in.readInt()];
+            in.readByteArray(data);
+        }
+        Keyphrase[] keyphrases =
+                (Keyphrase[]) in.readParcelableArray(Keyphrase.class.getClassLoader());
+        return new KeyphraseSoundModel(uuid, data, keyphrases);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(uuid.toString());
+        if (data != null) {
+            dest.writeInt(data.length);
+            dest.writeByteArray(data);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeParcelableArray(keyphrases, 0);
+    }
+}
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 7a4e5a5..1f48a92 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2014 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java b/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java
new file mode 100644
index 0000000..0be068d
--- /dev/null
+++ b/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java
@@ -0,0 +1,217 @@
+/**
+ * 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 android.hardware.soundtrigger;
+
+import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
+import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+
+/**
+ * Helper for {@link SoundTrigger} APIs.
+ * Currently this just acts as an abstraction over all SoundTrigger API calls.
+ *
+ * @hide
+ */
+public class SoundTriggerHelper implements SoundTrigger.StatusListener {
+    static final String TAG = "SoundTriggerHelper";
+    // TODO: Remove this.
+    static final int TEMP_KEYPHRASE_ID = 1;
+
+    /**
+     * Return codes for {@link #startRecognition(Keyphrase)}, {@link #stopRecognition(Keyphrase)}
+     * Note: Keep in sync with AlwaysOnKeyphraseInteractor.java
+     */
+    public static final int STATUS_ERROR = Integer.MIN_VALUE;
+    public static final int STATUS_OK = 1;
+
+    /**
+     * States for {@link Listener#onListeningStateChanged(int, int)}.
+     */
+    public static final int STATE_STOPPED = 0;
+    public static final int STATE_STARTED = 1;
+
+    private static final int INVALID_SOUND_MODEL_HANDLE = -1;
+
+    /** The {@link DspInfo} for the system, or null if none exists. */
+    public final DspInfo dspInfo;
+
+    /** The properties for the DSP module */
+    private final ModuleProperties mModuleProperties;
+    private final SoundTriggerModule mModule;
+
+    private final SparseArray<Listener> mListeners;
+
+    private int mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
+
+    /**
+     * The callback for sound trigger events.
+     */
+    public interface Listener {
+        /** Called when the given keyphrase is spoken. */
+        void onKeyphraseSpoken();
+
+        /**
+         * Called when the listening state for the given keyphrase changes.
+         * @param state Indicates the current state.
+         */
+        void onListeningStateChanged(int state);
+    }
+
+    public SoundTriggerHelper() {
+        ArrayList <ModuleProperties> modules = new ArrayList<>();
+        int status = SoundTrigger.listModules(modules);
+        mListeners = new SparseArray<>(1);
+        if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
+            // TODO: Figure out how to handle errors in listing the modules here.
+            dspInfo = null;
+            mModuleProperties = null;
+            mModule = null;
+        } else {
+            // TODO: Figure out how to determine which module corresponds to the DSP hardware.
+            mModuleProperties = modules.get(0);
+            dspInfo = new DspInfo(mModuleProperties.uuid, mModuleProperties.implementor,
+                    mModuleProperties.description, mModuleProperties.version,
+                    mModuleProperties.powerConsumptionMw);
+            mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
+        }
+    }
+
+    /**
+     * @return True, if the given {@link Keyphrase} is supported on DSP.
+     */
+    public boolean isKeyphraseSupported(Keyphrase keyphrase) {
+        // TODO: We also need to look into a SoundTrigger API that let's us
+        // query this. For now just return true.
+        return true;
+    }
+
+    /**
+     * @return True, if the given {@link Keyphrase} has been enrolled.
+     */
+    public boolean isKeyphraseEnrolled(Keyphrase keyphrase) {
+        // TODO: Query VoiceInteractionManagerService
+        // to list registered sound models.
+        return false;
+    }
+
+    /**
+     * @return True, if a recognition for the given {@link Keyphrase} is active.
+     */
+    public boolean isKeyphraseActive(Keyphrase keyphrase) {
+        // TODO: Check if the recognition for the keyphrase is currently active.
+        return false;
+    }
+
+    /**
+     * Starts recognition for the given {@link Keyphrase}.
+     *
+     * @param keyphraseId The identifier of the keyphrase for which
+     *        the recognition is to be started.
+     * @param listener The listener for the recognition events related to the given keyphrase.
+     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+     */
+    public int startRecognition(int keyphraseId, Listener listener) {
+        if (dspInfo == null || mModule == null) {
+            Slog.w(TAG, "Attempting startRecognition without the capability");
+            return STATUS_ERROR;
+        }
+
+        if (mListeners.get(keyphraseId) != listener) {
+            if (mCurrentSoundModelHandle != INVALID_SOUND_MODEL_HANDLE) {
+                Slog.w(TAG, "Canceling previous recognition");
+                // TODO: Inspect the return codes here.
+                mModule.unloadSoundModel(mCurrentSoundModelHandle);
+            }
+            mListeners.get(keyphraseId).onListeningStateChanged(STATE_STOPPED);
+        }
+
+        // Register the new listener. This replaces the old one.
+        // There can only be a maximum of one active listener for a keyphrase
+        // at any given time.
+        mListeners.put(keyphraseId, listener);
+        // TODO: Get the sound model for the given keyphrase here.
+        // mModule.loadSoundModel(model, soundModelHandle);
+        // mModule.startRecognition(soundModelHandle, data);
+        // mCurrentSoundModelHandle = soundModelHandle;
+        return STATUS_ERROR;
+    }
+
+    /**
+     * Stops recognition for the given {@link Keyphrase} if a recognition is currently active.
+     *
+     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+     */
+    public int stopRecognition(int id, Listener listener) {
+        if (dspInfo == null || mModule == null) {
+            Slog.w(TAG, "Attempting stopRecognition without the capability");
+            return STATUS_ERROR;
+        }
+
+        if (mListeners.get(id) != listener) {
+            Slog.w(TAG, "Attempting stopRecognition for another recognition");
+            return STATUS_ERROR;
+        } else {
+            // Stop recognition if it's the current one, ignore otherwise.
+            // TODO: Inspect the return codes here.
+            mModule.stopRecognition(mCurrentSoundModelHandle);
+            mModule.unloadSoundModel(mCurrentSoundModelHandle);
+            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
+            return STATUS_OK;
+        }
+    }
+
+    //---- SoundTrigger.StatusListener methods
+    @Override
+    public void onRecognition(RecognitionEvent event) {
+        // Check which keyphrase triggered, and fire the appropriate event.
+        // TODO: Get the keyphrase out of the event and fire events on it.
+        // For now, as a nasty workaround, we fire all events to the listener for
+        // keyphrase with TEMP_KEYPHRASE_ID.
+
+        switch (event.status) {
+            case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
+                // TODO: The keyphrase should come from the recognition event
+                // as it may be for a different keyphrase than the current one.
+                if (mListeners.get(TEMP_KEYPHRASE_ID) != null) {
+                    mListeners.get(TEMP_KEYPHRASE_ID).onKeyphraseSpoken();
+                }
+                break;
+            case SoundTrigger.RECOGNITION_STATUS_ABORT:
+                // TODO: The keyphrase should come from the recognition event
+                // as it may be for a different keyphrase than the current one.
+                if (mListeners.get(TEMP_KEYPHRASE_ID) != null) {
+                    mListeners.get(TEMP_KEYPHRASE_ID).onListeningStateChanged(STATE_STOPPED);
+                }
+                break;
+            case SoundTrigger.RECOGNITION_STATUS_FAILURE:
+                // TODO: The keyphrase should come from the recognition event
+                // as it may be for a different keyphrase than the current one.
+                if (mListeners.get(TEMP_KEYPHRASE_ID) != null) {
+                    mListeners.get(TEMP_KEYPHRASE_ID).onListeningStateChanged(STATE_STOPPED);
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void onServiceDied() {
+        // TODO: Figure out how to restart the recognition here.
+    }
+}
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
new file mode 100644
index 0000000..67ce31e
--- /dev/null
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -0,0 +1,270 @@
+/**
+ * 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 android.service.voice;
+
+import android.content.Intent;
+import android.hardware.soundtrigger.Keyphrase;
+import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
+import android.hardware.soundtrigger.KeyphraseMetadata;
+import android.hardware.soundtrigger.SoundTriggerHelper;
+import android.util.Slog;
+
+/**
+ * A class that lets a VoiceInteractionService implementation interact with
+ * always-on keyphrase detection APIs.
+ */
+public class AlwaysOnHotwordDetector {
+    //---- States of Keyphrase availability ----//
+    /**
+     * Indicates that the given keyphrase is not available on the system because of the
+     * hardware configuration.
+     */
+    public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2;
+    /**
+     * Indicates that the given keyphrase is not supported.
+     */
+    public static final int KEYPHRASE_UNSUPPORTED = -1;
+    /**
+     * Indicates that the given keyphrase is not enrolled.
+     */
+    public static final int KEYPHRASE_UNENROLLED = 1;
+    /**
+     * Indicates that the given keyphrase is currently enrolled but not being actively listened for.
+     */
+    public static final int KEYPHRASE_ENROLLED = 2;
+
+    // Keyphrase management actions ----//
+    /** Indicates that we need to enroll. */
+    public static final int MANAGE_ACTION_ENROLL = 0;
+    /** Indicates that we need to re-enroll. */
+    public static final int MANAGE_ACTION_RE_ENROLL = 1;
+    /** Indicates that we need to un-enroll. */
+    public static final int MANAGE_ACTION_UN_ENROLL = 2;
+
+    /**
+     * Return codes for {@link #startRecognition()}, {@link #stopRecognition()}
+     */
+    public static final int STATUS_ERROR = Integer.MIN_VALUE;
+    public static final int STATUS_OK = 1;
+
+    //---- Keyphrase recognition status ----//
+    // TODO: Figure out if they are exclusive or should be flags instead?
+    public static final int RECOGNITION_NOT_AVAILABLE = -3;
+    public static final int RECOGNITION_NOT_REQUESTED = -2;
+    public static final int RECOGNITION_DISABLED_TEMPORARILY = -1;
+    public static final int RECOGNITION_REQUESTED = 1;
+    public static final int RECOGNITION_ACTIVE = 2;
+    static final String TAG = "AlwaysOnHotwordDetector";
+
+    private final String mText;
+    private final String mLocale;
+    private final Keyphrase mKeyphrase;
+    private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
+    private final SoundTriggerHelper mSoundTriggerHelper;
+    private final SoundTriggerHelper.Listener mListener;
+    private final int mAvailability;
+
+    private int mRecognitionState;
+
+    /**
+     * Callbacks for always-on hotword detection.
+     */
+    public interface Callback {
+        /**
+         * Called when the keyphrase is spoken.
+         * TODO: Add more data to the callback.
+         */
+        void onDetected();
+        /**
+         * Called when the detection for the associated keyphrase starts.
+         */
+        void onDetectionStarted();
+        /**
+         * Called when the detection for the associated keyphrase stops.
+         */
+        void onDetectionStopped();
+    }
+
+    /**
+     * @param text The keyphrase text to get the detector for.
+     * @param locale The java locale for the detector.
+     * @param callback A non-null Callback for receiving the recognition events.
+     *
+     * @hide
+     */
+    public AlwaysOnHotwordDetector(String text, String locale, Callback callback,
+            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
+            SoundTriggerHelper soundTriggerHelper) {
+        mText = text;
+        mLocale = locale;
+        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
+        KeyphraseMetadata keyphraseMetadata =
+                mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
+        if (keyphraseMetadata != null) {
+            mKeyphrase = new Keyphrase(keyphraseMetadata.id, text, locale);
+        } else {
+            mKeyphrase = null;
+        }
+        mListener = new SoundTriggerListener(callback);
+        mSoundTriggerHelper = soundTriggerHelper;
+        mAvailability = getAvailabilityInternal();
+    }
+
+    /**
+     * 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.
+     *
+     * @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}.
+     */
+    public int getAvailability() {
+        return mAvailability;
+    }
+
+    /**
+     * Gets the status of the recognition.
+     * @return One of {@link #RECOGNITION_NOT_AVAILABLE}, {@link #RECOGNITION_NOT_REQUESTED},
+     *         {@link #RECOGNITION_DISABLED_TEMPORARILY} or {@link #RECOGNITION_ACTIVE}.
+     * @throws UnsupportedOperationException if the recognition isn't supported.
+     *         Callers should check the availability by calling {@link #getAvailability()}
+     *         before calling this method to avoid this exception.
+     */
+    public int getRecognitionStatus() {
+        if (mAvailability != KEYPHRASE_ENROLLED) {
+            throw new UnsupportedOperationException(
+                    "Recognition for the given keyphrase is not supported");
+        }
+
+        return mRecognitionState;
+    }
+
+    /**
+     * Starts recognition for the associated keyphrase.
+     *
+     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+     * @throws UnsupportedOperationException if the recognition isn't supported.
+     *         Callers should check the availability by calling {@link #getAvailability()}
+     *         before calling this method to avoid this exception.
+     */
+    public int startRecognition() {
+        if (mAvailability != KEYPHRASE_ENROLLED) {
+            throw new UnsupportedOperationException(
+                    "Recognition for the given keyphrase is not supported");
+        }
+
+        mRecognitionState = RECOGNITION_REQUESTED;
+        int code = mSoundTriggerHelper.startRecognition(mKeyphrase.id, mListener);
+        if (code != SoundTriggerHelper.STATUS_OK) {
+            Slog.w(TAG, "startRecognition() failed with error code " + code);
+            return STATUS_ERROR;
+        } else {
+            return STATUS_OK;
+        }
+    }
+
+    /**
+     * Stops recognition for the associated keyphrase.
+     *
+     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+     * @throws UnsupportedOperationException if the recognition isn't supported.
+     *         Callers should check the availability by calling {@link #getAvailability()}
+     *         before calling this method to avoid this exception.
+     */
+    public int stopRecognition() {
+        if (mAvailability != KEYPHRASE_ENROLLED) {
+            throw new UnsupportedOperationException(
+                    "Recognition for the given keyphrase is not supported");
+        }
+
+        mRecognitionState = RECOGNITION_NOT_REQUESTED;
+        int code = mSoundTriggerHelper.stopRecognition(mKeyphrase.id, mListener);
+        if (code != SoundTriggerHelper.STATUS_OK) {
+            Slog.w(TAG, "stopRecognition() failed with error code " + code);
+            return STATUS_ERROR;
+        } else {
+            return STATUS_OK;
+        }
+    }
+
+    /**
+     * Gets an intent to manage the associated keyphrase.
+     *
+     * @param action The manage action that needs to be performed.
+     *        One of {@link #MANAGE_ACTION_ENROLL}, {@link #MANAGE_ACTION_RE_ENROLL} or
+     *        {@link #MANAGE_ACTION_UN_ENROLL}.
+     * @return An {@link Intent} to manage the given keyphrase.
+     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
+     *         Callers should check the availability by calling {@link #getAvailability()}
+     *         before calling this method to avoid this exception.
+     */
+    public Intent getManageIntent(int action) {
+        if (mAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE
+                || mAvailability == KEYPHRASE_UNSUPPORTED) {
+            throw new UnsupportedOperationException(
+                    "Managing the given keyphrase is not supported");
+        }
+        if (action != MANAGE_ACTION_ENROLL
+                && action != MANAGE_ACTION_RE_ENROLL
+                && action != MANAGE_ACTION_UN_ENROLL) {
+            throw new IllegalArgumentException("Invalid action specified " + action);
+        }
+
+        return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
+    }
+
+    private int getAvailabilityInternal() {
+        if (mSoundTriggerHelper.dspInfo == null) {
+            return KEYPHRASE_HARDWARE_UNAVAILABLE;
+        }
+        if (mKeyphrase == null || !mSoundTriggerHelper.isKeyphraseSupported(mKeyphrase)) {
+            return KEYPHRASE_UNSUPPORTED;
+        }
+        if (!mSoundTriggerHelper.isKeyphraseEnrolled(mKeyphrase)) {
+            return KEYPHRASE_UNENROLLED;
+        }
+        return KEYPHRASE_ENROLLED;
+    }
+
+    /** @hide */
+    static final class SoundTriggerListener implements SoundTriggerHelper.Listener {
+        private final Callback mCallback;
+
+        public SoundTriggerListener(Callback callback) {
+            this.mCallback = callback;
+        }
+
+        @Override
+        public void onKeyphraseSpoken() {
+            Slog.i(TAG, "onKeyphraseSpoken");
+            mCallback.onDetected();
+        }
+
+        @Override
+        public void onListeningStateChanged(int state) {
+            Slog.i(TAG, "onListeningStateChanged: state=" + state);
+            if (state == SoundTriggerHelper.STATE_STARTED) {
+                mCallback.onDetectionStarted();
+            } else if (state == SoundTriggerHelper.STATE_STOPPED) {
+                mCallback.onDetectionStopped();
+            }
+        }
+    }
+}
diff --git a/core/java/android/service/voice/KeyphraseInfo.java b/core/java/android/service/voice/KeyphraseInfo.java
deleted file mode 100644
index d266e1a..0000000
--- a/core/java/android/service/voice/KeyphraseInfo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package android.service.voice;
-
-import android.util.ArraySet;
-
-/**
- * A Voice Keyphrase.
- * @hide
- */
-public class KeyphraseInfo {
-    public final int id;
-    public final String keyphrase;
-    public final ArraySet<String> supportedLocales;
-
-    public KeyphraseInfo(int id, String keyphrase, String[] supportedLocales) {
-        this.id = id;
-        this.keyphrase = keyphrase;
-        this.supportedLocales = new ArraySet<String>(supportedLocales.length);
-        for (String locale : supportedLocales) {
-            this.supportedLocales.add(locale);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "id=" + id + ", keyphrase=" + keyphrase + ", supported-locales=" + supportedLocales;
-    }
-}
diff --git a/core/java/android/service/voice/SoundTriggerManager.java b/core/java/android/service/voice/SoundTriggerManager.java
deleted file mode 100644
index 2d049b9..0000000
--- a/core/java/android/service/voice/SoundTriggerManager.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 android.service.voice;
-
-import android.hardware.soundtrigger.SoundTrigger;
-import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
-
-import java.util.ArrayList;
-
-/**
- * Manager for {@link SoundTrigger} APIs.
- * Currently this just acts as an abstraction over all SoundTrigger API calls.
- * @hide
- */
-public class SoundTriggerManager {
-    /** The {@link DspInfo} for the system, or null if none exists. */
-    public DspInfo dspInfo;
-
-    public SoundTriggerManager() {
-        ArrayList <ModuleProperties> modules = new ArrayList<>();
-        int status = SoundTrigger.listModules(modules);
-        if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
-            // TODO(sansid, elaurent): Figure out how to handle errors in listing the modules here.
-            dspInfo = null;
-        } else {
-            // TODO(sansid, elaurent): Figure out how to determine which module corresponds to the
-            // DSP hardware.
-            ModuleProperties properties = modules.get(0);
-            dspInfo = new DspInfo(properties.uuid, properties.implementor, properties.description,
-                    properties.version, properties.powerConsumptionMw);
-        }
-    }
-
-    /**
-     * @return True, if the keyphrase is supported on DSP for the given locale.
-     */
-    public boolean isKeyphraseSupported(String keyphrase, String locale) {
-        // TODO(sansid): We also need to look into a SoundTrigger API that let's us
-        // query this. For now just return supported if there's a DSP available.
-        return dspInfo != null;
-    }
-
-    /**
-     * @return True, if the keyphrase is has been enrolled for the given locale.
-     */
-    public boolean isKeyphraseEnrolled(String keyphrase, String locale) {
-        // TODO(sansid, elaurent): Query SoundTrigger to list currently loaded sound models.
-        // They have been enrolled.
-        return false;
-    }
-
-    /**
-     * @return True, if a recognition for the keyphrase is active for the given locale.
-     */
-    public boolean isKeyphraseActive(String keyphrase, String locale) {
-        // TODO(sansid, elaurent): Check if the recognition for the keyphrase is currently active.
-        return false;
-    }
-}
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index e0329f8..cf8d502 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -20,6 +20,8 @@
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
+import android.hardware.soundtrigger.SoundTriggerHelper;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -53,16 +55,6 @@
     public static final String SERVICE_INTERFACE =
             "android.service.voice.VoiceInteractionService";
 
-    // TODO(sansid): Unhide these.
-    /** @hide */
-    public static final int KEYPHRASE_UNAVAILABLE = 0;
-    /** @hide */
-    public static final int KEYPHRASE_UNENROLLED = 1;
-    /** @hide */
-    public static final int KEYPHRASE_ENROLLED = 2;
-    /** @hide */
-    public static final int KEYPHRASE_ACTIVE = 3;
-
     /**
      * Name under which a VoiceInteractionService component publishes information about itself.
      * This meta-data should reference an XML resource containing a
@@ -76,8 +68,8 @@
 
     IVoiceInteractionManagerService mSystemService;
 
-    private SoundTriggerManager mSoundTriggerManager;
     private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
+    private SoundTriggerHelper mSoundTriggerHelper;
 
     public void startSession(Bundle args) {
         try {
@@ -92,7 +84,7 @@
         mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
                 ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
         mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager());
-        mSoundTriggerManager = new SoundTriggerManager();
+        mSoundTriggerHelper = new SoundTriggerHelper();
     }
 
     @Override
@@ -104,34 +96,18 @@
     }
 
     /**
-     * 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 return code is one of {@link #KEYPHRASE_UNAVAILABLE}, {@link #KEYPHRASE_UNENROLLED}
-     * {@link #KEYPHRASE_ENROLLED} or {@link #KEYPHRASE_ACTIVE}.
-     *
-     * @param keyphrase The keyphrase whose availability is being checked.
-     * @param locale The locale for which the availability is being checked.
-     * @return Indicates if always-on hotword detection is available for the given keyphrase.
-     * TODO(sansid): Unhide this.
-     * @hide
+     * @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 int getAlwaysOnKeyphraseAvailability(String keyphrase, String locale) {
-        // The available keyphrases is a combination of DSP availability and
-        // the keyphrases that have an enrollment application for them.
-        if (!mSoundTriggerManager.isKeyphraseSupported(keyphrase, locale)
-                || !mKeyphraseEnrollmentInfo.isKeyphraseEnrollmentSupported(keyphrase, locale)) {
-            return KEYPHRASE_UNAVAILABLE;
-        }
-        if (!mSoundTriggerManager.isKeyphraseEnrolled(keyphrase, locale)) {
-            return KEYPHRASE_UNENROLLED;
-        }
-        if (!mSoundTriggerManager.isKeyphraseActive(keyphrase, locale)) {
-            return KEYPHRASE_ENROLLED;
-        } else {
-            return KEYPHRASE_ACTIVE;
-        }
+    public final AlwaysOnHotwordDetector getAlwaysOnHotwordDetector(
+            String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) {
+        // 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, mSoundTriggerHelper);
     }
 
     /**
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 98e35dd..c78f770 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -20,6 +20,7 @@
 import android.os.Bundle;
 
 import com.android.internal.app.IVoiceInteractor;
+import android.hardware.soundtrigger.KeyphraseSoundModel;
 import android.service.voice.IVoiceInteractionService;
 import android.service.voice.IVoiceInteractionSession;
 
@@ -29,4 +30,16 @@
             IVoiceInteractor interactor);
     int startVoiceActivity(IBinder token, in Intent intent, String resolvedType);
     void finish(IBinder token);
+
+    /**
+     * Lists the registered Sound models for keyphrase detection.
+     * May be null if no matching sound models exist.
+     *
+     * @param service The current voice interaction service.
+     */
+    List<KeyphraseSoundModel> listRegisteredKeyphraseSoundModels(in IVoiceInteractionService service);
+    /**
+     * Updates the given keyphrase sound model. Adds the model if it doesn't exist currently.
+     */
+    int updateKeyphraseSoundModel(in KeyphraseSoundModel model);
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 16afc8f..85042f7 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -24,6 +24,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
+import android.hardware.soundtrigger.KeyphraseSoundModel;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -35,6 +36,7 @@
 import android.service.voice.IVoiceInteractionService;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.Slog;
+
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.content.PackageMonitor;
@@ -44,6 +46,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.List;
 
 
 /**
@@ -53,6 +56,10 @@
 
     static final String TAG = "VoiceInteractionManagerService";
 
+    // TODO: Add descriptive error codes.
+    public static final int STATUS_ERROR = -1;
+    public static final int STATUS_OK = 1;
+
     final Context mContext;
     final ContentResolver mResolver;
 
@@ -231,6 +238,59 @@
         }
 
         @Override
+        public List<KeyphraseSoundModel> listRegisteredKeyphraseSoundModels(
+                IVoiceInteractionService service) {
+            // Allow the call if this is the current voice interaction service
+            // or the caller holds the MANAGE_VOICE_KEYPHRASES permission.
+            synchronized (this) {
+                boolean permissionGranted =
+                        mContext.checkCallingPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
+                        == PackageManager.PERMISSION_GRANTED;
+                boolean currentVoiceInteractionService = service != null
+                        && mImpl != null
+                        && mImpl.mService != null
+                        && service.asBinder() == mImpl.mService.asBinder();
+
+                if (!permissionGranted && !currentVoiceInteractionService) {
+                    if (!currentVoiceInteractionService) {
+                        throw new SecurityException(
+                                "Caller is not the current voice interaction service");
+                    }
+                    if (!permissionGranted) {
+                        throw new SecurityException("Caller does not hold the permission "
+                                + Manifest.permission.MANAGE_VOICE_KEYPHRASES);
+                    }
+                }
+
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    // TODO: Add the implementation here.
+                    return null;
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
+        @Override
+        public int updateKeyphraseSoundModel(KeyphraseSoundModel model) {
+            synchronized (this) {
+                if (mContext.checkCallingPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller does not hold the permission "
+                            + Manifest.permission.MANAGE_VOICE_KEYPHRASES);
+                }
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    // TODO: Add the implementation here.
+                    return VoiceInteractionManagerService.STATUS_ERROR;
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                     != PackageManager.PERMISSION_GRANTED) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 8d96fb3..1f40c26 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -26,7 +26,6 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -42,6 +41,7 @@
 import android.util.Slog;
 import android.view.IWindowManager;
 import android.view.WindowManager;
+
 import com.android.internal.app.IVoiceInteractor;
 
 import java.io.FileDescriptor;
diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
index 00c2c64..db43be3 100644
--- a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
+++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
@@ -32,7 +32,7 @@
         Log.i(TAG, "Creating " + this);
         Log.i(TAG, "Keyphrase enrollment error? " + getKeyphraseEnrollmentInfo().getParseError());
         Log.i(TAG, "Keyphrase enrollment meta-data: "
-                + Arrays.toString(getKeyphraseEnrollmentInfo().getKeyphrases()));
+                + Arrays.toString(getKeyphraseEnrollmentInfo().listKeyphraseMetadata()));
     }
 
     @Override