diff options
author | Nicholas Ambur <nambur@google.com> | 2020-01-14 20:28:37 -0800 |
---|---|---|
committer | Nicholas Ambur <nambur@google.com> | 2020-01-22 16:40:13 -0800 |
commit | ef84fc48433d47ea9c91dcb3273ae3d74ca6d32a (patch) | |
tree | 7ae618ac7d63eb705ca913274d800c38cbc98b58 | |
parent | 4ec0be1fafd70ff43f5c579c73627b147168b194 (diff) |
add KeyphraseModelManager
Exposes a set of @SystemApi's allowing the active VoiceInteractionService
to enroll voice models.
Bug: 147159435
Test: manual tested enrollment and unenrollment via bundled
hotwordenrollment application and test app.
Change-Id: I94ef3550df236486401a0a6f9de9d874b9bf9b46
11 files changed, 458 insertions, 157 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index d02bbfb44439..c5e7ae390387 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3566,9 +3566,34 @@ package android.hardware.radio { package android.hardware.soundtrigger { public class SoundTrigger { + field public static final int RECOGNITION_MODE_GENERIC = 8; // 0x8 + field public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 4; // 0x4 + field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2 + field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1 field public static final int STATUS_OK = 0; // 0x0 } + public static final class SoundTrigger.Keyphrase implements android.os.Parcelable { + ctor public SoundTrigger.Keyphrase(int, int, @NonNull java.util.Locale, @NonNull String, @Nullable int[]); + method @NonNull public static android.hardware.soundtrigger.SoundTrigger.Keyphrase readFromParcel(@NonNull android.os.Parcel); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.Keyphrase> CREATOR; + field public final int id; + field @NonNull public final java.util.Locale locale; + field public final int recognitionModes; + field @NonNull public final String text; + field @NonNull public final int[] users; + } + + public static final class SoundTrigger.KeyphraseSoundModel extends android.hardware.soundtrigger.SoundTrigger.SoundModel implements android.os.Parcelable { + ctor public SoundTrigger.KeyphraseSoundModel(@NonNull java.util.UUID, @NonNull java.util.UUID, @Nullable byte[], @Nullable android.hardware.soundtrigger.SoundTrigger.Keyphrase[], int); + ctor public SoundTrigger.KeyphraseSoundModel(@NonNull java.util.UUID, @NonNull java.util.UUID, @Nullable byte[], @Nullable android.hardware.soundtrigger.SoundTrigger.Keyphrase[]); + method @NonNull public static android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel readFromParcel(@NonNull android.os.Parcel); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel> CREATOR; + field @NonNull public final android.hardware.soundtrigger.SoundTrigger.Keyphrase[] keyphrases; + } + public static final class SoundTrigger.ModelParamRange implements android.os.Parcelable { method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.ModelParamRange> CREATOR; @@ -3607,6 +3632,16 @@ package android.hardware.soundtrigger { method public boolean isCaptureAvailable(); } + public static class SoundTrigger.SoundModel { + field public static final int TYPE_GENERIC_SOUND = 1; // 0x1 + field public static final int TYPE_KEYPHRASE = 0; // 0x0 + field @NonNull public final byte[] data; + field public final int type; + field @NonNull public final java.util.UUID uuid; + field @NonNull public final java.util.UUID vendorUuid; + field public final int version; + } + } package android.hardware.usb { @@ -4739,6 +4774,16 @@ package android.media.tv.tuner.filter { } +package android.media.voice { + + public final class KeyphraseModelManager { + method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void deleteKeyphraseSoundModel(int, @NonNull java.util.Locale); + method @Nullable @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel getKeyphraseSoundModel(int, @NonNull java.util.Locale); + method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void updateKeyphraseSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel); + } + +} + package android.metrics { public class LogMaker { @@ -9128,6 +9173,14 @@ package android.service.trust { } +package android.service.voice { + + public class VoiceInteractionService extends android.app.Service { + method @NonNull @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public final android.media.voice.KeyphraseModelManager createKeyphraseModelManager(); + } + +} + package android.service.wallpaper { public class WallpaperService.Engine { diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java index d43a619a7c1f..a30fd6b51e76 100644 --- a/core/java/android/hardware/soundtrigger/ConversionUtil.java +++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java @@ -130,7 +130,7 @@ class ConversionUtil { aidlPhrase.id = apiPhrase.id; aidlPhrase.recognitionModes = api2aidlRecognitionModes(apiPhrase.recognitionModes); aidlPhrase.users = Arrays.copyOf(apiPhrase.users, apiPhrase.users.length); - aidlPhrase.locale = apiPhrase.locale; + aidlPhrase.locale = apiPhrase.locale.toLanguageTag(); aidlPhrase.text = apiPhrase.text; return aidlPhrase; } diff --git a/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java index eb5d0cb539a1..ef76c620f3f3 100644 --- a/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java +++ b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java @@ -17,6 +17,7 @@ package android.hardware.soundtrigger; import android.Manifest; +import android.annotation.IntDef; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -24,7 +25,6 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; 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; @@ -35,6 +35,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -66,9 +68,10 @@ public class KeyphraseEnrollmentInfo { "com.android.intent.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}. + * + * @see #MANAGE_ACTION_ENROLL + * @see #MANAGE_ACTION_RE_ENROLL + * @see #MANAGE_ACTION_UN_ENROLL */ public static final String EXTRA_VOICE_KEYPHRASE_ACTION = "com.android.intent.extra.VOICE_KEYPHRASE_ACTION"; @@ -86,6 +89,31 @@ public class KeyphraseEnrollmentInfo { "com.android.intent.extra.VOICE_KEYPHRASE_LOCALE"; /** + * Keyphrase management actions used with the {@link #EXTRA_VOICE_KEYPHRASE_ACTION} intent extra + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "MANAGE_ACTION_" }, value = { + MANAGE_ACTION_ENROLL, + MANAGE_ACTION_RE_ENROLL, + MANAGE_ACTION_UN_ENROLL + }) + public @interface ManageActions {} + + /** + * Indicates desired action to enroll keyphrase model + */ + public static final int MANAGE_ACTION_ENROLL = 0; + /** + * Indicates desired action to re-enroll keyphrase model + */ + public static final int MANAGE_ACTION_RE_ENROLL = 1; + /** + * Indicates desired action to un-enroll keyphrase model + */ + public static final int MANAGE_ACTION_UN_ENROLL = 2; + + /** * List of available keyphrases. */ final private KeyphraseMetadata[] mKeyphrases; @@ -294,15 +322,13 @@ public class KeyphraseEnrollmentInfo { * for the locale. * * @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. * @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, Locale locale) { + public Intent getManageKeyphraseIntent(@ManageActions int action, String keyphrase, + Locale locale) { if (mKeyphrasePackageMap == null || mKeyphrasePackageMap.isEmpty()) { Slog.w(TAG, "No enrollment application exists"); return null; diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 1932f46975c5..d505ae59dfaf 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -49,6 +49,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Locale; import java.util.UUID; /** @@ -148,6 +149,7 @@ public class SoundTrigger { public final int maxUsers; /** Supported recognition modes (bit field, RECOGNITION_MODE_VOICE_TRIGGER ...) */ + @RecognitionModes public final int recognitionModes; /** Supports seamless transition to capture mode after recognition */ @@ -175,9 +177,9 @@ public class SoundTrigger { ModuleProperties(int id, @NonNull String implementor, @NonNull String description, @NonNull String uuid, int version, @NonNull String supportedModelArch, - int maxSoundModels, int maxKeyphrases, int maxUsers, int recognitionModes, - boolean supportsCaptureTransition, int maxBufferMs, - boolean supportsConcurrentCapture, int powerConsumptionMw, + int maxSoundModels, int maxKeyphrases, int maxUsers, + @RecognitionModes int recognitionModes, boolean supportsCaptureTransition, + int maxBufferMs, boolean supportsConcurrentCapture, int powerConsumptionMw, boolean returnsTriggerInEvent, int audioCapabilities) { this.id = id; this.implementor = requireNonNull(implementor); @@ -271,16 +273,27 @@ public class SoundTrigger { } } - /***************************************************************************** + /** * A SoundModel describes the attributes and contains the binary data used by the hardware * implementation to detect a particular sound pattern. * A specialized version {@link KeyphraseSoundModel} is defined for key phrase * sound models. - * - * @hide - ****************************************************************************/ + */ public static class SoundModel { - /** Undefined sound model type */ + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_GENERIC_SOUND, + TYPE_KEYPHRASE, + TYPE_UNKNOWN, + }) + public @interface SoundModelType {} + + /** + * Undefined sound model type + * @hide + */ public static final int TYPE_UNKNOWN = -1; /** Keyphrase sound model */ @@ -293,15 +306,14 @@ public class SoundTrigger { public static final int TYPE_GENERIC_SOUND = 1; /** Unique sound model identifier */ - @UnsupportedAppUsage @NonNull public final UUID uuid; /** Sound model type (e.g. TYPE_KEYPHRASE); */ + @SoundModelType public final int type; /** Unique sound model vendor identifier */ - @UnsupportedAppUsage @NonNull public final UUID vendorUuid; @@ -309,11 +321,11 @@ public class SoundTrigger { public final int version; /** Opaque data. For use by vendor implementation and enrollment application */ - @UnsupportedAppUsage @NonNull public final byte[] data; - public SoundModel(@NonNull UUID uuid, @Nullable UUID vendorUuid, int type, + /** @hide */ + public SoundModel(@NonNull UUID uuid, @Nullable UUID vendorUuid, @SoundModelType int type, @Nullable byte[] data, int version) { this.uuid = requireNonNull(uuid); this.vendorUuid = vendorUuid != null ? vendorUuid : new UUID(0, 0); @@ -336,67 +348,90 @@ public class SoundTrigger { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (!(obj instanceof SoundModel)) + } + if (!(obj instanceof SoundModel)) { return false; + } SoundModel other = (SoundModel) obj; - if (type != other.type) + if (type != other.type) { return false; + } if (uuid == null) { - if (other.uuid != null) + if (other.uuid != null) { return false; - } else if (!uuid.equals(other.uuid)) + } + } else if (!uuid.equals(other.uuid)) { return false; + } if (vendorUuid == null) { - if (other.vendorUuid != null) + if (other.vendorUuid != null) { return false; - } else if (!vendorUuid.equals(other.vendorUuid)) + } + } else if (!vendorUuid.equals(other.vendorUuid)) { return false; - if (!Arrays.equals(data, other.data)) + } + if (!Arrays.equals(data, other.data)) { return false; - if (version != other.version) + } + if (version != other.version) { return false; + } return true; } } - /***************************************************************************** + /** * A Keyphrase describes a key phrase that can be detected by a * {@link KeyphraseSoundModel} - * - * @hide - ****************************************************************************/ - public static class Keyphrase implements Parcelable { + */ + public static final class Keyphrase implements Parcelable { /** Unique identifier for this keyphrase */ - @UnsupportedAppUsage public final int id; - /** Recognition modes supported for this key phrase in the model */ - @UnsupportedAppUsage + /** + * Recognition modes supported for this key phrase in the model + * + * @see #RECOGNITION_MODE_VOICE_TRIGGER + * @see #RECOGNITION_MODE_USER_IDENTIFICATION + * @see #RECOGNITION_MODE_USER_AUTHENTICATION + * @see #RECOGNITION_MODE_GENERIC + */ + @RecognitionModes public final int recognitionModes; - /** Locale of the keyphrase. JAVA Locale string e.g en_US */ - @UnsupportedAppUsage + /** Locale of the keyphrase. */ @NonNull - public final String locale; + public final Locale locale; /** Key phrase text */ - @UnsupportedAppUsage @NonNull public final String text; - /** Users this key phrase has been trained for. countains sound trigger specific user IDs - * derived from system user IDs {@link android.os.UserHandle#getIdentifier()}. */ - @UnsupportedAppUsage + /** + * Users this key phrase has been trained for. countains sound trigger specific user IDs + * derived from system user IDs {@link android.os.UserHandle#getIdentifier()}. + */ @NonNull public final int[] users; - @UnsupportedAppUsage - public Keyphrase(int id, int recognitionModes, @NonNull String locale, @NonNull String text, - @Nullable int[] users) { + /** + * Constructor for Keyphrase describes a key phrase that can be detected by a + * {@link KeyphraseSoundModel} + * + * @param id Unique keyphrase identifier for this keyphrase + * @param recognitionModes Bit field representation of recognition modes this keyphrase + * supports + * @param locale Locale of the keyphrase + * @param text Key phrase text + * @param users Users this key phrase has been trained for. + */ + public Keyphrase(int id, @RecognitionModes int recognitionModes, @NonNull Locale locale, + @NonNull String text, @Nullable int[] users) { this.id = id; this.recognitionModes = recognitionModes; this.locale = requireNonNull(locale); @@ -404,21 +439,27 @@ public class SoundTrigger { this.users = users != null ? users : new int[0]; } - public static final @android.annotation.NonNull Parcelable.Creator<Keyphrase> CREATOR - = new Parcelable.Creator<Keyphrase>() { - public Keyphrase createFromParcel(Parcel in) { - return Keyphrase.fromParcel(in); + public static final @NonNull Parcelable.Creator<Keyphrase> CREATOR = + new Parcelable.Creator<Keyphrase>() { + @NonNull + public Keyphrase createFromParcel(@NonNull Parcel in) { + return Keyphrase.readFromParcel(in); } + @NonNull public Keyphrase[] newArray(int size) { return new Keyphrase[size]; } }; - private static Keyphrase fromParcel(Parcel in) { + /** + * Read from Parcel to generate keyphrase + */ + @NonNull + public static Keyphrase readFromParcel(@NonNull Parcel in) { int id = in.readInt(); int recognitionModes = in.readInt(); - String locale = in.readString(); + Locale locale = Locale.forLanguageTag(in.readString()); String text = in.readString(); int[] users = null; int numUsers = in.readInt(); @@ -430,10 +471,10 @@ public class SoundTrigger { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(id); dest.writeInt(recognitionModes); - dest.writeString(locale); + dest.writeString(locale.toLanguageTag()); dest.writeString(text); if (users != null) { dest.writeInt(users.length); @@ -443,6 +484,7 @@ public class SoundTrigger { } } + /** @hide */ @Override public int describeContents() { return 0; @@ -462,49 +504,57 @@ public class SoundTrigger { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } Keyphrase other = (Keyphrase) obj; if (text == null) { - if (other.text != null) + if (other.text != null) { return false; - } else if (!text.equals(other.text)) + } + } else if (!text.equals(other.text)) { return false; - if (id != other.id) + } + if (id != other.id) { return false; + } if (locale == null) { - if (other.locale != null) + if (other.locale != null) { return false; - } else if (!locale.equals(other.locale)) + } + } else if (!locale.equals(other.locale)) { return false; - if (recognitionModes != other.recognitionModes) + } + if (recognitionModes != other.recognitionModes) { return false; - if (!Arrays.equals(users, other.users)) + } + if (!Arrays.equals(users, other.users)) { return false; + } return true; } @Override public String toString() { - return "Keyphrase [id=" + id + ", recognitionModes=" + recognitionModes + ", locale=" - + locale + ", text=" + text + ", users=" + Arrays.toString(users) + "]"; + return "Keyphrase [id=" + id + ", recognitionModes=" + recognitionModes + + ", locale=" + locale.toLanguageTag() + ", text=" + text + + ", users=" + Arrays.toString(users) + "]"; } } - /***************************************************************************** + /** * A KeyphraseSoundModel is a specialized {@link SoundModel} for key phrases. * It contains data needed by the hardware to detect a certain number of key phrases * and the list of corresponding {@link Keyphrase} descriptors. - * - * @hide - ****************************************************************************/ - public static class KeyphraseSoundModel extends SoundModel implements Parcelable { + */ + public static final class KeyphraseSoundModel extends SoundModel implements Parcelable { /** Key phrases in this sound model */ - @UnsupportedAppUsage @NonNull public final Keyphrase[] keyphrases; // keyword phrases in model @@ -515,24 +565,29 @@ public class SoundTrigger { this.keyphrases = keyphrases != null ? keyphrases : new Keyphrase[0]; } - @UnsupportedAppUsage public KeyphraseSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, @Nullable Keyphrase[] keyphrases) { this(uuid, vendorUuid, data, keyphrases, -1); } - public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseSoundModel> CREATOR - = new Parcelable.Creator<KeyphraseSoundModel>() { - public KeyphraseSoundModel createFromParcel(Parcel in) { - return KeyphraseSoundModel.fromParcel(in); + public static final @NonNull Parcelable.Creator<KeyphraseSoundModel> CREATOR = + new Parcelable.Creator<KeyphraseSoundModel>() { + @NonNull + public KeyphraseSoundModel createFromParcel(@NonNull Parcel in) { + return KeyphraseSoundModel.readFromParcel(in); } + @NonNull public KeyphraseSoundModel[] newArray(int size) { return new KeyphraseSoundModel[size]; } }; - private static KeyphraseSoundModel fromParcel(Parcel in) { + /** + * Read from Parcel to generate KeyphraseSoundModel + */ + @NonNull + public static KeyphraseSoundModel readFromParcel(@NonNull Parcel in) { UUID uuid = UUID.fromString(in.readString()); UUID vendorUuid = null; int length = in.readInt(); @@ -545,13 +600,14 @@ public class SoundTrigger { return new KeyphraseSoundModel(uuid, vendorUuid, data, keyphrases, version); } + /** @hide */ @Override public int describeContents() { return 0; } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(uuid.toString()); if (vendorUuid == null) { dest.writeInt(-1); @@ -583,15 +639,19 @@ public class SoundTrigger { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (!super.equals(obj)) + } + if (!super.equals(obj)) { return false; - if (!(obj instanceof KeyphraseSoundModel)) + } + if (!(obj instanceof KeyphraseSoundModel)) { return false; + } KeyphraseSoundModel other = (KeyphraseSoundModel) obj; - if (!Arrays.equals(keyphrases, other.keyphrases)) + if (!Arrays.equals(keyphrases, other.keyphrases)) { return false; + } return true; } } @@ -760,31 +820,32 @@ public class SoundTrigger { } /** - * Modes for key phrase recognition + * Modes for key phrase recognition + * @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = { + RECOGNITION_MODE_VOICE_TRIGGER, + RECOGNITION_MODE_USER_IDENTIFICATION, + RECOGNITION_MODE_USER_AUTHENTICATION, + RECOGNITION_MODE_GENERIC + }) + public @interface RecognitionModes {} /** - * Simple recognition of the key phrase - * - * @hide + * Trigger on recognition of a key phrase */ public static final int RECOGNITION_MODE_VOICE_TRIGGER = 0x1; /** * Trigger only if one user is identified - * - * @hide */ public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 0x2; /** * Trigger only if one user is authenticated - * - * @hide */ public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 0x4; /** * Generic (non-speech) recognition. - * - * @hide */ public static final int RECOGNITION_MODE_GENERIC = 0x8; diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 0f339988ba3e..d7b81c38f2c1 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -85,34 +85,6 @@ public class AlwaysOnHotwordDetector { */ private static final int STATE_NOT_READY = 0; - // Keyphrase management actions. Used in getManageIntent() ----// - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "MANAGE_ACTION_" }, value = { - MANAGE_ACTION_ENROLL, - MANAGE_ACTION_RE_ENROLL, - MANAGE_ACTION_UN_ENROLL - }) - private @interface ManageActions {} - - /** - * Indicates that we need to enroll. - * - * @hide - */ - public static final int MANAGE_ACTION_ENROLL = 0; - /** - * Indicates that we need to re-enroll. - * - * @hide - */ - public static final int MANAGE_ACTION_RE_ENROLL = 1; - /** - * Indicates that we need to un-enroll. - * - * @hide - */ - public static final int MANAGE_ACTION_UN_ENROLL = 2; - //-- Flags for startRecognition ----// /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -679,7 +651,7 @@ public class AlwaysOnHotwordDetector { public Intent createEnrollIntent() { if (DBG) Slog.d(TAG, "createEnrollIntent"); synchronized (mLock) { - return getManageIntentLocked(MANAGE_ACTION_ENROLL); + return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_ENROLL); } } @@ -700,7 +672,7 @@ public class AlwaysOnHotwordDetector { public Intent createUnEnrollIntent() { if (DBG) Slog.d(TAG, "createUnEnrollIntent"); synchronized (mLock) { - return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL); + return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_UN_ENROLL); } } @@ -721,11 +693,11 @@ public class AlwaysOnHotwordDetector { public Intent createReEnrollIntent() { if (DBG) Slog.d(TAG, "createReEnrollIntent"); synchronized (mLock) { - return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); + return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_RE_ENROLL); } } - private Intent getManageIntentLocked(int action) { + private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action) { if (mAvailability == STATE_INVALID) { throw new IllegalStateException("getManageIntent called on an invalid detector"); } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 36e057f4a97d..fc99836b82fd 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -16,14 +16,18 @@ package android.service.voice; +import android.Manifest; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SdkConstant; +import android.annotation.SystemApi; import android.app.Service; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.media.voice.KeyphraseModelManager; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -304,6 +308,23 @@ public class VoiceInteractionService extends Service { } /** + * Creates an {@link KeyphraseModelManager} to use for enrolling voice models outside of the + * pre-bundled system voice models. + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES) + @NonNull + public final KeyphraseModelManager createKeyphraseModelManager() { + if (mSystemService == null) { + throw new IllegalStateException("Not available until onReady() is called"); + } + synchronized (mLock) { + return new KeyphraseModelManager(mSystemService); + } + } + + /** * @return Details of keyphrases available for enrollment. * @hide */ diff --git a/media/java/android/media/voice/KeyphraseModelManager.java b/media/java/android/media/voice/KeyphraseModelManager.java new file mode 100644 index 000000000000..3fa38e0a5854 --- /dev/null +++ b/media/java/android/media/voice/KeyphraseModelManager.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 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.media.voice; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.hardware.soundtrigger.SoundTrigger; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Slog; + +import com.android.internal.app.IVoiceInteractionManagerService; + +import java.util.Locale; +import java.util.Objects; + +/** + * This class provides management of voice based sound recognition models. Usage of this class is + * restricted to system or signature applications only. This allows OEMs to write apps that can + * manage voice based sound trigger models. + * Callers of this class are expected to have whitelist manifest permission MANAGE_VOICE_KEYPHRASES. + * Callers of this class are expected to be the designated voice interaction service via + * {@link Settings.Secure.VOICE_INTERACTION_SERVICE}. + * @hide + */ +@SystemApi +public final class KeyphraseModelManager { + private static final boolean DBG = false; + private static final String TAG = "KeyphraseModelManager"; + + private final IVoiceInteractionManagerService mVoiceInteractionManagerService; + + /** + * @hide + */ + public KeyphraseModelManager( + IVoiceInteractionManagerService voiceInteractionManagerService) { + if (DBG) { + Slog.i(TAG, "KeyphraseModelManager created."); + } + mVoiceInteractionManagerService = voiceInteractionManagerService; + } + + + /** + * Gets the registered sound model for keyphrase detection for the current user. + * The keyphraseId and locale passed must match a supported model passed in via + * {@link #updateKeyphraseSoundModel}. + * If the active voice interaction service changes from the current user, all requests will be + * rejected, and any registered models will be unregistered. + * + * @param keyphraseId The unique identifier for the keyphrase. + * @param locale The locale language tag supported by the desired model. + * @return Registered keyphrase sound model matching the keyphrase ID and locale. May be null if + * no matching sound model exists. + * @throws SecurityException Thrown when caller does not have MANAGE_VOICE_KEYPHRASES permission + * or if the caller is not the active voice interaction service. + */ + @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES) + @Nullable + public SoundTrigger.KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, + @NonNull Locale locale) { + Objects.requireNonNull(locale); + try { + return mVoiceInteractionManagerService.getKeyphraseSoundModel(keyphraseId, + locale.toLanguageTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Add or update the given keyphrase sound model to the registered models pool for the current + * user. + * If a model exists with the same Keyphrase ID, locale, and user list. The registered model + * will be overwritten with the new model. + * If the active voice interaction service changes from the current user, all requests will be + * rejected, and any registered models will be unregistered. + * + * @param model Keyphrase sound model to be updated. + * @throws ServiceSpecificException Thrown with error code if failed to update the keyphrase + * sound model. + * @throws SecurityException Thrown when caller does not have MANAGE_VOICE_KEYPHRASES permission + * or if the caller is not the active voice interaction service. + */ + @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES) + public void updateKeyphraseSoundModel(@NonNull SoundTrigger.KeyphraseSoundModel model) { + Objects.requireNonNull(model); + try { + int status = mVoiceInteractionManagerService.updateKeyphraseSoundModel(model); + if (status != SoundTrigger.STATUS_OK) { + throw new ServiceSpecificException(status); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Delete keyphrase sound model from the registered models pool for the current user matching\ + * the keyphrase ID and locale. + * The keyphraseId and locale passed must match a supported model passed in via + * {@link #updateKeyphraseSoundModel}. + * If the active voice interaction service changes from the current user, all requests will be + * rejected, and any registered models will be unregistered. + * + * @param keyphraseId The unique identifier for the keyphrase. + * @param locale The locale language tag supported by the desired model. + * @throws ServiceSpecificException Thrown with error code if failed to delete the keyphrase + * sound model. + * @throws SecurityException Thrown when caller does not have MANAGE_VOICE_KEYPHRASES permission + * or if the caller is not the active voice interaction service. + */ + @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES) + public void deleteKeyphraseSoundModel(int keyphraseId, @NonNull Locale locale) { + Objects.requireNonNull(locale); + try { + int status = mVoiceInteractionManagerService.deleteKeyphraseSoundModel(keyphraseId, + locale.toLanguageTag()); + if (status != SoundTrigger.STATUS_OK) { + throw new ServiceSpecificException(status); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java index c58b6da64baa..195a9e49d70d 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java @@ -46,18 +46,21 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String NAME = "sound_model.db"; private static final int VERSION = 7; - public static interface SoundModelContract { - public static final String TABLE = "sound_model"; - public static final String KEY_MODEL_UUID = "model_uuid"; - public static final String KEY_VENDOR_UUID = "vendor_uuid"; - public static final String KEY_KEYPHRASE_ID = "keyphrase_id"; - public static final String KEY_TYPE = "type"; - public static final String KEY_DATA = "data"; - public static final String KEY_RECOGNITION_MODES = "recognition_modes"; - public static final String KEY_LOCALE = "locale"; - public static final String KEY_HINT_TEXT = "hint_text"; - public static final String KEY_USERS = "users"; - public static final String KEY_MODEL_VERSION = "model_version"; + /** + * Keyphrase sound model database columns + */ + public interface SoundModelContract { + String TABLE = "sound_model"; + String KEY_MODEL_UUID = "model_uuid"; + String KEY_VENDOR_UUID = "vendor_uuid"; + String KEY_KEYPHRASE_ID = "keyphrase_id"; + String KEY_TYPE = "type"; + String KEY_DATA = "data"; + String KEY_RECOGNITION_MODES = "recognition_modes"; + String KEY_LOCALE = "locale"; + String KEY_HINT_TEXT = "hint_text"; + String KEY_USERS = "users"; + String KEY_MODEL_VERSION = "model_version"; } // Table Create Statement @@ -173,7 +176,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { soundModel.keyphrases[0].recognitionModes); values.put(SoundModelContract.KEY_USERS, getCommaSeparatedString(soundModel.keyphrases[0].users)); - values.put(SoundModelContract.KEY_LOCALE, soundModel.keyphrases[0].locale); + values.put(SoundModelContract.KEY_LOCALE, + soundModel.keyphrases[0].locale.toLanguageTag()); values.put(SoundModelContract.KEY_HINT_TEXT, soundModel.keyphrases[0].text); try { return db.insertWithOnConflict(SoundModelContract.TABLE, null, values, @@ -257,8 +261,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES)); int[] users = getArrayForCommaSeparatedString( c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS))); - String modelLocale = c.getString( - c.getColumnIndex(SoundModelContract.KEY_LOCALE)); + Locale modelLocale = Locale.forLanguageTag(c.getString( + c.getColumnIndex(SoundModelContract.KEY_LOCALE))); String text = c.getString( c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT)); int version = c.getInt( @@ -431,8 +435,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { } } + /** + * Dumps contents of database for dumpsys + */ public void dump(PrintWriter pw) { - synchronized(this) { + synchronized (this) { String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE; SQLiteDatabase db = getReadableDatabase(); Cursor c = db.rawQuery(selectQuery, null); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 506c67e12528..ec0a1bacf094 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -923,6 +923,8 @@ public class VoiceInteractionManagerService extends SystemService { } //----------------- Model management APIs --------------------------------// + // TODO: add check to only allow active voice interaction service or keyphrase enrollment + // application to manage voice models @Override public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, String bcp47Locale) { diff --git a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java index 65a3d8a337db..c900eaedbdae 100644 --- a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java +++ b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java @@ -30,6 +30,7 @@ import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; import java.util.Arrays; +import java.util.Locale; import java.util.Random; import java.util.UUID; @@ -38,7 +39,8 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseParcelUnparcel_noUsers() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", null); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", null); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -57,7 +59,8 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseParcelUnparcel_zeroUsers() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[0]); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", new int[0]); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -76,7 +79,8 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseParcelUnparcel_pos() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[] {1, 2, 3, 4, 5}); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", new int[] {1, 2, 3, 4, 5}); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -96,8 +100,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseSoundModelParcelUnparcel_noData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), null, keyphrases); @@ -119,8 +125,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseSoundModelParcelUnparcel_zeroData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), new byte[0], keyphrases); @@ -186,8 +194,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { @LargeTest public void testKeyphraseSoundModelParcelUnparcel_largeData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); byte[] data = new byte[200 * 1024]; mRandom.nextBytes(data); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java index 54c944f9588e..b357ad076c11 100644 --- a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java +++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java @@ -16,9 +16,6 @@ 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; @@ -29,6 +26,13 @@ import android.util.Log; import android.view.View; import android.widget.Toast; +import java.util.Locale; +import java.util.Random; +import java.util.UUID; + +/** + * TODO: must be transitioned to a service. + */ public class TestEnrollmentActivity extends Activity { private static final String TAG = "TestEnrollmentActivity"; private static final boolean DBG = false; @@ -56,7 +60,8 @@ public class TestEnrollmentActivity extends Activity { * Performs a fresh enrollment. */ public void onEnrollButtonClicked(View v) { - Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES, BCP47_LOCALE, TEXT, + Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES, + Locale.forLanguageTag(BCP47_LOCALE), TEXT, new int[] { UserManager.get(this).getUserHandle() /* current user */}); UUID modelUuid = UUID.randomUUID(); // Generate a fake model to push. |