summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicholas Ambur <nambur@google.com>2020-01-14 20:28:37 -0800
committerNicholas Ambur <nambur@google.com>2020-01-22 16:40:13 -0800
commitef84fc48433d47ea9c91dcb3273ae3d74ca6d32a (patch)
tree7ae618ac7d63eb705ca913274d800c38cbc98b58
parent4ec0be1fafd70ff43f5c579c73627b147168b194 (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
-rwxr-xr-xapi/system-current.txt53
-rw-r--r--core/java/android/hardware/soundtrigger/ConversionUtil.java2
-rw-r--r--core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java42
-rw-r--r--core/java/android/hardware/soundtrigger/SoundTrigger.java235
-rw-r--r--core/java/android/service/voice/AlwaysOnHotwordDetector.java36
-rw-r--r--core/java/android/service/voice/VoiceInteractionService.java21
-rw-r--r--media/java/android/media/voice/KeyphraseModelManager.java144
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java39
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java2
-rw-r--r--tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java28
-rw-r--r--tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java13
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.