diff options
author | Sandeep Siddhartha <sansid@google.com> | 2014-10-06 16:46:55 -0700 |
---|---|---|
committer | Sandeep Siddhartha <sansid@google.com> | 2014-10-07 11:24:51 -0700 |
commit | b585ac5b5e672c11c80a01eb42a0d3ebd495f21b (patch) | |
tree | d1ea45482784a05a460317d65d91186f288f3902 | |
parent | fc8d65197a6404a93f1230ac5ebc635438a5c094 (diff) | |
download | frameworks_base-b585ac5b5e672c11c80a01eb42a0d3ebd495f21b.zip frameworks_base-b585ac5b5e672c11c80a01eb42a0d3ebd495f21b.tar.gz frameworks_base-b585ac5b5e672c11c80a01eb42a0d3ebd495f21b.tar.bz2 |
Add tests for model management [SDK Only]
This doesn't change any functionality/APIs etc.
It allows us to launch the activity and manually test the enrollment
methods.
Bug: 17885286
Change-Id: I506d9bb98a592131c04a50c9d6224164ffe07183
5 files changed, 369 insertions, 4 deletions
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(); + } + } } |