Merge changes from topic "voiceinteraction-model-enrollment"

* changes:
  async enrollment support AlwaysOnHotwordDetector
  add KeyphraseModelManager
This commit is contained in:
TreeHugger Robot
2020-01-23 02:46:50 +00:00
committed by Android (Google) Code Review
15 changed files with 814 additions and 285 deletions

View File

@@ -43525,7 +43525,7 @@ package android.service.voice {
field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
field public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
field @Deprecated public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
}
public abstract static class AlwaysOnHotwordDetector.Callback {

View File

@@ -3636,9 +3636,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;
@@ -3677,6 +3702,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 {
@@ -5881,6 +5916,16 @@ package android.media.tv.tuner.frontend {
}
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 {
@@ -10463,6 +10508,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 {

View File

@@ -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;
}

View File

@@ -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";
@@ -85,6 +88,31 @@ public class KeyphraseEnrollmentInfo {
public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
"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.
*/
@@ -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;

View File

@@ -0,0 +1,19 @@
/**
* 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 android.hardware.soundtrigger;
parcelable KeyphraseMetadata;

View File

@@ -16,8 +16,13 @@
package android.hardware.soundtrigger;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcelable;
import android.util.ArraySet;
import com.android.internal.util.DataClass;
import java.util.Locale;
/**
@@ -25,37 +30,168 @@ import java.util.Locale;
*
* @hide
*/
public class KeyphraseMetadata {
@DataClass(
genEqualsHashCode = true,
genToString = true,
genConstructor = false,
genHiddenConstDefs = true)
public final class KeyphraseMetadata implements Parcelable {
public final int id;
@NonNull
public final String keyphrase;
@NonNull
public final ArraySet<Locale> supportedLocales;
public final int recognitionModeFlags;
public KeyphraseMetadata(int id, String keyphrase, ArraySet<Locale> supportedLocales,
int recognitionModeFlags) {
public KeyphraseMetadata(int id, @NonNull String keyphrase,
@NonNull ArraySet<Locale> supportedLocales, int recognitionModeFlags) {
this.id = id;
this.keyphrase = keyphrase;
this.supportedLocales = supportedLocales;
this.recognitionModeFlags = recognitionModeFlags;
}
@Override
public String toString() {
return "id=" + id + ", keyphrase=" + keyphrase + ", supported-locales=" + supportedLocales
+ ", recognition-modes=" + recognitionModeFlags;
}
/**
* @return Indicates if we support the given phrase.
*/
public boolean supportsPhrase(String phrase) {
public boolean supportsPhrase(@Nullable String phrase) {
return keyphrase.isEmpty() || keyphrase.equalsIgnoreCase(phrase);
}
/**
* @return Indicates if we support the given locale.
*/
public boolean supportsLocale(Locale locale) {
public boolean supportsLocale(@Nullable Locale locale) {
return supportedLocales.isEmpty() || supportedLocales.contains(locale);
}
// Code below generated by codegen v1.0.14.
//
// DO NOT MODIFY!
// CHECKSTYLE:OFF Generated code
//
// To regenerate run:
// $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java
//
// To exclude the generated code from IntelliJ auto-formatting enable (one-time):
// Settings > Editor > Code Style > Formatter Control
//@formatter:off
@Override
@DataClass.Generated.Member
public String toString() {
// You can override field toString logic by defining methods like:
// String fieldNameToString() { ... }
return "KeyphraseMetadata { " +
"id = " + id + ", " +
"keyphrase = " + keyphrase + ", " +
"supportedLocales = " + supportedLocales + ", " +
"recognitionModeFlags = " + recognitionModeFlags +
" }";
}
@Override
@DataClass.Generated.Member
public boolean equals(@Nullable Object o) {
// You can override field equality logic by defining either of the methods like:
// boolean fieldNameEquals(KeyphraseMetadata other) { ... }
// boolean fieldNameEquals(FieldType otherValue) { ... }
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
@SuppressWarnings("unchecked")
KeyphraseMetadata that = (KeyphraseMetadata) o;
//noinspection PointlessBooleanExpression
return true
&& id == that.id
&& java.util.Objects.equals(keyphrase, that.keyphrase)
&& java.util.Objects.equals(supportedLocales, that.supportedLocales)
&& recognitionModeFlags == that.recognitionModeFlags;
}
@Override
@DataClass.Generated.Member
public int hashCode() {
// You can override field hashCode logic by defining methods like:
// int fieldNameHashCode() { ... }
int _hash = 1;
_hash = 31 * _hash + id;
_hash = 31 * _hash + java.util.Objects.hashCode(keyphrase);
_hash = 31 * _hash + java.util.Objects.hashCode(supportedLocales);
_hash = 31 * _hash + recognitionModeFlags;
return _hash;
}
@Override
@DataClass.Generated.Member
public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
// You can override field parcelling by defining methods like:
// void parcelFieldName(Parcel dest, int flags) { ... }
dest.writeInt(id);
dest.writeString(keyphrase);
dest.writeArraySet(supportedLocales);
dest.writeInt(recognitionModeFlags);
}
@Override
@DataClass.Generated.Member
public int describeContents() { return 0; }
/** @hide */
@SuppressWarnings({"unchecked", "RedundantCast"})
@DataClass.Generated.Member
/* package-private */ KeyphraseMetadata(@NonNull android.os.Parcel in) {
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
int _id = in.readInt();
String _keyphrase = in.readString();
ArraySet<Locale> _supportedLocales = (ArraySet) in.readArraySet(null);
int _recognitionModeFlags = in.readInt();
this.id = _id;
this.keyphrase = _keyphrase;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, keyphrase);
this.supportedLocales = _supportedLocales;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, supportedLocales);
this.recognitionModeFlags = _recognitionModeFlags;
// onConstructed(); // You can define this method to get a callback
}
@DataClass.Generated.Member
public static final @NonNull Parcelable.Creator<KeyphraseMetadata> CREATOR
= new Parcelable.Creator<KeyphraseMetadata>() {
@Override
public KeyphraseMetadata[] newArray(int size) {
return new KeyphraseMetadata[size];
}
@Override
public KeyphraseMetadata createFromParcel(@NonNull android.os.Parcel in) {
return new KeyphraseMetadata(in);
}
};
@DataClass.Generated(
time = 1579290593964L,
codegenVersion = "1.0.14",
sourceFile = "frameworks/base/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java",
inputSignatures = "public final int id\npublic final @android.annotation.NonNull java.lang.String keyphrase\npublic final @android.annotation.NonNull android.util.ArraySet<java.util.Locale> supportedLocales\npublic final int recognitionModeFlags\npublic boolean supportsPhrase(java.lang.String)\npublic boolean supportsLocale(java.util.Locale)\nclass KeyphraseMetadata extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genConstructor=false, genHiddenConstDefs=true)")
@Deprecated
private void __metadata() {}
//@formatter:on
// End of generated code
}

View File

@@ -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;

View File

@@ -29,7 +29,6 @@ import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat;
@@ -67,7 +66,12 @@ public class AlwaysOnHotwordDetector {
/**
* Indicates that recognition for the given keyphrase is not supported.
* No further interaction should be performed with the detector that returns this availability.
*
* @deprecated This is no longer a valid state. Enrollment can occur outside of
* {@link KeyphraseEnrollmentInfo} through another privileged application. We can no longer
* determine ahead of time if the keyphrase and locale are unsupported by the system.
*/
@Deprecated
public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
/**
* Indicates that the given keyphrase is not enrolled.
@@ -85,34 +89,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)
@@ -248,7 +224,8 @@ public class AlwaysOnHotwordDetector {
* The metadata of the Keyphrase, derived from the enrollment application.
* This may be null if this keyphrase isn't supported by the enrollment application.
*/
private final KeyphraseMetadata mKeyphraseMetadata;
@Nullable
private KeyphraseMetadata mKeyphraseMetadata;
private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
private final IVoiceInteractionService mVoiceInteractionService;
private final IVoiceInteractionManagerService mModelManagementService;
@@ -448,7 +425,6 @@ public class AlwaysOnHotwordDetector {
mText = text;
mLocale = locale;
mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
mExternalCallback = callback;
mHandler = new MyHandler();
mInternalCallback = new SoundTriggerListener(mHandler);
@@ -484,8 +460,7 @@ public class AlwaysOnHotwordDetector {
}
// This method only makes sense if we can actually support a recognition.
if (mAvailability != STATE_KEYPHRASE_ENROLLED
&& mAvailability != STATE_KEYPHRASE_UNENROLLED) {
if (mAvailability != STATE_KEYPHRASE_ENROLLED || mKeyphraseMetadata == null) {
throw new UnsupportedOperationException(
"Getting supported recognition modes for the keyphrase is not supported");
}
@@ -679,7 +654,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 +675,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 +696,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");
}
@@ -761,8 +736,7 @@ public class AlwaysOnHotwordDetector {
void onSoundModelsChanged() {
synchronized (mLock) {
if (mAvailability == STATE_INVALID
|| mAvailability == STATE_HARDWARE_UNAVAILABLE
|| mAvailability == STATE_KEYPHRASE_UNSUPPORTED) {
|| mAvailability == STATE_HARDWARE_UNAVAILABLE) {
Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
return;
}
@@ -772,7 +746,9 @@ public class AlwaysOnHotwordDetector {
// or was deleted.
// The availability change callback should ensure that the client starts recognition
// again if needed.
stopRecognitionLocked();
if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
stopRecognitionLocked();
}
// Execute a refresh availability task - which should then notify of a change.
new RefreshAvailabiltyTask().execute();
@@ -955,20 +931,17 @@ public class AlwaysOnHotwordDetector {
@Override
public Void doInBackground(Void... params) {
int availability = internalGetInitialAvailability();
boolean enrolled = false;
// Fetch the sound model if the availability is one of the supported ones.
if (availability == STATE_NOT_READY
|| availability == STATE_KEYPHRASE_UNENROLLED
|| availability == STATE_KEYPHRASE_ENROLLED) {
enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id, mLocale);
if (!enrolled) {
availability = STATE_KEYPHRASE_UNENROLLED;
} else {
availability = STATE_KEYPHRASE_ENROLLED;
}
}
synchronized (mLock) {
if (availability == STATE_NOT_READY) {
internalUpdateEnrolledKeyphraseMetadata();
if (mKeyphraseMetadata != null) {
availability = STATE_KEYPHRASE_ENROLLED;
} else {
availability = STATE_KEYPHRASE_UNENROLLED;
}
}
if (DBG) {
Slog.d(TAG, "Hotword availability changed from " + mAvailability
+ " -> " + availability);
@@ -997,28 +970,22 @@ public class AlwaysOnHotwordDetector {
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in getDspProperties!", e);
}
// No DSP available
if (dspModuleProperties == null) {
return STATE_HARDWARE_UNAVAILABLE;
}
// No enrollment application supports this keyphrase/locale
if (mKeyphraseMetadata == null) {
return STATE_KEYPHRASE_UNSUPPORTED;
}
return STATE_NOT_READY;
}
/**
* @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
*/
private boolean internalGetIsEnrolled(int keyphraseId, Locale locale) {
private void internalUpdateEnrolledKeyphraseMetadata() {
try {
return mModelManagementService.isEnrolledForKeyphrase(
mVoiceInteractionService, keyphraseId, locale.toLanguageTag());
mKeyphraseMetadata = mModelManagementService.getEnrolledKeyphraseMetadata(
mVoiceInteractionService, mText, mLocale.toLanguageTag());
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e);
Slog.w(TAG, "RemoteException in internalUpdateEnrolledKeyphraseMetadata", e);
}
return false;
}
}

View File

@@ -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;
@@ -303,6 +307,23 @@ public class VoiceInteractionService extends Service {
return mHotwordDetector;
}
/**
* 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

View File

@@ -26,6 +26,7 @@ import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.IVoiceInteractionSessionListener;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.KeyphraseMetadata;
import android.hardware.soundtrigger.ModelParams;
import android.hardware.soundtrigger.SoundTrigger;
import android.service.voice.IVoiceInteractionService;
@@ -72,8 +73,8 @@ interface IVoiceInteractionManagerService {
*/
SoundTrigger.ModuleProperties getDspModuleProperties(in IVoiceInteractionService service);
/**
* Indicates if there's a keyphrase sound model available for the given keyphrase ID.
* This performs the check for the current user.
* Indicates if there's a keyphrase sound model available for the given keyphrase ID and the
* user ID of the caller.
*
* @param service The current VoiceInteractionService.
* @param keyphraseId The unique identifier for the keyphrase.
@@ -81,6 +82,18 @@ interface IVoiceInteractionManagerService {
*/
boolean isEnrolledForKeyphrase(IVoiceInteractionService service, int keyphraseId,
String bcp47Locale);
/**
* Generates KeyphraseMetadata for an enrolled sound model based on keyphrase string, locale,
* and the user ID of the caller.
*
* @param service The current VoiceInteractionService
* @param keyphrase Keyphrase text associated with the enrolled model
* @param bcp47Locale The BCP47 language tag for the keyphrase's locale.
* @return The metadata for the enrolled voice model bassed on the passed in parameters. Null if
* no matching voice model exists.
*/
KeyphraseMetadata getEnrolledKeyphraseMetadata(IVoiceInteractionService service,
String keyphrase, String bcp47Locale);
/**
* Starts a recognition for the given keyphrase.
*/

View File

@@ -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();
}
}
}

View File

@@ -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,
@@ -190,7 +194,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
* Deletes the sound model and associated keyphrases.
*/
public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) {
// Sanitize the locale to guard against SQL injection.
// Normalize the locale to guard against SQL injection.
bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
synchronized(this) {
KeyphraseSoundModel soundModel = getKeyphraseSoundModel(keyphraseId, userHandle,
@@ -226,92 +230,119 @@ public class DatabaseHelper extends SQLiteOpenHelper {
String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE
+ " WHERE " + SoundModelContract.KEY_KEYPHRASE_ID + "= '" + keyphraseId
+ "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'";
SQLiteDatabase db = getReadableDatabase();
Cursor c = db.rawQuery(selectQuery, null);
try {
if (c.moveToFirst()) {
do {
int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) {
if (DBG) {
Slog.w(TAG, "Ignoring SoundModel since it's type is incorrect");
}
continue;
}
String modelUuid = c.getString(
c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
if (modelUuid == null) {
Slog.w(TAG, "Ignoring SoundModel since it doesn't specify an ID");
continue;
}
String vendorUuidString = null;
int vendorUuidColumn = c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID);
if (vendorUuidColumn != -1) {
vendorUuidString = c.getString(vendorUuidColumn);
}
byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
int recognitionModes = c.getInt(
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));
String text = c.getString(
c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT));
int version = c.getInt(
c.getColumnIndex(SoundModelContract.KEY_MODEL_VERSION));
// Only add keyphrases meant for the current user.
if (users == null) {
// No users present in the keyphrase.
Slog.w(TAG, "Ignoring SoundModel since it doesn't specify users");
continue;
}
boolean isAvailableForCurrentUser = false;
for (int user : users) {
if (userHandle == user) {
isAvailableForCurrentUser = true;
break;
}
}
if (!isAvailableForCurrentUser) {
if (DBG) {
Slog.w(TAG, "Ignoring SoundModel since user handles don't match");
}
continue;
} else {
if (DBG) Slog.d(TAG, "Found a SoundModel for user: " + userHandle);
}
Keyphrase[] keyphrases = new Keyphrase[1];
keyphrases[0] = new Keyphrase(
keyphraseId, recognitionModes, modelLocale, text, users);
UUID vendorUuid = null;
if (vendorUuidString != null) {
vendorUuid = UUID.fromString(vendorUuidString);
}
KeyphraseSoundModel model = new KeyphraseSoundModel(
UUID.fromString(modelUuid), vendorUuid, data, keyphrases, version);
if (DBG) {
Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: "
+ model);
}
return model;
} while (c.moveToNext());
}
Slog.w(TAG, "No SoundModel available for the given keyphrase");
} finally {
c.close();
db.close();
}
return null;
return getValidKeyphraseSoundModelForUser(selectQuery, userHandle);
}
}
/**
* Returns a matching {@link KeyphraseSoundModel} for the keyphrase string.
* Returns null if a match isn't found.
*
* TODO: We only support one keyphrase currently.
*/
public KeyphraseSoundModel getKeyphraseSoundModel(String keyphrase, int userHandle,
String bcp47Locale) {
// Sanitize the locale to guard against SQL injection.
bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
synchronized (this) {
// Find the corresponding sound model ID for the keyphrase.
String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE
+ " WHERE " + SoundModelContract.KEY_HINT_TEXT + "= '" + keyphrase
+ "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'";
return getValidKeyphraseSoundModelForUser(selectQuery, userHandle);
}
}
private KeyphraseSoundModel getValidKeyphraseSoundModelForUser(String selectQuery,
int userHandle) {
SQLiteDatabase db = getReadableDatabase();
Cursor c = db.rawQuery(selectQuery, null);
try {
if (c.moveToFirst()) {
do {
int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) {
if (DBG) {
Slog.w(TAG, "Ignoring SoundModel since its type is incorrect");
}
continue;
}
String modelUuid = c.getString(
c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
if (modelUuid == null) {
Slog.w(TAG, "Ignoring SoundModel since it doesn't specify an ID");
continue;
}
String vendorUuidString = null;
int vendorUuidColumn = c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID);
if (vendorUuidColumn != -1) {
vendorUuidString = c.getString(vendorUuidColumn);
}
int keyphraseId = c.getInt(
c.getColumnIndex(SoundModelContract.KEY_KEYPHRASE_ID));
byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
int recognitionModes = c.getInt(
c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
int[] users = getArrayForCommaSeparatedString(
c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS)));
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(
c.getColumnIndex(SoundModelContract.KEY_MODEL_VERSION));
// Only add keyphrases meant for the current user.
if (users == null) {
// No users present in the keyphrase.
Slog.w(TAG, "Ignoring SoundModel since it doesn't specify users");
continue;
}
boolean isAvailableForCurrentUser = false;
for (int user : users) {
if (userHandle == user) {
isAvailableForCurrentUser = true;
break;
}
}
if (!isAvailableForCurrentUser) {
if (DBG) {
Slog.w(TAG, "Ignoring SoundModel since user handles don't match");
}
continue;
} else {
if (DBG) Slog.d(TAG, "Found a SoundModel for user: " + userHandle);
}
Keyphrase[] keyphrases = new Keyphrase[1];
keyphrases[0] = new Keyphrase(
keyphraseId, recognitionModes, modelLocale, text, users);
UUID vendorUuid = null;
if (vendorUuidString != null) {
vendorUuid = UUID.fromString(vendorUuidString);
}
KeyphraseSoundModel model = new KeyphraseSoundModel(
UUID.fromString(modelUuid), vendorUuid, data, keyphrases, version);
if (DBG) {
Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: "
+ model);
}
return model;
} while (c.moveToNext());
}
Slog.w(TAG, "No SoundModel available for the given keyphrase");
} finally {
c.close();
db.close();
}
return null;
}
private static String getCommaSeparatedString(int[] users) {
if (users == null) {
return "";
@@ -431,8 +462,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);

View File

@@ -41,7 +41,9 @@ import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.KeyphraseMetadata;
import android.hardware.soundtrigger.ModelParams;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModelParamRange;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
@@ -90,6 +92,7 @@ import com.android.server.wm.ActivityTaskManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -923,6 +926,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) {
@@ -1022,6 +1027,41 @@ public class VoiceInteractionManagerService extends SystemService {
}
}
@Nullable
public KeyphraseMetadata getEnrolledKeyphraseMetadata(IVoiceInteractionService service,
String keyphrase, String bcp47Locale) {
synchronized (this) {
enforceIsCurrentVoiceInteractionService(service);
}
if (bcp47Locale == null) {
throw new IllegalArgumentException("Illegal argument(s) in isEnrolledForKeyphrase");
}
final int callingUid = UserHandle.getCallingUserId();
final long caller = Binder.clearCallingIdentity();
try {
KeyphraseSoundModel model =
mDbHelper.getKeyphraseSoundModel(keyphrase, callingUid, bcp47Locale);
if (model == null) {
return null;
}
for (SoundTrigger.Keyphrase phrase : model.keyphrases) {
if (keyphrase.equals(phrase.text)) {
ArraySet<Locale> locales = new ArraySet<>();
locales.add(phrase.locale);
return new KeyphraseMetadata(phrase.id, phrase.text, locales,
phrase.recognitionModes);
}
}
} finally {
Binder.restoreCallingIdentity(caller);
}
return null;
}
@Override
public ModuleProperties getDspModuleProperties(IVoiceInteractionService service) {
// Allow the call if this is the current voice interaction service.

View File

@@ -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(),

View File

@@ -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.