/** * 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; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; import static android.Manifest.permission.RECORD_AUDIO; import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY; import static android.system.OsConstants.EBUSY; import static android.system.OsConstants.EINVAL; import static android.system.OsConstants.ENODEV; import static android.system.OsConstants.ENOSYS; import static android.system.OsConstants.EPERM; import static android.system.OsConstants.EPIPE; import static java.util.Objects.requireNonNull; import android.annotation.ElapsedRealtimeLong; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.media.AudioFormat; import android.media.permission.Identity; import android.media.soundtrigger.Status; import android.media.soundtrigger_middleware.ISoundTriggerInjection; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.os.SystemClock; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Locale; import java.util.Objects; import java.util.UUID; /** * The SoundTrigger class provides access to the service managing the sound trigger HAL. * * @hide */ @SystemApi public class SoundTrigger { private static final String TAG = "SoundTrigger"; private SoundTrigger() { } /** * Model architecture associated with a fake STHAL which can be injected. * Used for testing purposes. * @hide */ public static final String FAKE_HAL_ARCH = ISoundTriggerInjection.FAKE_HAL_ARCH; /** * Status code used when the operation succeeded */ public static final int STATUS_OK = 0; /** @hide */ public static final int STATUS_ERROR = Integer.MIN_VALUE; /** @hide */ public static final int STATUS_PERMISSION_DENIED = -EPERM; /** @hide */ public static final int STATUS_NO_INIT = -ENODEV; /** @hide */ public static final int STATUS_BAD_VALUE = -EINVAL; /** @hide */ public static final int STATUS_DEAD_OBJECT = -EPIPE; /** @hide */ public static final int STATUS_INVALID_OPERATION = -ENOSYS; /** @hide */ public static final int STATUS_BUSY = -EBUSY; /***************************************************************************** * A ModuleProperties describes a given sound trigger hardware module * managed by the native sound trigger service. Each module has a unique * ID used to target any API call to this paricular module. Module * properties are returned by listModules() method. * ****************************************************************************/ public static final class ModuleProperties implements Parcelable { /** * Bit field values of AudioCapabilities supported by the implemented HAL * driver. * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "AUDIO_CAPABILITY_" }, value = { AUDIO_CAPABILITY_ECHO_CANCELLATION, AUDIO_CAPABILITY_NOISE_SUPPRESSION }) public @interface AudioCapabilities {} /** * If set the underlying module supports AEC. * Describes bit field {@link ModuleProperties#mAudioCapabilities} */ public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 0x1; /** * If set, the underlying module supports noise suppression. * Describes bit field {@link ModuleProperties#mAudioCapabilities} */ public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 0x2; private final int mId; @NonNull private final String mImplementor; @NonNull private final String mDescription; @NonNull private final UUID mUuid; private final int mVersion; @NonNull private final String mSupportedModelArch; private final int mMaxSoundModels; private final int mMaxKeyphrases; private final int mMaxUsers; @RecognitionModes private final int mRecognitionModes; private final boolean mSupportsCaptureTransition; private final int mMaxBufferMillis; private final boolean mSupportsConcurrentCapture; private final int mPowerConsumptionMw; private final boolean mReturnsTriggerInEvent; @AudioCapabilities private final int mAudioCapabilities; /** @hide */ @TestApi public ModuleProperties(int id, @NonNull String implementor, @NonNull String description, @NonNull String uuid, int version, @NonNull String supportedModelArch, int maxSoundModels, int maxKeyphrases, int maxUsers, @RecognitionModes int recognitionModes, boolean supportsCaptureTransition, int maxBufferMs, boolean supportsConcurrentCapture, int powerConsumptionMw, boolean returnsTriggerInEvent, int audioCapabilities) { this.mId = id; this.mImplementor = requireNonNull(implementor); this.mDescription = requireNonNull(description); this.mUuid = UUID.fromString(requireNonNull(uuid)); this.mVersion = version; this.mSupportedModelArch = requireNonNull(supportedModelArch); this.mMaxSoundModels = maxSoundModels; this.mMaxKeyphrases = maxKeyphrases; this.mMaxUsers = maxUsers; this.mRecognitionModes = recognitionModes; this.mSupportsCaptureTransition = supportsCaptureTransition; this.mMaxBufferMillis = maxBufferMs; this.mSupportsConcurrentCapture = supportsConcurrentCapture; this.mPowerConsumptionMw = powerConsumptionMw; this.mReturnsTriggerInEvent = returnsTriggerInEvent; this.mAudioCapabilities = audioCapabilities; } /** Unique module ID provided by the native service */ public int getId() { return mId; } /** human readable voice detection engine implementor */ @NonNull public String getImplementor() { return mImplementor; } /** human readable voice detection engine description */ @NonNull public String getDescription() { return mDescription; } /** Unique voice engine Id (changes with each version) */ @NonNull public UUID getUuid() { return mUuid; } /** Voice detection engine version */ public int getVersion() { return mVersion; } /** * String naming the architecture used for running the supported models. * (eg. a platform running models on a DSP could implement this string to convey the DSP * architecture used) */ @NonNull public String getSupportedModelArch() { return mSupportedModelArch; } /** Maximum number of active sound models */ public int getMaxSoundModels() { return mMaxSoundModels; } /** Maximum number of key phrases */ public int getMaxKeyphrases() { return mMaxKeyphrases; } /** Maximum number of users per key phrase */ public int getMaxUsers() { return mMaxUsers; } /** Supported recognition modes (bit field, RECOGNITION_MODE_VOICE_TRIGGER ...) */ @RecognitionModes public int getRecognitionModes() { return mRecognitionModes; } /** Supports seamless transition to capture mode after recognition */ public boolean isCaptureTransitionSupported() { return mSupportsCaptureTransition; } /** Maximum buffering capacity in ms if supportsCaptureTransition() is true */ public int getMaxBufferMillis() { return mMaxBufferMillis; } /** Supports capture by other use cases while detection is active */ public boolean isConcurrentCaptureSupported() { return mSupportsConcurrentCapture; } /** Rated power consumption when detection is active with TDB silence/sound/speech ratio */ public int getPowerConsumptionMw() { return mPowerConsumptionMw; } /** Returns the trigger (key phrase) capture in the binary data of the * recognition callback event */ public boolean isTriggerReturnedInEvent() { return mReturnsTriggerInEvent; } /** * Bit field encoding of the AudioCapabilities * supported by the firmware. */ @AudioCapabilities public int getAudioCapabilities() { return mAudioCapabilities; } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public ModuleProperties createFromParcel(Parcel in) { return ModuleProperties.fromParcel(in); } public ModuleProperties[] newArray(int size) { return new ModuleProperties[size]; } }; private static ModuleProperties fromParcel(Parcel in) { int id = in.readInt(); String implementor = in.readString(); String description = in.readString(); String uuid = in.readString(); int version = in.readInt(); String supportedModelArch = in.readString(); int maxSoundModels = in.readInt(); int maxKeyphrases = in.readInt(); int maxUsers = in.readInt(); int recognitionModes = in.readInt(); boolean supportsCaptureTransition = in.readByte() == 1; int maxBufferMs = in.readInt(); boolean supportsConcurrentCapture = in.readByte() == 1; int powerConsumptionMw = in.readInt(); boolean returnsTriggerInEvent = in.readByte() == 1; int audioCapabilities = in.readInt(); return new ModuleProperties(id, implementor, description, uuid, version, supportedModelArch, maxSoundModels, maxKeyphrases, maxUsers, recognitionModes, supportsCaptureTransition, maxBufferMs, supportsConcurrentCapture, powerConsumptionMw, returnsTriggerInEvent, audioCapabilities); } @Override public void writeToParcel(@SuppressLint("MissingNullability") Parcel dest, int flags) { dest.writeInt(getId()); dest.writeString(getImplementor()); dest.writeString(getDescription()); dest.writeString(getUuid().toString()); dest.writeInt(getVersion()); dest.writeString(getSupportedModelArch()); dest.writeInt(getMaxSoundModels()); dest.writeInt(getMaxKeyphrases()); dest.writeInt(getMaxUsers()); dest.writeInt(getRecognitionModes()); dest.writeByte((byte) (isCaptureTransitionSupported() ? 1 : 0)); dest.writeInt(getMaxBufferMillis()); dest.writeByte((byte) (isConcurrentCaptureSupported() ? 1 : 0)); dest.writeInt(getPowerConsumptionMw()); dest.writeByte((byte) (isTriggerReturnedInEvent() ? 1 : 0)); dest.writeInt(getAudioCapabilities()); } @Override public int describeContents() { return 0; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof ModuleProperties)) { return false; } ModuleProperties other = (ModuleProperties) obj; if (mId != other.mId) { return false; } if (!mImplementor.equals(other.mImplementor)) { return false; } if (!mDescription.equals(other.mDescription)) { return false; } if (!mUuid.equals(other.mUuid)) { return false; } if (mVersion != other.mVersion) { return false; } if (!mSupportedModelArch.equals(other.mSupportedModelArch)) { return false; } if (mMaxSoundModels != other.mMaxSoundModels) { return false; } if (mMaxKeyphrases != other.mMaxKeyphrases) { return false; } if (mMaxUsers != other.mMaxUsers) { return false; } if (mRecognitionModes != other.mRecognitionModes) { return false; } if (mSupportsCaptureTransition != other.mSupportsCaptureTransition) { return false; } if (mMaxBufferMillis != other.mMaxBufferMillis) { return false; } if (mSupportsConcurrentCapture != other.mSupportsConcurrentCapture) { return false; } if (mPowerConsumptionMw != other.mPowerConsumptionMw) { return false; } if (mReturnsTriggerInEvent != other.mReturnsTriggerInEvent) { return false; } if (mAudioCapabilities != other.mAudioCapabilities) { return false; } return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + mId; result = prime * result + mImplementor.hashCode(); result = prime * result + mDescription.hashCode(); result = prime * result + mUuid.hashCode(); result = prime * result + mVersion; result = prime * result + mSupportedModelArch.hashCode(); result = prime * result + mMaxSoundModels; result = prime * result + mMaxKeyphrases; result = prime * result + mMaxUsers; result = prime * result + mRecognitionModes; result = prime * result + (mSupportsCaptureTransition ? 1 : 0); result = prime * result + mMaxBufferMillis; result = prime * result + (mSupportsConcurrentCapture ? 1 : 0); result = prime * result + mPowerConsumptionMw; result = prime * result + (mReturnsTriggerInEvent ? 1 : 0); result = prime * result + mAudioCapabilities; return result; } @Override public String toString() { return "ModuleProperties [id=" + getId() + ", implementor=" + getImplementor() + ", description=" + getDescription() + ", uuid=" + getUuid() + ", version=" + getVersion() + " , supportedModelArch=" + getSupportedModelArch() + ", maxSoundModels=" + getMaxSoundModels() + ", maxKeyphrases=" + getMaxKeyphrases() + ", maxUsers=" + getMaxUsers() + ", recognitionModes=" + getRecognitionModes() + ", supportsCaptureTransition=" + isCaptureTransitionSupported() + ", maxBufferMs=" + getMaxBufferMillis() + ", supportsConcurrentCapture=" + isConcurrentCaptureSupported() + ", powerConsumptionMw=" + getPowerConsumptionMw() + ", returnsTriggerInEvent=" + isTriggerReturnedInEvent() + ", audioCapabilities=" + getAudioCapabilities() + "]"; } } /** * 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. */ public static class SoundModel { /** @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 */ public static final int TYPE_KEYPHRASE = 0; /** * A generic sound model. Use this type only for non-keyphrase sound models such as * ones that match a particular sound pattern. */ public static final int TYPE_GENERIC_SOUND = 1; @NonNull private final UUID mUuid; @SoundModelType private final int mType; @NonNull private final UUID mVendorUuid; private final int mVersion; @NonNull private final byte[] mData; /** @hide */ public SoundModel(@NonNull UUID uuid, @Nullable UUID vendorUuid, @SoundModelType int type, @Nullable byte[] data, int version) { this.mUuid = requireNonNull(uuid); this.mVendorUuid = vendorUuid != null ? vendorUuid : new UUID(0, 0); this.mType = type; this.mVersion = version; this.mData = data != null ? data : new byte[0]; } /** Unique sound model identifier */ @NonNull public UUID getUuid() { return mUuid; } /** Sound model type (e.g. TYPE_KEYPHRASE); */ @SoundModelType public int getType() { return mType; } /** Unique sound model vendor identifier */ @NonNull public UUID getVendorUuid() { return mVendorUuid; } /** vendor specific version number of the model */ public int getVersion() { return mVersion; } /** Opaque data. For use by vendor implementation and enrollment application */ @NonNull public byte[] getData() { return mData; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getVersion(); result = prime * result + Arrays.hashCode(getData()); result = prime * result + getType(); result = prime * result + ((getUuid() == null) ? 0 : getUuid().hashCode()); result = prime * result + ((getVendorUuid() == null) ? 0 : getVendorUuid().hashCode()); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof SoundModel)) { return false; } SoundModel other = (SoundModel) obj; if (getType() != other.getType()) { return false; } if (getUuid() == null) { if (other.getUuid() != null) { return false; } } else if (!getUuid().equals(other.getUuid())) { return false; } if (getVendorUuid() == null) { if (other.getVendorUuid() != null) { return false; } } else if (!getVendorUuid().equals(other.getVendorUuid())) { return false; } if (!Arrays.equals(getData(), other.getData())) { return false; } if (getVersion() != other.getVersion()) { return false; } return true; } } /** * A Keyphrase describes a key phrase that can be detected by a * {@link KeyphraseSoundModel} */ public static final class Keyphrase implements Parcelable { private final int mId; @RecognitionModes private final int mRecognitionModes; @NonNull private final Locale mLocale; @NonNull private final String mText; @NonNull private final int[] mUsers; /** * 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.mId = id; this.mRecognitionModes = recognitionModes; this.mLocale = requireNonNull(locale); this.mText = requireNonNull(text); this.mUsers = users != null ? users : new int[0]; } /** Unique identifier for this keyphrase */ public int getId() { return mId; } /** * 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 int getRecognitionModes() { return mRecognitionModes; } /** Locale of the keyphrase. */ @NonNull public Locale getLocale() { return mLocale; } /** Key phrase text */ @NonNull public String getText() { return mText; } /** * 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 int[] getUsers() { return mUsers; } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @NonNull public Keyphrase createFromParcel(@NonNull Parcel in) { return Keyphrase.readFromParcel(in); } @NonNull public Keyphrase[] newArray(int size) { return new Keyphrase[size]; } }; /** * Read from Parcel to generate keyphrase */ @NonNull public static Keyphrase readFromParcel(@NonNull Parcel in) { int id = in.readInt(); int recognitionModes = in.readInt(); Locale locale = Locale.forLanguageTag(in.readString()); String text = in.readString(); int[] users = null; int numUsers = in.readInt(); if (numUsers >= 0) { users = new int[numUsers]; in.readIntArray(users); } return new Keyphrase(id, recognitionModes, locale, text, users); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(getId()); dest.writeInt(getRecognitionModes()); dest.writeString(getLocale().toLanguageTag()); dest.writeString(getText()); if (getUsers() != null) { dest.writeInt(getUsers().length); dest.writeIntArray(getUsers()); } else { dest.writeInt(-1); } } /** @hide */ @Override public int describeContents() { return 0; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getText() == null) ? 0 : getText().hashCode()); result = prime * result + getId(); result = prime * result + ((getLocale() == null) ? 0 : getLocale().hashCode()); result = prime * result + getRecognitionModes(); result = prime * result + Arrays.hashCode(getUsers()); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Keyphrase other = (Keyphrase) obj; if (getText() == null) { if (other.getText() != null) { return false; } } else if (!getText().equals(other.getText())) { return false; } if (getId() != other.getId()) { return false; } if (getLocale() == null) { if (other.getLocale() != null) { return false; } } else if (!getLocale().equals(other.getLocale())) { return false; } if (getRecognitionModes() != other.getRecognitionModes()) { return false; } if (!Arrays.equals(getUsers(), other.getUsers())) { return false; } return true; } @Override public String toString() { return "Keyphrase [id=" + getId() + ", recognitionModes=" + getRecognitionModes() + ", locale=" + getLocale().toLanguageTag() + ", text=" + getText() + ", users=" + Arrays.toString(getUsers()) + "]"; } } /** * 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. */ public static final class KeyphraseSoundModel extends SoundModel implements Parcelable { @NonNull private final Keyphrase[] mKeyphrases; public KeyphraseSoundModel( @NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, @Nullable Keyphrase[] keyphrases, int version) { super(uuid, vendorUuid, TYPE_KEYPHRASE, data, version); this.mKeyphrases = keyphrases != null ? keyphrases : new Keyphrase[0]; } public KeyphraseSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, @Nullable Keyphrase[] keyphrases) { this(uuid, vendorUuid, data, keyphrases, -1); } /** Key phrases in this sound model */ @NonNull public Keyphrase[] getKeyphrases() { return mKeyphrases; } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @NonNull public KeyphraseSoundModel createFromParcel(@NonNull Parcel in) { return KeyphraseSoundModel.readFromParcel(in); } @NonNull public KeyphraseSoundModel[] newArray(int size) { return new KeyphraseSoundModel[size]; } }; /** * 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(); if (length >= 0) { vendorUuid = UUID.fromString(in.readString()); } int version = in.readInt(); byte[] data = in.readBlob(); Keyphrase[] keyphrases = in.createTypedArray(Keyphrase.CREATOR); return new KeyphraseSoundModel(uuid, vendorUuid, data, keyphrases, version); } /** @hide */ @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(getUuid().toString()); if (getVendorUuid() == null) { dest.writeInt(-1); } else { dest.writeInt(getVendorUuid().toString().length()); dest.writeString(getVendorUuid().toString()); } dest.writeInt(getVersion()); dest.writeBlob(getData()); dest.writeTypedArray(getKeyphrases(), flags); } @Override public String toString() { return "KeyphraseSoundModel [keyphrases=" + Arrays.toString(getKeyphrases()) + ", uuid=" + getUuid() + ", vendorUuid=" + getVendorUuid() + ", type=" + getType() + ", data=" + (getData() == null ? 0 : getData().length) + ", version=" + getVersion() + "]"; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + Arrays.hashCode(getKeyphrases()); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (!super.equals(obj)) { return false; } if (!(obj instanceof KeyphraseSoundModel)) { return false; } KeyphraseSoundModel other = (KeyphraseSoundModel) obj; if (!Arrays.equals(getKeyphrases(), other.getKeyphrases())) { return false; } return true; } } /***************************************************************************** * A GenericSoundModel is a specialized {@link SoundModel} for non-voice sound * patterns. * * @hide ****************************************************************************/ public static class GenericSoundModel extends SoundModel implements Parcelable { public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public GenericSoundModel createFromParcel(Parcel in) { return GenericSoundModel.fromParcel(in); } public GenericSoundModel[] newArray(int size) { return new GenericSoundModel[size]; } }; public GenericSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, int version) { super(uuid, vendorUuid, TYPE_GENERIC_SOUND, data, version); } @UnsupportedAppUsage public GenericSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data) { this(uuid, vendorUuid, data, -1); } @Override public int describeContents() { return 0; } private static GenericSoundModel fromParcel(Parcel in) { UUID uuid = UUID.fromString(in.readString()); UUID vendorUuid = null; int length = in.readInt(); if (length >= 0) { vendorUuid = UUID.fromString(in.readString()); } byte[] data = in.readBlob(); int version = in.readInt(); return new GenericSoundModel(uuid, vendorUuid, data, version); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(getUuid().toString()); if (getVendorUuid() == null) { dest.writeInt(-1); } else { dest.writeInt(getVendorUuid().toString().length()); dest.writeString(getVendorUuid().toString()); } dest.writeBlob(getData()); dest.writeInt(getVersion()); } @Override public String toString() { return "GenericSoundModel [uuid=" + getUuid() + ", vendorUuid=" + getVendorUuid() + ", type=" + getType() + ", data=" + (getData() == null ? 0 : getData().length) + ", version=" + getVersion() + "]"; } } /** * A ModelParamRange is a representation of supported parameter range for a * given loaded model. */ public static final class ModelParamRange implements Parcelable { /** * The inclusive start of supported range. */ private final int mStart; /** * The inclusive end of supported range. */ private final int mEnd; /** @hide */ @TestApi public ModelParamRange(int start, int end) { this.mStart = start; this.mEnd = end; } /** @hide */ private ModelParamRange(@NonNull Parcel in) { this.mStart = in.readInt(); this.mEnd = in.readInt(); } /** * Get the beginning of the param range * * @return The inclusive start of the supported range. */ public int getStart() { return mStart; } /** * Get the end of the param range * * @return The inclusive end of the supported range. */ public int getEnd() { return mEnd; } @NonNull public static final Creator CREATOR = new Creator() { @Override @NonNull public ModelParamRange createFromParcel(@NonNull Parcel in) { return new ModelParamRange(in); } @Override @NonNull public ModelParamRange[] newArray(int size) { return new ModelParamRange[size]; } }; /** @hide */ @Override public int describeContents() { return 0; } /** @hide */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (mStart); result = prime * result + (mEnd); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ModelParamRange other = (ModelParamRange) obj; if (mStart != other.mStart) { return false; } if (mEnd != other.mEnd) { return false; } return true; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mStart); dest.writeInt(mEnd); } @Override @NonNull public String toString() { return "ModelParamRange [start=" + mStart + ", end=" + mEnd + "]"; } } /** * SoundTrigger model parameter types. * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "MODEL_PARAM" }, value = { MODEL_PARAM_INVALID, MODEL_PARAM_THRESHOLD_FACTOR }) public @interface ModelParamTypes {} /** * See {@link ModelParams.INVALID} * @hide */ @TestApi public static final int MODEL_PARAM_INVALID = ModelParams.INVALID; /** * See {@link ModelParams.THRESHOLD_FACTOR} * @hide */ @TestApi public static final int MODEL_PARAM_THRESHOLD_FACTOR = ModelParams.THRESHOLD_FACTOR; /** * 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 {} /** * Trigger on recognition of a key phrase */ public static final int RECOGNITION_MODE_VOICE_TRIGGER = 0x1; /** * Trigger only if one user is identified */ public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 0x2; /** * Trigger only if one user is authenticated */ public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 0x4; /** * Generic (non-speech) recognition. */ public static final int RECOGNITION_MODE_GENERIC = 0x8; /** * Status codes for {@link RecognitionEvent} */ /** * Recognition success * * @hide */ public static final int RECOGNITION_STATUS_SUCCESS = 0; /** * Recognition aborted (e.g. capture preempted by anotehr use case * * @hide */ public static final int RECOGNITION_STATUS_ABORT = 1; /** * Recognition failure * * @hide */ public static final int RECOGNITION_STATUS_FAILURE = 2; /** * Recognition event was triggered by a getModelState request, not by the * DSP. * * @hide */ public static final int RECOGNITION_STATUS_GET_STATE_RESPONSE = 3; /** * A RecognitionEvent is provided by the * {@code StatusListener#onRecognition(RecognitionEvent)} * callback upon recognition success or failure. */ public static class RecognitionEvent { /** * Recognition status e.g RECOGNITION_STATUS_SUCCESS * * @hide */ @UnsupportedAppUsage public final int status; /** * * Sound Model corresponding to this event callback * * @hide */ @UnsupportedAppUsage public final int soundModelHandle; /** * True if it is possible to capture audio from this utterance buffered by the hardware * * @hide */ @UnsupportedAppUsage public final boolean captureAvailable; /** * Audio session ID to be used when capturing the utterance with an AudioRecord * if captureAvailable() is true. * * @hide */ @UnsupportedAppUsage public final int captureSession; /** * Delay in ms between end of model detection and start of audio available for capture. * A negative value is possible (e.g. if keyphrase is also available for capture) * * @hide */ public final int captureDelayMs; /** * Duration in ms of audio captured before the start of the trigger. 0 if none. * * @hide */ public final int capturePreambleMs; /** * True if the trigger (key phrase capture is present in binary data * * @hide */ public final boolean triggerInData; /** * Audio format of either the trigger in event data or to use for capture of the * rest of the utterance * * @hide */ @NonNull public final AudioFormat captureFormat; /** * Opaque data for use by system applications who know about voice engine internals, * typically during enrollment. * * @hide */ @UnsupportedAppUsage @NonNull public final byte[] data; /** * Is recognition still active after this event. * @hide */ public final boolean recognitionStillActive; /** * Timestamp of when the trigger event from SoundTriggerHal was received by the * framework. * *

Clock monotonic including suspend time or its equivalent on the system, * in the same units and timebase as {@link SystemClock#elapsedRealtime()}. * *

Value represents elapsed realtime in milliseconds when the event was received from the * HAL. The value will be -1 if the event was not generated from the HAL. * * @hide */ @ElapsedRealtimeLong public final long halEventReceivedMillis; /** * Binder token returned by {@link SoundTriggerModule#startRecognitionWithToken( * int soundModelHandle, SoundTrigger.RecognitionConfig config)} * @hide */ public final IBinder token; /** @hide */ @TestApi @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, @ElapsedRealtimeLong long halEventReceivedMillis) { this(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, status == RECOGNITION_STATUS_GET_STATE_RESPONSE, halEventReceivedMillis, null); } /** @hide */ public RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, boolean recognitionStillActive, @ElapsedRealtimeLong long halEventReceivedMillis, IBinder token) { this.status = status; this.soundModelHandle = soundModelHandle; this.captureAvailable = captureAvailable; this.captureSession = captureSession; this.captureDelayMs = captureDelayMs; this.capturePreambleMs = capturePreambleMs; this.triggerInData = triggerInData; this.captureFormat = requireNonNull(captureFormat); this.data = data != null ? data : new byte[0]; this.recognitionStillActive = recognitionStillActive; this.halEventReceivedMillis = halEventReceivedMillis; this.token = token; } /** * Check if is possible to capture audio from this utterance buffered by the hardware. * * @return {@code true} iff a capturing is possible */ public boolean isCaptureAvailable() { return captureAvailable; } /** * Get the audio format of either the trigger in event data or to use for capture of the * rest of the utterance * * @return the audio format */ @Nullable public AudioFormat getCaptureFormat() { return captureFormat; } /** * Get Audio session ID to be used when capturing the utterance with an {@link AudioRecord} * if {@link #isCaptureAvailable()} is true. * * @return The id of the capture session */ public int getCaptureSession() { return captureSession; } /** * Get the opaque data for use by system applications who know about voice engine * internals, typically during enrollment. * * @return The data of the event */ @SuppressLint("MissingNullability") public byte[] getData() { return data; } /** * Timestamp of when the trigger event from SoundTriggerHal was received by the * framework. * * Clock monotonic including suspend time or its equivalent on the system, * in the same units and timebase as {@link SystemClock#elapsedRealtime()}. * * @return Elapsed realtime in milliseconds when the event was received from the HAL. * Returns -1 if the event was not generated from the HAL. */ @ElapsedRealtimeLong public long getHalEventReceivedMillis() { return halEventReceivedMillis; } /** * Get token associated with this recognition session returned by *{@link SoundTriggerModule#startRecognitionWithToken( * int soundModelHandle, SoundTrigger.RecognitionConfig config)} * @hide */ public IBinder getToken() { return token; } /** @hide */ public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public RecognitionEvent createFromParcel(Parcel in) { return RecognitionEvent.fromParcel(in); } public RecognitionEvent[] newArray(int size) { return new RecognitionEvent[size]; } }; /** @hide */ protected static RecognitionEvent fromParcel(Parcel in) { int status = in.readInt(); int soundModelHandle = in.readInt(); boolean captureAvailable = in.readByte() == 1; int captureSession = in.readInt(); int captureDelayMs = in.readInt(); int capturePreambleMs = in.readInt(); boolean triggerInData = in.readByte() == 1; AudioFormat captureFormat = null; if (in.readByte() == 1) { int sampleRate = in.readInt(); int encoding = in.readInt(); int channelMask = in.readInt(); captureFormat = (new AudioFormat.Builder()) .setChannelMask(channelMask) .setEncoding(encoding) .setSampleRate(sampleRate) .build(); } byte[] data = in.readBlob(); boolean recognitionStillActive = in.readBoolean(); long halEventReceivedMillis = in.readLong(); IBinder token = in.readStrongBinder(); return new RecognitionEvent(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, recognitionStillActive, halEventReceivedMillis, token); } /** @hide */ public int describeContents() { return 0; } /** @hide */ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(status); dest.writeInt(soundModelHandle); dest.writeByte((byte) (captureAvailable ? 1 : 0)); dest.writeInt(captureSession); dest.writeInt(captureDelayMs); dest.writeInt(capturePreambleMs); dest.writeByte((byte) (triggerInData ? 1 : 0)); if (captureFormat != null) { dest.writeByte((byte)1); dest.writeInt(captureFormat.getSampleRate()); dest.writeInt(captureFormat.getEncoding()); dest.writeInt(captureFormat.getChannelMask()); } else { dest.writeByte((byte)0); } dest.writeBlob(data); dest.writeBoolean(recognitionStillActive); dest.writeLong(halEventReceivedMillis); dest.writeStrongBinder(token); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (captureAvailable ? 1231 : 1237); result = prime * result + captureDelayMs; result = prime * result + capturePreambleMs; result = prime * result + captureSession; result = prime * result + (triggerInData ? 1231 : 1237); if (captureFormat != null) { result = prime * result + captureFormat.getSampleRate(); result = prime * result + captureFormat.getEncoding(); result = prime * result + captureFormat.getChannelMask(); } result = prime * result + Arrays.hashCode(data); result = prime * result + soundModelHandle; result = prime * result + status; result = result + (recognitionStillActive ? 1289 : 1291); result = prime * result + Long.hashCode(halEventReceivedMillis); result = prime * result + Objects.hashCode(token); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; RecognitionEvent other = (RecognitionEvent) obj; if (captureAvailable != other.captureAvailable) return false; if (captureDelayMs != other.captureDelayMs) return false; if (capturePreambleMs != other.capturePreambleMs) return false; if (captureSession != other.captureSession) return false; if (!Arrays.equals(data, other.data)) return false; if (recognitionStillActive != other.recognitionStillActive) return false; if (soundModelHandle != other.soundModelHandle) return false; if (halEventReceivedMillis != other.halEventReceivedMillis) { return false; } if (!Objects.equals(token, other.token)) { return false; } if (status != other.status) return false; if (triggerInData != other.triggerInData) return false; if (captureFormat == null) { if (other.captureFormat != null) return false; } else { if (other.captureFormat == null) return false; if (captureFormat.getSampleRate() != other.captureFormat.getSampleRate()) return false; if (captureFormat.getEncoding() != other.captureFormat.getEncoding()) return false; if (captureFormat.getChannelMask() != other.captureFormat.getChannelMask()) return false; } return true; } @NonNull @Override public String toString() { return "RecognitionEvent [status=" + status + ", soundModelHandle=" + soundModelHandle + ", captureAvailable=" + captureAvailable + ", captureSession=" + captureSession + ", captureDelayMs=" + captureDelayMs + ", capturePreambleMs=" + capturePreambleMs + ", triggerInData=" + triggerInData + ((captureFormat == null) ? "" : (", sampleRate=" + captureFormat.getSampleRate())) + ((captureFormat == null) ? "" : (", encoding=" + captureFormat.getEncoding())) + ((captureFormat == null) ? "" : (", channelMask=" + captureFormat.getChannelMask())) + ", data=" + (data == null ? 0 : data.length) + ", recognitionStillActive=" + recognitionStillActive + ", halEventReceivedMillis=" + halEventReceivedMillis + ", token=" + token + "]"; } } /** * A RecognitionConfig is provided to * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} to configure the * recognition request. * * @hide */ @TestApi public static final class RecognitionConfig implements Parcelable { /** True if the DSP should capture the trigger sound and make it available for further * capture. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public final boolean captureRequested; /** * True if the service should restart listening after the DSP triggers. * Note: This config flag is currently used at the service layer rather than by the DSP. */ public final boolean allowMultipleTriggers; /** List of all keyphrases in the sound model for which recognition should be performed with * options for each keyphrase. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @NonNull @SuppressLint("ArrayReturn") public final KeyphraseRecognitionExtra keyphrases[]; /** Opaque data for use by system applications who know about voice engine internals, * typically during enrollment. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @NonNull public final byte[] data; /** * Bit field encoding of the AudioCapabilities * supported by the firmware. */ @ModuleProperties.AudioCapabilities public final int audioCapabilities; public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data, int audioCapabilities) { this.captureRequested = captureRequested; this.allowMultipleTriggers = allowMultipleTriggers; this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; this.data = data != null ? data : new byte[0]; this.audioCapabilities = audioCapabilities; } @UnsupportedAppUsage public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data) { this(captureRequested, allowMultipleTriggers, keyphrases, data, 0); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public RecognitionConfig createFromParcel(Parcel in) { return RecognitionConfig.fromParcel(in); } public RecognitionConfig[] newArray(int size) { return new RecognitionConfig[size]; } }; private static RecognitionConfig fromParcel(Parcel in) { boolean captureRequested = in.readByte() == 1; boolean allowMultipleTriggers = in.readByte() == 1; KeyphraseRecognitionExtra[] keyphrases = in.createTypedArray(KeyphraseRecognitionExtra.CREATOR); byte[] data = in.readBlob(); int audioCapabilities = in.readInt(); return new RecognitionConfig(captureRequested, allowMultipleTriggers, keyphrases, data, audioCapabilities); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeByte((byte) (captureRequested ? 1 : 0)); dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0)); dest.writeTypedArray(keyphrases, flags); dest.writeBlob(data); dest.writeInt(audioCapabilities); } @Override public int describeContents() { return 0; } @Override public String toString() { return "RecognitionConfig [captureRequested=" + captureRequested + ", allowMultipleTriggers=" + allowMultipleTriggers + ", keyphrases=" + Arrays.toString(keyphrases) + ", data=" + Arrays.toString(data) + ", audioCapabilities=" + Integer.toHexString(audioCapabilities) + "]"; } @Override public final boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof RecognitionConfig)) return false; RecognitionConfig other = (RecognitionConfig) obj; if (captureRequested != other.captureRequested) { return false; } if (allowMultipleTriggers != other.allowMultipleTriggers) { return false; } if (!Arrays.equals(keyphrases, other.keyphrases)) { return false; } if (!Arrays.equals(data, other.data)) { return false; } if (audioCapabilities != other.audioCapabilities) { return false; } return true; } @Override public final int hashCode() { final int prime = 31; int result = 1; result = prime * result + (captureRequested ? 1 : 0); result = prime * result + (allowMultipleTriggers ? 1 : 0); result = prime * result + Arrays.hashCode(keyphrases); result = prime * result + Arrays.hashCode(data); result = prime * result + audioCapabilities; return result; } } /** * Confidence level for users defined in a keyphrase. * - The confidence level is expressed in percent (0% -100%). * When used in a {@link KeyphraseRecognitionEvent} it indicates the detected confidence level * When used in a {@link RecognitionConfig} it indicates the minimum confidence level that * should trigger a recognition. * - The user ID is derived from the system ID {@link android.os.UserHandle#getIdentifier()}. * * @hide */ public static class ConfidenceLevel implements Parcelable { @UnsupportedAppUsage public final int userId; @UnsupportedAppUsage public final int confidenceLevel; @UnsupportedAppUsage public ConfidenceLevel(int userId, int confidenceLevel) { this.userId = userId; this.confidenceLevel = confidenceLevel; } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public ConfidenceLevel createFromParcel(Parcel in) { return ConfidenceLevel.fromParcel(in); } public ConfidenceLevel[] newArray(int size) { return new ConfidenceLevel[size]; } }; private static ConfidenceLevel fromParcel(Parcel in) { int userId = in.readInt(); int confidenceLevel = in.readInt(); return new ConfidenceLevel(userId, confidenceLevel); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(userId); dest.writeInt(confidenceLevel); } @Override public int describeContents() { return 0; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + confidenceLevel; result = prime * result + userId; return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ConfidenceLevel other = (ConfidenceLevel) obj; if (confidenceLevel != other.confidenceLevel) return false; if (userId != other.userId) return false; return true; } @Override public String toString() { return "ConfidenceLevel [userId=" + userId + ", confidenceLevel=" + confidenceLevel + "]"; } } /** * Additional data conveyed by a {@link KeyphraseRecognitionEvent} * for a key phrase detection. */ public static final class KeyphraseRecognitionExtra implements Parcelable { /** * The keyphrase ID * * @hide */ @UnsupportedAppUsage public final int id; /** * Recognition modes matched for this event * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public final int recognitionModes; /** * Confidence level for mode RECOGNITION_MODE_VOICE_TRIGGER when user identification * is not performed * * @hide */ @UnsupportedAppUsage public final int coarseConfidenceLevel; /** * Confidence levels for all users recognized (KeyphraseRecognitionEvent) or to * be recognized (RecognitionConfig) * * @hide */ @UnsupportedAppUsage @NonNull public final ConfidenceLevel[] confidenceLevels; /** * @hide */ @TestApi public KeyphraseRecognitionExtra(int id, @RecognitionModes int recognitionModes, int coarseConfidenceLevel) { this(id, recognitionModes, coarseConfidenceLevel, new ConfidenceLevel[0]); } /** * @hide */ @UnsupportedAppUsage public KeyphraseRecognitionExtra(int id, int recognitionModes, @IntRange(from = 0, to = 100) int coarseConfidenceLevel, @Nullable ConfidenceLevel[] confidenceLevels) { this.id = id; this.recognitionModes = recognitionModes; this.coarseConfidenceLevel = coarseConfidenceLevel; this.confidenceLevels = confidenceLevels != null ? confidenceLevels : new ConfidenceLevel[0]; } /** * The keyphrase ID associated with this class' additional data */ public int getKeyphraseId() { return id; } /** * Recognition modes matched for this event */ @RecognitionModes public int getRecognitionModes() { return recognitionModes; } /** * Confidence level for mode RECOGNITION_MODE_VOICE_TRIGGER when user identification * is not performed * *

The confidence level is expressed in percent (0% -100%). */ @IntRange(from = 0, to = 100) public int getCoarseConfidenceLevel() { return coarseConfidenceLevel; } /** * Detected confidence level for users defined in a keyphrase. * *

The confidence level is expressed in percent (0% -100%). * *

The user ID is derived from the system ID * {@link android.os.UserHandle#getIdentifier()}. * * @hide */ @NonNull public Collection getConfidenceLevels() { return Arrays.asList(confidenceLevels); } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public KeyphraseRecognitionExtra createFromParcel(Parcel in) { return KeyphraseRecognitionExtra.fromParcel(in); } public KeyphraseRecognitionExtra[] newArray(int size) { return new KeyphraseRecognitionExtra[size]; } }; private static KeyphraseRecognitionExtra fromParcel(Parcel in) { int id = in.readInt(); int recognitionModes = in.readInt(); int coarseConfidenceLevel = in.readInt(); ConfidenceLevel[] confidenceLevels = in.createTypedArray(ConfidenceLevel.CREATOR); return new KeyphraseRecognitionExtra(id, recognitionModes, coarseConfidenceLevel, confidenceLevels); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(id); dest.writeInt(recognitionModes); dest.writeInt(coarseConfidenceLevel); dest.writeTypedArray(confidenceLevels, flags); } @Override public int describeContents() { return 0; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + Arrays.hashCode(confidenceLevels); result = prime * result + id; result = prime * result + recognitionModes; result = prime * result + coarseConfidenceLevel; return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } KeyphraseRecognitionExtra other = (KeyphraseRecognitionExtra) obj; if (!Arrays.equals(confidenceLevels, other.confidenceLevels)) { return false; } if (id != other.id) { return false; } if (recognitionModes != other.recognitionModes) { return false; } if (coarseConfidenceLevel != other.coarseConfidenceLevel) { return false; } return true; } @Override public String toString() { return "KeyphraseRecognitionExtra [id=" + id + ", recognitionModes=" + recognitionModes + ", coarseConfidenceLevel=" + coarseConfidenceLevel + ", confidenceLevels=" + Arrays.toString(confidenceLevels) + "]"; } } /** * Specialized {@link RecognitionEvent} for a key phrase detection. * * @hide */ public static class KeyphraseRecognitionEvent extends RecognitionEvent implements Parcelable { /** Indicates if the key phrase is present in the buffered audio available for capture */ @UnsupportedAppUsage @NonNull public final KeyphraseRecognitionExtra[] keyphraseExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public KeyphraseRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, @Nullable KeyphraseRecognitionExtra[] keyphraseExtras, @ElapsedRealtimeLong long halEventReceivedMillis, IBinder token) { this(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, keyphraseExtras, status == RECOGNITION_STATUS_GET_STATE_RESPONSE, halEventReceivedMillis, token); } public KeyphraseRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, @Nullable KeyphraseRecognitionExtra[] keyphraseExtras, boolean recognitionStillActive, @ElapsedRealtimeLong long halEventReceivedMillis, IBinder token) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, recognitionStillActive, halEventReceivedMillis, token); this.keyphraseExtras = keyphraseExtras != null ? keyphraseExtras : new KeyphraseRecognitionExtra[0]; } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public KeyphraseRecognitionEvent createFromParcel(Parcel in) { return KeyphraseRecognitionEvent.fromParcelForKeyphrase(in); } public KeyphraseRecognitionEvent[] newArray(int size) { return new KeyphraseRecognitionEvent[size]; } }; private static KeyphraseRecognitionEvent fromParcelForKeyphrase(Parcel in) { int status = in.readInt(); int soundModelHandle = in.readInt(); boolean captureAvailable = in.readByte() == 1; int captureSession = in.readInt(); int captureDelayMs = in.readInt(); int capturePreambleMs = in.readInt(); boolean triggerInData = in.readByte() == 1; AudioFormat captureFormat = null; if (in.readByte() == 1) { int sampleRate = in.readInt(); int encoding = in.readInt(); int channelMask = in.readInt(); captureFormat = (new AudioFormat.Builder()) .setChannelMask(channelMask) .setEncoding(encoding) .setSampleRate(sampleRate) .build(); } byte[] data = in.readBlob(); boolean recognitionStillActive = in.readBoolean(); long halEventReceivedMillis = in.readLong(); IBinder token = in.readStrongBinder(); KeyphraseRecognitionExtra[] keyphraseExtras = in.createTypedArray(KeyphraseRecognitionExtra.CREATOR); return new KeyphraseRecognitionEvent(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, keyphraseExtras, recognitionStillActive, halEventReceivedMillis, token); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(status); dest.writeInt(soundModelHandle); dest.writeByte((byte) (captureAvailable ? 1 : 0)); dest.writeInt(captureSession); dest.writeInt(captureDelayMs); dest.writeInt(capturePreambleMs); dest.writeByte((byte) (triggerInData ? 1 : 0)); if (captureFormat != null) { dest.writeByte((byte)1); dest.writeInt(captureFormat.getSampleRate()); dest.writeInt(captureFormat.getEncoding()); dest.writeInt(captureFormat.getChannelMask()); } else { dest.writeByte((byte)0); } dest.writeBlob(data); dest.writeBoolean(recognitionStillActive); dest.writeLong(halEventReceivedMillis); dest.writeStrongBinder(token); dest.writeTypedArray(keyphraseExtras, flags); } @Override public int describeContents() { return 0; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + Arrays.hashCode(keyphraseExtras); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; KeyphraseRecognitionEvent other = (KeyphraseRecognitionEvent) obj; if (!Arrays.equals(keyphraseExtras, other.keyphraseExtras)) return false; return true; } @Override public String toString() { return "KeyphraseRecognitionEvent [keyphraseExtras=" + Arrays.toString(keyphraseExtras) + ", status=" + status + ", soundModelHandle=" + soundModelHandle + ", captureAvailable=" + captureAvailable + ", captureSession=" + captureSession + ", captureDelayMs=" + captureDelayMs + ", capturePreambleMs=" + capturePreambleMs + ", triggerInData=" + triggerInData + ((captureFormat == null) ? "" : (", sampleRate=" + captureFormat.getSampleRate())) + ((captureFormat == null) ? "" : (", encoding=" + captureFormat.getEncoding())) + ((captureFormat == null) ? "" : (", channelMask=" + captureFormat.getChannelMask())) + ", data=" + (data == null ? 0 : data.length) + ", recognitionStillActive=" + recognitionStillActive + ", halEventReceivedMillis=" + halEventReceivedMillis + ", token=" + token + "]"; } } /** * Sub-class of RecognitionEvent specifically for sound-trigger based sound * models(non-keyphrase). Currently does not contain any additional fields. * * @hide */ public static class GenericRecognitionEvent extends RecognitionEvent implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public GenericRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, @ElapsedRealtimeLong long halEventReceivedMillis, IBinder token) { this(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, status == RECOGNITION_STATUS_GET_STATE_RESPONSE, halEventReceivedMillis, token); } public GenericRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, boolean recognitionStillActive, @ElapsedRealtimeLong long halEventReceivedMillis, IBinder token) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data, recognitionStillActive, halEventReceivedMillis, token); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public GenericRecognitionEvent createFromParcel(Parcel in) { return GenericRecognitionEvent.fromParcelForGeneric(in); } public GenericRecognitionEvent[] newArray(int size) { return new GenericRecognitionEvent[size]; } }; private static GenericRecognitionEvent fromParcelForGeneric(Parcel in) { RecognitionEvent event = RecognitionEvent.fromParcel(in); return new GenericRecognitionEvent(event.status, event.soundModelHandle, event.captureAvailable, event.captureSession, event.captureDelayMs, event.capturePreambleMs, event.triggerInData, event.captureFormat, event.data, event.recognitionStillActive, event.halEventReceivedMillis, event.token); } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; RecognitionEvent other = (RecognitionEvent) obj; return super.equals(obj); } @Override public String toString() { return "GenericRecognitionEvent ::" + super.toString(); } } private static Object mServiceLock = new Object(); /** * Translate an exception thrown from interaction with the underlying service to an error code. * Throws a runtime exception for unexpected conditions. * @param e The caught exception. * @return The error code. * * @hide */ public static int handleException(Exception e) { Log.w(TAG, "Exception caught", e); if (e instanceof RemoteException) { return STATUS_DEAD_OBJECT; } if (e instanceof ServiceSpecificException) { switch (((ServiceSpecificException) e).errorCode) { case Status.OPERATION_NOT_SUPPORTED: return STATUS_INVALID_OPERATION; case Status.TEMPORARY_PERMISSION_DENIED: return STATUS_PERMISSION_DENIED; case Status.DEAD_OBJECT: return STATUS_DEAD_OBJECT; case Status.INTERNAL_ERROR: return STATUS_ERROR; case Status.RESOURCE_CONTENTION: return STATUS_BUSY; } return STATUS_ERROR; } if (e instanceof SecurityException) { return STATUS_PERMISSION_DENIED; } if (e instanceof IllegalStateException) { return STATUS_INVALID_OPERATION; } if (e instanceof IllegalArgumentException || e instanceof NullPointerException) { return STATUS_BAD_VALUE; } // This is not one of the conditions represented by our error code, escalate to a // RuntimeException. Log.e(TAG, "Escalating unexpected exception: ", e); throw new RuntimeException(e); } /** * Returns a list of descriptors for all hardware modules loaded. * @param modules A ModuleProperties array where the list will be returned. * @return - {@link #STATUS_OK} in case of success * - {@link #STATUS_ERROR} in case of unspecified error * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission * - {@link #STATUS_NO_INIT} if the native service cannot be reached * - {@link #STATUS_BAD_VALUE} if modules is null * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails * * @removed Please use {@link #listModulesAsOriginator(ArrayList, Identity)} or * {@link #listModulesAsMiddleman(ArrayList, Identity, Identity)}, based on whether the * client is acting on behalf of its own identity or a separate identity. * @hide */ @UnsupportedAppUsage public static int listModules(@NonNull ArrayList modules) { return STATUS_OK; } /** * Returns a list of descriptors for all hardware modules loaded. * This variant is intended for use when the caller itself is the originator of the operation. * @param modules A ModuleProperties array where the list will be returned. * @param originatorIdentity The identity of the originator, which will be used for permission * purposes. * @return - {@link #STATUS_OK} in case of success * - {@link #STATUS_ERROR} in case of unspecified error * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission * - {@link #STATUS_NO_INIT} if the native service cannot be reached * - {@link #STATUS_BAD_VALUE} if modules is null * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails * @deprecated Use {@link android.media.soundtrigger.SoundTriggerManager} instead. * @hide */ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) @Deprecated public static int listModulesAsOriginator(@NonNull ArrayList modules, @NonNull Identity originatorIdentity) { try { SoundTriggerModuleDescriptor[] descs = getService().listModulesAsOriginator( originatorIdentity); convertDescriptorsToModuleProperties(descs, modules); return STATUS_OK; } catch (Exception e) { return handleException(e); } } /** * Returns a list of descriptors for all hardware modules loaded. * This variant is intended for use when the caller is acting on behalf of a different identity * for permission purposes. * @param modules A ModuleProperties array where the list will be returned. * @param middlemanIdentity The identity of the caller, acting as middleman. * @param originatorIdentity The identity of the originator, which will be used for permission * purposes. * @return - {@link #STATUS_OK} in case of success * - {@link #STATUS_ERROR} in case of unspecified error * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission * - {@link #STATUS_NO_INIT} if the native service cannot be reached * - {@link #STATUS_BAD_VALUE} if modules is null * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails * * @deprecated Use {@link android.media.soundtrigger.SoundTriggerManager} instead. * @hide */ @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY) @Deprecated public static int listModulesAsMiddleman(@NonNull ArrayList modules, @NonNull Identity middlemanIdentity, @NonNull Identity originatorIdentity) { try { SoundTriggerModuleDescriptor[] descs = getService().listModulesAsMiddleman( middlemanIdentity, originatorIdentity); convertDescriptorsToModuleProperties(descs, modules); return STATUS_OK; } catch (Exception e) { return handleException(e); } } /** * Converts an array of SoundTriggerModuleDescriptor into an (existing) ArrayList of * ModuleProperties. * @param descsIn The input descriptors. * @param modulesOut The output list. */ private static void convertDescriptorsToModuleProperties( @NonNull SoundTriggerModuleDescriptor[] descsIn, @NonNull ArrayList modulesOut) { modulesOut.clear(); modulesOut.ensureCapacity(descsIn.length); for (SoundTriggerModuleDescriptor desc : descsIn) { modulesOut.add(ConversionUtil.aidl2apiModuleDescriptor(desc)); } } /** * Get an interface on a hardware module to control sound models and recognition on * this module. * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory. * @param listener {@link StatusListener} interface. Mandatory. * @param handler the Handler that will receive the callabcks. Can be null if default handler * is OK. * @return a valid sound module in case of success or null in case of error. * * @removed Use {@link android.media.soundtrigger.SoundTriggerManager} instead. * @hide */ @UnsupportedAppUsage private static SoundTriggerModule attachModule(int moduleId, @NonNull StatusListener listener, @Nullable Handler handler) { return null; } /** * Get an interface on a hardware module to control sound models and recognition on * this module. * This variant is intended for use when the caller is acting on behalf of a different identity * for permission purposes. * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory. * @param listener {@link StatusListener} interface. Mandatory. * @param handler the Handler that will receive the callabcks. Can be null if default handler * is OK. * @param middlemanIdentity The identity of the caller, acting as middleman. * @param originatorIdentity The identity of the originator, which will be used for permission * purposes. * @return a valid sound module in case of success or null in case of error. * @deprecated Use {@link android.media.soundtrigger.SoundTriggerManager} instead. * @hide */ @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY) @Deprecated public static SoundTriggerModule attachModuleAsMiddleman(int moduleId, @NonNull SoundTrigger.StatusListener listener, @Nullable Handler handler, Identity middlemanIdentity, Identity originatorIdentity) { Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); try { return new SoundTriggerModule(getService(), moduleId, listener, looper, middlemanIdentity, originatorIdentity, false); } catch (Exception e) { Log.e(TAG, "", e); return null; } } /** * Get an interface on a hardware module to control sound models and recognition on * this module. * This variant is intended for use when the caller itself is the originator of the operation. * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory. * @param listener {@link StatusListener} interface. Mandatory. * @param handler the Handler that will receive the callabcks. Can be null if default handler * is OK. * @param originatorIdentity The identity of the originator, which will be used for permission * purposes. * @return a valid sound module in case of success or null in case of error. * * @hide */ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) public static SoundTriggerModule attachModuleAsOriginator(int moduleId, @NonNull SoundTrigger.StatusListener listener, @Nullable Handler handler, @NonNull Identity originatorIdentity) { Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); try { return new SoundTriggerModule(getService(), moduleId, listener, looper, originatorIdentity); } catch (Exception e) { Log.e(TAG, "", e); return null; } } private static ISoundTriggerMiddlewareService getService() { synchronized (mServiceLock) { while (true) { IBinder binder = null; try { binder = ServiceManager.getServiceOrThrow( Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE); return ISoundTriggerMiddlewareService.Stub.asInterface(binder); } catch (Exception e) { Log.e(TAG, "Failed to bind to soundtrigger service", e); } } } } /** * Interface provided by the client application when attaching to a {@link SoundTriggerModule} * to received recognition and error notifications. * * @hide */ public interface StatusListener { /** * Called when recognition succeeds of fails */ void onRecognition(RecognitionEvent event); /** * Called when a sound model has been preemptively unloaded by the underlying * implementation. */ void onModelUnloaded(int modelHandle); /** * Called whenever underlying conditions change, such that load/start operations that have * previously failed or got preempted may now succeed. This is not a guarantee, merely a * hint that the client may want to retry operations. */ void onResourcesAvailable(); /** * Called when the sound trigger native service dies */ void onServiceDied(); } }