Merge "Always derive native library paths at runtime."
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