Merge "Fix the Locale story in the hotword API" into lmp-dev
diff --git a/api/current.txt b/api/current.txt
index fc39cba..a592b0e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -27464,7 +27464,7 @@
 
   public class VoiceInteractionService extends android.app.Service {
     ctor public VoiceInteractionService();
-    method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.lang.String, android.service.voice.AlwaysOnHotwordDetector.Callback);
+    method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
     method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
     method public android.os.IBinder onBind(android.content.Intent);
     method public void onReady();
diff --git a/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
index 0dbde6b..2d5a271 100644
--- a/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
+++ b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java
@@ -25,6 +25,8 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.service.voice.AlwaysOnHotwordDetector;
+import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.Xml;
@@ -35,6 +37,7 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Enrollment information about the different available keyphrases.
@@ -151,33 +154,8 @@
 
             TypedArray array = res.obtainAttributes(attrs,
                     com.android.internal.R.styleable.VoiceEnrollmentApplication);
-            int searchKeyphraseId = array.getInt(
-                    com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphraseId,
-                    -1);
-            if (searchKeyphraseId != -1) {
-                String searchKeyphrase = array.getString(com.android.internal.R.styleable
-                        .VoiceEnrollmentApplication_searchKeyphrase);
-                if (searchKeyphrase == null) {
-                    searchKeyphrase = "";
-                }
-                String searchKeyphraseSupportedLocales =
-                        array.getString(com.android.internal.R.styleable
-                                .VoiceEnrollmentApplication_searchKeyphraseSupportedLocales);
-                String[] supportedLocales = new String[0];
-                // Get all the supported locales from the comma-delimted string.
-                if (searchKeyphraseSupportedLocales != null
-                        && !searchKeyphraseSupportedLocales.isEmpty()) {
-                    supportedLocales = searchKeyphraseSupportedLocales.split(",");
-                }
-                int recognitionModes = array.getInt(com.android.internal.R.styleable
-                        .VoiceEnrollmentApplication_searchKeyphraseRecognitionFlags, 0);
-                mKeyphrases = new KeyphraseMetadata[1];
-                mKeyphrases[0] = new KeyphraseMetadata(
-                        searchKeyphraseId, searchKeyphrase, supportedLocales, recognitionModes);
-            } else {
-                mParseError = "searchKeyphraseId not specified in meta-data";
-                return;
-            }
+            initializeKeyphrasesFromTypedArray(array);
+            array.recycle();
         } catch (XmlPullParserException e) {
             mParseError = "Error parsing keyphrase enrollment meta-data: " + e;
             Slog.w(TAG, "error parsing keyphrase enrollment meta-data", e);
@@ -195,6 +173,65 @@
         }
     }
 
+    private void initializeKeyphrasesFromTypedArray(TypedArray array) {
+        // Get the keyphrase ID.
+        int searchKeyphraseId = array.getInt(
+                com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphraseId, -1);
+        if (searchKeyphraseId <= 0) {
+            mParseError = "No valid searchKeyphraseId specified in meta-data";
+            Slog.w(TAG, mParseError);
+            return;
+        }
+
+        // Get the keyphrase text.
+        String searchKeyphrase = array.getString(
+                com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphrase);
+        if (searchKeyphrase == null) {
+            mParseError = "No valid searchKeyphrase specified in meta-data";
+            Slog.w(TAG, mParseError);
+            return;
+        }
+
+        // Get the supported locales.
+        String searchKeyphraseSupportedLocales = array.getString(
+                com.android.internal.R.styleable
+                        .VoiceEnrollmentApplication_searchKeyphraseSupportedLocales);
+        if (searchKeyphraseSupportedLocales == null) {
+            mParseError = "No valid searchKeyphraseSupportedLocales specified in meta-data";
+            Slog.w(TAG, mParseError);
+            return;
+        }
+        ArraySet<Locale> locales = new ArraySet<>();
+        // Try adding locales if the locale string is non-empty.
+        if (!TextUtils.isEmpty(searchKeyphraseSupportedLocales)) {
+            try {
+                String[] supportedLocalesDelimited = searchKeyphraseSupportedLocales.split(",");
+                for (int i = 0; i < supportedLocalesDelimited.length; i++) {
+                    locales.add(Locale.forLanguageTag(supportedLocalesDelimited[i]));
+                }
+            } catch (Exception ex) {
+                // We catch a generic exception here because we don't want the system service
+                // to be affected by a malformed metadata because invalid locales were specified
+                // by the system application.
+                mParseError = "Error reading searchKeyphraseSupportedLocales from meta-data";
+                Slog.w(TAG, mParseError, ex);
+                return;
+            }
+        }
+
+        // Get the supported recognition modes.
+        int recognitionModes = array.getInt(com.android.internal.R.styleable
+                .VoiceEnrollmentApplication_searchKeyphraseRecognitionFlags, -1);
+        if (recognitionModes < 0) {
+            mParseError = "No valid searchKeyphraseRecognitionFlags specified in meta-data";
+            Slog.w(TAG, mParseError);
+            return;
+        }
+        mKeyphrases = new KeyphraseMetadata[1];
+        mKeyphrases[0] = new KeyphraseMetadata(searchKeyphraseId, searchKeyphrase, locales,
+                recognitionModes);
+    }
+
     public String getParseError() {
         return mParseError;
     }
@@ -217,11 +254,10 @@
      *        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(int action, String keyphrase, String locale) {
+    public Intent getManageKeyphraseIntent(int action, String keyphrase, Locale locale) {
         if (mEnrollmentPackage == null || mEnrollmentPackage.isEmpty()) {
             Slog.w(TAG, "No enrollment application exists");
             return null;
@@ -248,7 +284,7 @@
      * @return The metadata, if the enrollment client supports the given keyphrase
      *         and locale, null otherwise.
      */
-    public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, String locale) {
+    public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, Locale locale) {
         if (mKeyphrases == null || mKeyphrases.length == 0) {
             Slog.w(TAG, "Enrollment application doesn't support keyphrases");
             return null;
diff --git a/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
index 38305f9..ed8c296 100644
--- a/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
+++ b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
@@ -18,6 +18,8 @@
 
 import android.util.ArraySet;
 
+import java.util.Locale;
+
 /**
  * A Voice Keyphrase metadata read from the enrollment application.
  *
@@ -26,17 +28,14 @@
 public class KeyphraseMetadata {
     public final int id;
     public final String keyphrase;
-    public final ArraySet<String> supportedLocales;
+    public final ArraySet<Locale> supportedLocales;
     public final int recognitionModeFlags;
 
-    public KeyphraseMetadata(int id, String keyphrase, String[] supportedLocales,
+    public KeyphraseMetadata(int id, String keyphrase, ArraySet<Locale> supportedLocales,
             int recognitionModeFlags) {
         this.id = id;
         this.keyphrase = keyphrase;
-        this.supportedLocales = new ArraySet<String>(supportedLocales.length);
-        for (String locale : supportedLocales) {
-            this.supportedLocales.add(locale);
-        }
+        this.supportedLocales = supportedLocales;
         this.recognitionModeFlags = recognitionModeFlags;
     }
 
@@ -56,7 +55,7 @@
     /**
      * @return Indicates if we support the given locale.
      */
-    public boolean supportsLocale(String locale) {
+    public boolean supportsLocale(Locale locale) {
         return supportedLocales.isEmpty() || supportedLocales.contains(locale);
     }
 }
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 15e66a0..2095773 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -39,10 +39,10 @@
 
 import com.android.internal.app.IVoiceInteractionManagerService;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
 
 /**
  * A class that lets a VoiceInteractionService implementation interact with
@@ -167,7 +167,7 @@
     private static final int MSG_DETECTION_RESUME = 5;
 
     private final String mText;
-    private final String mLocale;
+    private final Locale mLocale;
     /**
      * The metadata of the Keyphrase, derived from the enrollment application.
      * This may be null if this keyphrase isn't supported by the enrollment application.
@@ -317,7 +317,7 @@
      *
      * @hide
      */
-    public AlwaysOnHotwordDetector(String text, String locale, Callback callback,
+    public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback,
             KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
             IVoiceInteractionService voiceInteractionService,
             IVoiceInteractionManagerService modelManagementService) {
@@ -491,8 +491,6 @@
      */
     void onSoundModelsChanged() {
         synchronized (mLock) {
-            // FIXME: This should stop the recognition if it was using an enrolled sound model
-            // that's no longer available.
             if (mAvailability == STATE_INVALID
                     || mAvailability == STATE_HARDWARE_UNAVAILABLE
                     || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) {
@@ -500,6 +498,13 @@
                 return;
             }
 
+            // Stop the recognition before proceeding.
+            // This is done because we want to stop the recognition on an older model if it changed
+            // or was deleted.
+            // The availability change callback should ensure that the client starts recognition
+            // again if needed.
+            stopRecognitionLocked();
+
             // Execute a refresh availability task - which should then notify of a change.
             new RefreshAvailabiltyTask().execute();
         }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index b2003568..884fa9f 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -35,6 +35,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.Locale;
 
 
 /**
@@ -163,7 +164,7 @@
      * Called during service initialization to tell you when the system is ready
      * to receive interaction from it. You should generally do initialization here
      * rather than in {@link #onCreate()}. Methods such as {@link #startSession(Bundle)} and
-     * {@link #createAlwaysOnHotwordDetector(String, String, android.service.voice.AlwaysOnHotwordDetector.Callback)}
+     * {@link #createAlwaysOnHotwordDetector(String, Locale, android.service.voice.AlwaysOnHotwordDetector.Callback)}
      * will not be operational until this point.
      */
     public void onReady() {
@@ -200,6 +201,17 @@
     }
 
     /**
+     * FIXME: Remove once the prebuilts are updated.
+     *
+     * @hide
+     */
+    @Deprecated
+    public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
+            String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) {
+        return createAlwaysOnHotwordDetector(keyphrase, new Locale(locale), callback);
+    }
+
+    /**
      * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
      * This instance must be retained and used by the client.
      * Calling this a second time invalidates the previously created hotword detector
@@ -207,12 +219,11 @@
      *
      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
      * @param locale The locale for which the enrollment needs to be performed.
-     *        This is a Java locale, for example "en_US".
      * @param callback The callback to notify of detection events.
      * @return An always-on hotword detector for the given keyphrase and locale.
      */
     public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
-            String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) {
+            String keyphrase, Locale locale, AlwaysOnHotwordDetector.Callback callback) {
         if (mSystemService == null) {
             throw new IllegalStateException("Not available until onReady() is called");
         }
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3524636..cc8d7cf 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -6930,8 +6930,8 @@
         <attr name="searchKeyphraseId" format="integer" />
         <!-- The actual keyphrase/hint text, or empty if not keyphrase dependent. @hide @SystemApi -->
         <attr name="searchKeyphrase" format="string" />
-        <!-- A comma separated list of java locales that are supported for this keyphrase,
-             or empty if not locale dependent. @hide @SystemApi -->
+        <!-- A comma separated list of BCP-47 language tag for locales that are supported
+             for this keyphrase, or empty if not locale dependent. @hide @SystemApi -->
         <attr name="searchKeyphraseSupportedLocales" format="string" />
         <!-- Flags for supported recognition modes. @hide @SystemApi -->
         <attr name="searchKeyphraseRecognitionFlags">
diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
index 77c0c32..1233c0c 100644
--- a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
+++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import java.util.Arrays;
+import java.util.Locale;
 
 public class MainInteractionService extends VoiceInteractionService {
     static final String TAG = "MainInteractionService";
@@ -67,7 +68,8 @@
         Log.i(TAG, "Keyphrase enrollment meta-data: "
                 + Arrays.toString(getKeyphraseEnrollmentInfo().listKeyphraseMetadata()));
 
-        mHotwordDetector = createAlwaysOnHotwordDetector("Hello There", "en-US", mHotwordCallback);
+        mHotwordDetector = createAlwaysOnHotwordDetector(
+                "Hello There", new Locale("en-US"), mHotwordCallback);
     }
 
     @Override