am f9c45add: am a591efe2: am dbc9aa2c: am eecf74bc: am ed65c63b: Merge "Add tests for model management [SDK Only]" into lmp-dev

* commit 'f9c45addceed8d98f5a9a8a7486e91e4980b1fec':
  Add tests for model management [SDK Only]
diff --git a/tests/VoiceEnrollment/AndroidManifest.xml b/tests/VoiceEnrollment/AndroidManifest.xml
index 6321222..46f6ff5 100644
--- a/tests/VoiceEnrollment/AndroidManifest.xml
+++ b/tests/VoiceEnrollment/AndroidManifest.xml
@@ -1,16 +1,20 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.test.voiceenrollment">
 
+    <uses-permission android:name="android.permission.MANAGE_VOICE_KEYPHRASES" />
     <application
         android:permission="android.permission.MANAGE_VOICE_KEYPHRASES">
-        <activity android:name="TestEnrollmentActivity" android:label="Voice Enrollment Application"
-                  android:theme="@android:style/Theme.Material.Light.Voice">
+        <activity
+            android:name="TestEnrollmentActivity"
+            android:label="Voice Enrollment Application"
+            android:theme="@android:style/Theme.Material.Light.Voice">
             <intent-filter>
                 <action android:name="com.android.intent.action.MANAGE_VOICE_KEYPHRASES" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <meta-data android:name="android.voice_enrollment"
+        <meta-data
+            android:name="android.voice_enrollment"
             android:resource="@xml/enrollment_application"/>
     </application>
 </manifest>
diff --git a/tests/VoiceEnrollment/res/layout/main.xml b/tests/VoiceEnrollment/res/layout/main.xml
new file mode 100644
index 0000000..9d2b9d9
--- /dev/null
+++ b/tests/VoiceEnrollment/res/layout/main.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 Google Inc.
+
+     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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+
+    <Button
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/enroll"
+        android:onClick="onEnrollButtonClicked"
+        android:padding="20dp" />
+
+    <Button
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/reenroll"
+        android:onClick="onReEnrollButtonClicked"
+        android:padding="20dp" />
+
+    <Button
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/unenroll"
+        android:onClick="onUnEnrollButtonClicked"
+        android:padding="20dp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/VoiceEnrollment/res/values/strings.xml b/tests/VoiceEnrollment/res/values/strings.xml
new file mode 100644
index 0000000..07bac2a
--- /dev/null
+++ b/tests/VoiceEnrollment/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 Google Inc.
+
+     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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string name="enroll">Enroll</string>
+    <string name="reenroll">Re-enroll</string>
+    <string name="unenroll">Un-enroll</string>
+</resources>
\ No newline at end of file
diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java
new file mode 100644
index 0000000..9e544a5
--- /dev/null
+++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java
@@ -0,0 +1,198 @@
+/*
+ * 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 com.android.test.voiceenrollment;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
+import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.voice.AlwaysOnHotwordDetector;
+import android.util.Log;
+
+import com.android.internal.app.IVoiceInteractionManagerService;
+
+/**
+ * Utility class for the enrollment operations like enroll;re-enroll & un-enroll.
+ */
+public class EnrollmentUtil {
+    private static final String TAG = "TestEnrollmentUtil";
+
+    /**
+     * Activity Action: Show activity for managing the keyphrases for hotword detection.
+     * This needs to be defined by an activity that supports enrolling users for hotword/keyphrase
+     * detection.
+     */
+    public static final String ACTION_MANAGE_VOICE_KEYPHRASES =
+            KeyphraseEnrollmentInfo.ACTION_MANAGE_VOICE_KEYPHRASES;
+
+    /**
+     * 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_ACTION =
+            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_ACTION;
+
+    /**
+     * Intent extra: The hint text to be shown on the voice keyphrase management UI.
+     */
+    public static final String EXTRA_VOICE_KEYPHRASE_HINT_TEXT =
+            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_HINT_TEXT;
+    /**
+     * Intent extra: The voice locale to use while managing the keyphrase.
+     */
+    public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
+            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_LOCALE;
+
+    /** Simple recognition of the key phrase */
+    public static final int RECOGNITION_MODE_VOICE_TRIGGER =
+            SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
+    /** Trigger only if one user is identified */
+    public static final int RECOGNITION_MODE_USER_IDENTIFICATION =
+            SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
+
+    private final IVoiceInteractionManagerService mModelManagementService;
+
+    public EnrollmentUtil() {
+        mModelManagementService = IVoiceInteractionManagerService.Stub.asInterface(
+                ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+    }
+
+    /**
+     * Adds/Updates a sound model.
+     * The sound model must contain a valid UUID,
+     * exactly 1 keyphrase,
+     * and users for which the keyphrase is valid - typically the current user.
+     *
+     * @param soundModel The sound model to add/update.
+     * @return {@code true} if the call succeeds, {@code false} otherwise.
+     */
+    public boolean addOrUpdateSoundModel(KeyphraseSoundModel soundModel) {
+        if (!verifyKeyphraseSoundModel(soundModel)) {
+            return false;
+        }
+
+        int status = SoundTrigger.STATUS_ERROR;
+        try {
+            status = mModelManagementService.updateKeyphraseSoundModel(soundModel);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel", e);
+        }
+        return status == SoundTrigger.STATUS_OK;
+    }
+
+    /**
+     * Gets the sound model for the given keyphrase, null if none exists.
+     * This should be used for re-enrollment purposes.
+     * If a sound model for a given keyphrase exists, and it needs to be updated,
+     * it should be obtained using this method, updated and then passed in to
+     * {@link #addOrUpdateSoundModel(KeyphraseSoundModel)} without changing the IDs.
+     *
+     * @param keyphraseId The keyphrase ID to look-up the sound model for.
+     * @param bcp47Locale The locale for with to look up the sound model for.
+     * @return The sound model if one was found, null otherwise.
+     */
+    @Nullable
+    public KeyphraseSoundModel getSoundModel(int keyphraseId, String bcp47Locale) {
+        if (keyphraseId <= 0) {
+            Log.e(TAG, "Keyphrase must have a valid ID");
+            return null;
+        }
+
+        KeyphraseSoundModel model = null;
+        try {
+            model = mModelManagementService.getKeyphraseSoundModel(keyphraseId, bcp47Locale);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel");
+        }
+
+        if (model == null) {
+            Log.w(TAG, "No models present for the gien keyphrase ID");
+            return null;
+        } else {
+            return model;
+        }
+    }
+
+    /**
+     * Deletes the sound model for the given keyphrase id.
+     *
+     * @param keyphraseId The keyphrase ID to look-up the sound model for.
+     * @return {@code true} if the call succeeds, {@code false} otherwise.
+     */
+    @Nullable
+    public boolean deleteSoundModel(int keyphraseId, String bcp47Locale) {
+        if (keyphraseId <= 0) {
+            Log.e(TAG, "Keyphrase must have a valid ID");
+            return false;
+        }
+
+        int status = SoundTrigger.STATUS_ERROR;
+        try {
+            status = mModelManagementService.deleteKeyphraseSoundModel(keyphraseId, bcp47Locale);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel");
+        }
+        return status == SoundTrigger.STATUS_OK;
+    }
+
+    private boolean verifyKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
+        if (soundModel == null) {
+            Log.e(TAG, "KeyphraseSoundModel must be non-null");
+            return false;
+        }
+        if (soundModel.uuid == null) {
+            Log.e(TAG, "KeyphraseSoundModel must have a UUID");
+            return false;
+        }
+        if (soundModel.data == null) {
+            Log.e(TAG, "KeyphraseSoundModel must have data");
+            return false;
+        }
+        if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
+            Log.e(TAG, "Keyphrase must be exactly 1");
+            return false;
+        }
+        Keyphrase keyphrase = soundModel.keyphrases[0];
+        if (keyphrase.id <= 0) {
+            Log.e(TAG, "Keyphrase must have a valid ID");
+            return false;
+        }
+        if (keyphrase.recognitionModes < 0) {
+            Log.e(TAG, "Recognition modes must be valid");
+            return false;
+        }
+        if (keyphrase.locale == null) {
+            Log.e(TAG, "Locale must not be null");
+            return false;
+        }
+        if (keyphrase.text == null) {
+            Log.e(TAG, "Text must not be null");
+            return false;
+        }
+        if (keyphrase.users == null || keyphrase.users.length == 0) {
+            Log.e(TAG, "Keyphrase must have valid user(s)");
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
index 7fbd965..2494db7 100644
--- a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
+++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
@@ -16,8 +16,106 @@
 
 package com.android.test.voiceenrollment;
 
+import java.util.Random;
+import java.util.UUID;
+
 import android.app.Activity;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
+import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
 
 public class TestEnrollmentActivity extends Activity {
-    // TODO(sansid): Add a test enrollment flow here.
+    private static final String TAG = "TestEnrollmentActivity";
+    private static final boolean DBG = true;
+
+    /** Keyphrase related constants, must match those defined in enrollment_application.xml */
+    private static final int KEYPHRASE_ID = 101;
+    private static final int RECOGNITION_MODES = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
+    private static final String BCP47_LOCALE = "fr-FR";
+    private static final String TEXT = "Hello There";
+
+    private EnrollmentUtil mEnrollmentUtil;
+    private Random mRandom;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        if (DBG) Log.d(TAG, "onCreate");
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+        mEnrollmentUtil = new EnrollmentUtil();
+        mRandom = new Random();
+    }
+
+    /**
+     * Called when the user clicks the enroll button.
+     * Performs a fresh enrollment.
+     */
+    public void onEnrollButtonClicked(View v) {
+        Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES, BCP47_LOCALE, TEXT,
+                new int[] { UserManager.get(this).getUserHandle() /* current user */});
+        UUID modelUuid = UUID.randomUUID();
+        // Generate a fake model to push.
+        byte[] data = new byte[1024];
+        mRandom.nextBytes(data);
+        KeyphraseSoundModel soundModel = new KeyphraseSoundModel(modelUuid, null, data,
+                new Keyphrase[] { kp });
+        boolean status = mEnrollmentUtil.addOrUpdateSoundModel(soundModel);
+        if (status) {
+            Toast.makeText(
+                    this, "Successfully enrolled, model UUID=" + modelUuid, Toast.LENGTH_SHORT)
+                    .show();
+        } else {
+            Toast.makeText(this, "Failed to enroll!!!" + modelUuid, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    /**
+     * Called when the user clicks the un-enroll button.
+     * Clears the enrollment information for the user.
+     */
+    public void onUnEnrollButtonClicked(View v) {
+        KeyphraseSoundModel soundModel = mEnrollmentUtil.getSoundModel(KEYPHRASE_ID, BCP47_LOCALE);
+        if (soundModel == null) {
+            Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
+            return;
+        }
+        boolean status = mEnrollmentUtil.deleteSoundModel(KEYPHRASE_ID, BCP47_LOCALE);
+        if (status) {
+            Toast.makeText(this, "Successfully un-enrolled, model UUID=" + soundModel.uuid,
+                    Toast.LENGTH_SHORT)
+                    .show();
+        } else {
+            Toast.makeText(this, "Failed to un-enroll!!!", Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    /**
+     * Called when the user clicks the re-enroll button.
+     * Uses the previously enrolled sound model and makes changes to it before pushing it back.
+     */
+    public void onReEnrollButtonClicked(View v) {
+        KeyphraseSoundModel soundModel = mEnrollmentUtil.getSoundModel(KEYPHRASE_ID, BCP47_LOCALE);
+        if (soundModel == null) {
+            Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
+            return;
+        }
+        // Generate a fake model to push.
+        byte[] data = new byte[2048];
+        mRandom.nextBytes(data);
+        KeyphraseSoundModel updated = new KeyphraseSoundModel(soundModel.uuid,
+                soundModel.vendorUuid, data, soundModel.keyphrases);
+        boolean status = mEnrollmentUtil.addOrUpdateSoundModel(updated);
+        if (status) {
+            Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.uuid,
+                    Toast.LENGTH_SHORT)
+                    .show();
+        } else {
+            Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show();
+        }
+    }
 }