diff options
author | Ytai Ben-tsvi <ytai@google.com> | 2019-12-17 22:33:55 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2019-12-17 22:33:55 +0000 |
commit | 0d52f4202ca7ec1f06fcd5b0b3661130aae29332 (patch) | |
tree | e2802502e60a44e5cb2cfa488cc65183725fa908 | |
parent | 1c66473972bdc9ee54219742ba10b962277f4aff (diff) | |
parent | 7d383d1a98957d46aa09ac64091e4eae73b4c662 (diff) |
Merge changes from topic "soundtrigger_refactor"
* changes:
Migrate SoundTrigger implementation to new service
Implement the soundtrigger_middlewware service
Add a permission for preempting sound trigger sessions
Sound trigger middleware service definition
Add audio.common types AIDL definition
56 files changed, 6374 insertions, 1235 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 808f2160109d..d0844491da53 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4336,6 +4336,15 @@ public abstract class Context { public static final String SOUND_TRIGGER_SERVICE = "soundtrigger"; /** + * Use with {@link #getSystemService(String)} to access the + * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService}. + * + * @hide + * @see #getSystemService(String) + */ + public static final String SOUND_TRIGGER_MIDDLEWARE_SERVICE = "soundtrigger_middleware"; + + /** * Official published name of the (internal) permission service. * * @see #getSystemService(String) diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java new file mode 100644 index 000000000000..8231c58a105e --- /dev/null +++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2019 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 android.hardware.soundtrigger.ModelParams; +import android.media.AudioFormat; +import android.media.audio.common.AudioConfig; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; + +import android.annotation.Nullable; + +import java.util.Arrays; +import java.util.UUID; + +/** @hide */ +class ConversionUtil { + public static SoundTrigger.ModuleProperties aidl2apiModuleDescriptor( + SoundTriggerModuleDescriptor aidlDesc) { + SoundTriggerModuleProperties properties = aidlDesc.properties; + return new SoundTrigger.ModuleProperties( + aidlDesc.handle, + properties.implementor, + properties.description, + properties.uuid, + properties.version, + properties.maxSoundModels, + properties.maxKeyPhrases, + properties.maxUsers, + aidl2apiRecognitionModes(properties.recognitionModes), + properties.captureTransition, + properties.maxBufferMs, + properties.concurrentCapture, + properties.powerConsumptionMw, + properties.triggerInEvent + ); + } + + public static int aidl2apiRecognitionModes(int aidlModes) { + int result = 0; + if ((aidlModes & RecognitionMode.VOICE_TRIGGER) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER; + } + if ((aidlModes & RecognitionMode.USER_IDENTIFICATION) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION; + } + if ((aidlModes & RecognitionMode.USER_AUTHENTICATION) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_USER_AUTHENTICATION; + } + if ((aidlModes & RecognitionMode.GENERIC_TRIGGER) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_GENERIC; + } + return result; + } + + public static int api2aidlRecognitionModes(int apiModes) { + int result = 0; + if ((apiModes & SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER) != 0) { + result |= RecognitionMode.VOICE_TRIGGER; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION) != 0) { + result |= RecognitionMode.USER_IDENTIFICATION; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_USER_AUTHENTICATION) != 0) { + result |= RecognitionMode.USER_AUTHENTICATION; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_GENERIC) != 0) { + result |= RecognitionMode.GENERIC_TRIGGER; + } + return result; + } + + + public static SoundModel api2aidlGenericSoundModel(SoundTrigger.GenericSoundModel apiModel) { + return api2aidlSoundModel(apiModel); + } + + public static SoundModel api2aidlSoundModel(SoundTrigger.SoundModel apiModel) { + SoundModel aidlModel = new SoundModel(); + aidlModel.type = apiModel.type; + aidlModel.uuid = api2aidlUuid(apiModel.uuid); + aidlModel.vendorUuid = api2aidlUuid(apiModel.vendorUuid); + aidlModel.data = Arrays.copyOf(apiModel.data, apiModel.data.length); + return aidlModel; + } + + public static String api2aidlUuid(UUID apiUuid) { + return apiUuid.toString(); + } + + public static PhraseSoundModel api2aidlPhraseSoundModel( + SoundTrigger.KeyphraseSoundModel apiModel) { + PhraseSoundModel aidlModel = new PhraseSoundModel(); + aidlModel.common = api2aidlSoundModel(apiModel); + aidlModel.phrases = new Phrase[apiModel.keyphrases.length]; + for (int i = 0; i < apiModel.keyphrases.length; ++i) { + aidlModel.phrases[i] = api2aidlPhrase(apiModel.keyphrases[i]); + } + return aidlModel; + } + + public static Phrase api2aidlPhrase(SoundTrigger.Keyphrase apiPhrase) { + Phrase aidlPhrase = new Phrase(); + aidlPhrase.id = apiPhrase.id; + aidlPhrase.recognitionModes = api2aidlRecognitionModes(apiPhrase.recognitionModes); + aidlPhrase.users = Arrays.copyOf(apiPhrase.users, apiPhrase.users.length); + aidlPhrase.locale = apiPhrase.locale; + aidlPhrase.text = apiPhrase.text; + return aidlPhrase; + } + + public static RecognitionConfig api2aidlRecognitionConfig( + SoundTrigger.RecognitionConfig apiConfig) { + RecognitionConfig aidlConfig = new RecognitionConfig(); + aidlConfig.captureRequested = apiConfig.captureRequested; + // apiConfig.allowMultipleTriggers is ignored by the lower layers. + aidlConfig.phraseRecognitionExtras = + new PhraseRecognitionExtra[apiConfig.keyphrases.length]; + for (int i = 0; i < apiConfig.keyphrases.length; ++i) { + aidlConfig.phraseRecognitionExtras[i] = api2aidlPhraseRecognitionExtra( + apiConfig.keyphrases[i]); + } + aidlConfig.data = Arrays.copyOf(apiConfig.data, apiConfig.data.length); + return aidlConfig; + } + + public static PhraseRecognitionExtra api2aidlPhraseRecognitionExtra( + SoundTrigger.KeyphraseRecognitionExtra apiExtra) { + PhraseRecognitionExtra aidlExtra = new PhraseRecognitionExtra(); + aidlExtra.id = apiExtra.id; + aidlExtra.recognitionModes = api2aidlRecognitionModes(apiExtra.recognitionModes); + aidlExtra.confidenceLevel = apiExtra.coarseConfidenceLevel; + aidlExtra.levels = new ConfidenceLevel[apiExtra.confidenceLevels.length]; + for (int i = 0; i < apiExtra.confidenceLevels.length; ++i) { + aidlExtra.levels[i] = api2aidlConfidenceLevel(apiExtra.confidenceLevels[i]); + } + return aidlExtra; + } + + public static SoundTrigger.KeyphraseRecognitionExtra aidl2apiPhraseRecognitionExtra( + PhraseRecognitionExtra aidlExtra) { + SoundTrigger.ConfidenceLevel[] apiLevels = + new SoundTrigger.ConfidenceLevel[aidlExtra.levels.length]; + for (int i = 0; i < aidlExtra.levels.length; ++i) { + apiLevels[i] = aidl2apiConfidenceLevel(aidlExtra.levels[i]); + } + return new SoundTrigger.KeyphraseRecognitionExtra(aidlExtra.id, + aidl2apiRecognitionModes(aidlExtra.recognitionModes), + aidlExtra.confidenceLevel, apiLevels); + } + + public static ConfidenceLevel api2aidlConfidenceLevel( + SoundTrigger.ConfidenceLevel apiLevel) { + ConfidenceLevel aidlLevel = new ConfidenceLevel(); + aidlLevel.levelPercent = apiLevel.confidenceLevel; + aidlLevel.userId = apiLevel.userId; + return aidlLevel; + } + + public static SoundTrigger.ConfidenceLevel aidl2apiConfidenceLevel( + ConfidenceLevel apiLevel) { + return new SoundTrigger.ConfidenceLevel(apiLevel.userId, apiLevel.levelPercent); + } + + public static SoundTrigger.RecognitionEvent aidl2apiRecognitionEvent( + int modelHandle, RecognitionEvent aidlEvent) { + return new SoundTrigger.GenericRecognitionEvent( + aidlEvent.status, + modelHandle, aidlEvent.captureAvailable, aidlEvent.captureSession, + aidlEvent.captureDelayMs, aidlEvent.capturePreambleMs, aidlEvent.triggerInData, + aidl2apiAudioFormat(aidlEvent.audioConfig), aidlEvent.data); + } + + public static SoundTrigger.RecognitionEvent aidl2apiPhraseRecognitionEvent( + int modelHandle, + PhraseRecognitionEvent aidlEvent) { + SoundTrigger.KeyphraseRecognitionExtra[] apiExtras = + new SoundTrigger.KeyphraseRecognitionExtra[aidlEvent.phraseExtras.length]; + for (int i = 0; i < aidlEvent.phraseExtras.length; ++i) { + apiExtras[i] = aidl2apiPhraseRecognitionExtra(aidlEvent.phraseExtras[i]); + } + return new SoundTrigger.KeyphraseRecognitionEvent(aidlEvent.common.status, modelHandle, + aidlEvent.common.captureAvailable, + aidlEvent.common.captureSession, aidlEvent.common.captureDelayMs, + aidlEvent.common.capturePreambleMs, aidlEvent.common.triggerInData, + aidl2apiAudioFormat(aidlEvent.common.audioConfig), aidlEvent.common.data, + apiExtras); + } + + public static AudioFormat aidl2apiAudioFormat(AudioConfig audioConfig) { + AudioFormat.Builder apiBuilder = new AudioFormat.Builder(); + apiBuilder.setSampleRate(audioConfig.sampleRateHz); + apiBuilder.setChannelMask(aidl2apiChannelInMask(audioConfig.channelMask)); + apiBuilder.setEncoding(aidl2apiEncoding(audioConfig.format)); + return apiBuilder.build(); + } + + public static int aidl2apiEncoding(int aidlFormat) { + switch (aidlFormat) { + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_16_BIT: + return AudioFormat.ENCODING_PCM_16BIT; + + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_8_BIT: + return AudioFormat.ENCODING_PCM_8BIT; + + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_FLOAT: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_8_24_BIT: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_24_BIT_PACKED: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_32_BIT: + return AudioFormat.ENCODING_PCM_FLOAT; + + case android.media.audio.common.AudioFormat.AC3: + return AudioFormat.ENCODING_AC3; + + case android.media.audio.common.AudioFormat.E_AC3: + return AudioFormat.ENCODING_E_AC3; + + case android.media.audio.common.AudioFormat.DTS: + return AudioFormat.ENCODING_DTS; + + case android.media.audio.common.AudioFormat.DTS_HD: + return AudioFormat.ENCODING_DTS_HD; + + case android.media.audio.common.AudioFormat.MP3: + return AudioFormat.ENCODING_MP3; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_LC: + return AudioFormat.ENCODING_AAC_LC; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_HE_V1: + return AudioFormat.ENCODING_AAC_HE_V1; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_HE_V2: + return AudioFormat.ENCODING_AAC_HE_V2; + + case android.media.audio.common.AudioFormat.IEC61937: + return AudioFormat.ENCODING_IEC61937; + + case android.media.audio.common.AudioFormat.DOLBY_TRUEHD: + return AudioFormat.ENCODING_DOLBY_TRUEHD; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_ELD: + return AudioFormat.ENCODING_AAC_ELD; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_XHE: + return AudioFormat.ENCODING_AAC_XHE; + + case android.media.audio.common.AudioFormat.AC4: + return AudioFormat.ENCODING_AC4; + + case android.media.audio.common.AudioFormat.E_AC3 + | android.media.audio.common.AudioFormat.E_AC3_SUB_JOC: + return AudioFormat.ENCODING_E_AC3_JOC; + + case android.media.audio.common.AudioFormat.MAT: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_1_0: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_2_0: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_2_1: + return AudioFormat.ENCODING_DOLBY_MAT; + + case android.media.audio.common.AudioFormat.DEFAULT: + return AudioFormat.ENCODING_DEFAULT; + + default: + return AudioFormat.ENCODING_INVALID; + } + } + + public static int api2aidlModelParameter(int apiParam) { + switch (apiParam) { + case ModelParams.THRESHOLD_FACTOR: + return android.media.soundtrigger_middleware.ModelParameter.THRESHOLD_FACTOR; + default: + return android.media.soundtrigger_middleware.ModelParameter.INVALID; + } + } + + public static int aidl2apiChannelInMask(int aidlMask) { + // We're assuming AudioFormat.CHANNEL_IN_* constants are kept in sync with + // android.media.audio.common.AudioChannelMask. + return aidlMask; + } + + public static SoundTrigger.ModelParamRange aidl2apiModelParameterRange( + @Nullable ModelParameterRange aidlRange) { + if (aidlRange == null) { + return null; + } + return new SoundTrigger.ModelParamRange(aidlRange.minInclusive, aidlRange.maxInclusive); + } +} diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 86f3eec4109e..5484df416e79 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -22,18 +22,29 @@ 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.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; import android.app.ActivityThread; +import android.content.Context; import android.media.AudioFormat; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; 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.util.Log; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.UUID; /** @@ -44,6 +55,7 @@ import java.util.UUID; */ @SystemApi public class SoundTrigger { + private static final String TAG = "SoundTrigger"; private SoundTrigger() { } @@ -119,15 +131,15 @@ public class SoundTrigger { * recognition callback event */ public final boolean returnsTriggerInEvent; - ModuleProperties(int id, String implementor, String description, - String uuid, int version, int maxSoundModels, int maxKeyphrases, + ModuleProperties(int id, @NonNull String implementor, @NonNull String description, + @NonNull String uuid, int version, int maxSoundModels, int maxKeyphrases, int maxUsers, int recognitionModes, boolean supportsCaptureTransition, int maxBufferMs, boolean supportsConcurrentCapture, int powerConsumptionMw, boolean returnsTriggerInEvent) { this.id = id; - this.implementor = implementor; - this.description = description; - this.uuid = UUID.fromString(uuid); + this.implementor = requireNonNull(implementor); + this.description = requireNonNull(description); + this.uuid = UUID.fromString(requireNonNull(uuid)); this.version = version; this.maxSoundModels = maxSoundModels; this.maxKeyphrases = maxKeyphrases; @@ -231,6 +243,7 @@ public class SoundTrigger { /** Unique sound model identifier */ @UnsupportedAppUsage + @NonNull public final UUID uuid; /** Sound model type (e.g. TYPE_KEYPHRASE); */ @@ -238,17 +251,20 @@ public class SoundTrigger { /** Unique sound model vendor identifier */ @UnsupportedAppUsage + @NonNull public final UUID vendorUuid; /** Opaque data. For use by vendor implementation and enrollment application */ @UnsupportedAppUsage + @NonNull public final byte[] data; - public SoundModel(UUID uuid, UUID vendorUuid, int type, byte[] data) { - this.uuid = uuid; - this.vendorUuid = vendorUuid; + public SoundModel(@NonNull UUID uuid, @Nullable UUID vendorUuid, int type, + @Nullable byte[] data) { + this.uuid = requireNonNull(uuid); + this.vendorUuid = vendorUuid != null ? vendorUuid : new UUID(0, 0); this.type = type; - this.data = data; + this.data = data != null ? data : new byte[0]; } @Override @@ -271,8 +287,6 @@ public class SoundTrigger { if (!(obj instanceof SoundModel)) return false; SoundModel other = (SoundModel) obj; - if (!Arrays.equals(data, other.data)) - return false; if (type != other.type) return false; if (uuid == null) { @@ -285,6 +299,8 @@ public class SoundTrigger { return false; } else if (!vendorUuid.equals(other.vendorUuid)) return false; + if (!Arrays.equals(data, other.data)) + return false; return true; } } @@ -306,24 +322,28 @@ public class SoundTrigger { /** Locale of the keyphrase. JAVA Locale string e.g en_US */ @UnsupportedAppUsage + @NonNull public final String 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 + @NonNull public final int[] users; @UnsupportedAppUsage - public Keyphrase(int id, int recognitionModes, String locale, String text, int[] users) { + public Keyphrase(int id, int recognitionModes, @NonNull String locale, @NonNull String text, + @Nullable int[] users) { this.id = id; this.recognitionModes = recognitionModes; - this.locale = locale; - this.text = text; - this.users = users; + this.locale = requireNonNull(locale); + this.text = requireNonNull(text); + this.users = users != null ? users : new int[0]; } public static final @android.annotation.NonNull Parcelable.Creator<Keyphrase> CREATOR @@ -427,13 +447,15 @@ public class SoundTrigger { public static class KeyphraseSoundModel extends SoundModel implements Parcelable { /** Key phrases in this sound model */ @UnsupportedAppUsage + @NonNull public final Keyphrase[] keyphrases; // keyword phrases in model @UnsupportedAppUsage public KeyphraseSoundModel( - UUID uuid, UUID vendorUuid, byte[] data, Keyphrase[] keyphrases) { + @NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, + @Nullable Keyphrase[] keyphrases) { super(uuid, vendorUuid, TYPE_KEYPHRASE, data); - this.keyphrases = keyphrases; + this.keyphrases = keyphrases != null ? keyphrases : new Keyphrase[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseSoundModel> CREATOR @@ -528,7 +550,8 @@ public class SoundTrigger { }; @UnsupportedAppUsage - public GenericSoundModel(UUID uuid, UUID vendorUuid, byte[] data) { + public GenericSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, + @Nullable byte[] data) { super(uuid, vendorUuid, TYPE_GENERIC_SOUND, data); } @@ -648,6 +671,12 @@ public class SoundTrigger { * @hide */ public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 0x4; + /** + * Generic (non-speech) recognition. + * + * @hide + */ + public static final int RECOGNITION_MODE_GENERIC = 0x8; /** * Status codes for {@link RecognitionEvent} @@ -739,6 +768,7 @@ public class SoundTrigger { * * @hide */ + @NonNull public final AudioFormat captureFormat; /** * Opaque data for use by system applications who know about voice engine internals, @@ -747,13 +777,14 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage + @NonNull public final byte[] data; /** @hide */ @UnsupportedAppUsage public RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, - boolean triggerInData, AudioFormat captureFormat, byte[] data) { + boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data) { this.status = status; this.soundModelHandle = soundModelHandle; this.captureAvailable = captureAvailable; @@ -761,8 +792,8 @@ public class SoundTrigger { this.captureDelayMs = captureDelayMs; this.capturePreambleMs = capturePreambleMs; this.triggerInData = triggerInData; - this.captureFormat = captureFormat; - this.data = data; + this.captureFormat = requireNonNull(captureFormat); + this.data = data != null ? data : new byte[0]; } /** @@ -965,19 +996,21 @@ public class SoundTrigger { /** List of all keyphrases in the sound model for which recognition should be performed with * options for each keyphrase. */ @UnsupportedAppUsage + @NonNull public final KeyphraseRecognitionExtra keyphrases[]; /** Opaque data for use by system applications who know about voice engine internals, * typically during enrollment. */ @UnsupportedAppUsage + @NonNull public final byte[] data; @UnsupportedAppUsage public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, - KeyphraseRecognitionExtra[] keyphrases, byte[] data) { + @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data) { this.captureRequested = captureRequested; this.allowMultipleTriggers = allowMultipleTriggers; - this.keyphrases = keyphrases; - this.data = data; + this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; + this.data = data != null ? data : new byte[0]; } public static final @android.annotation.NonNull Parcelable.Creator<RecognitionConfig> CREATOR @@ -1126,15 +1159,17 @@ public class SoundTrigger { /** Confidence levels for all users recognized (KeyphraseRecognitionEvent) or to * be recognized (RecognitionConfig) */ @UnsupportedAppUsage + @NonNull public final ConfidenceLevel[] confidenceLevels; @UnsupportedAppUsage public KeyphraseRecognitionExtra(int id, int recognitionModes, int coarseConfidenceLevel, - ConfidenceLevel[] confidenceLevels) { + @Nullable ConfidenceLevel[] confidenceLevels) { this.id = id; this.recognitionModes = recognitionModes; this.coarseConfidenceLevel = coarseConfidenceLevel; - this.confidenceLevels = confidenceLevels; + this.confidenceLevels = + confidenceLevels != null ? confidenceLevels : new ConfidenceLevel[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseRecognitionExtra> CREATOR @@ -1217,16 +1252,18 @@ public class SoundTrigger { 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 public KeyphraseRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, - boolean triggerInData, AudioFormat captureFormat, byte[] data, - KeyphraseRecognitionExtra[] keyphraseExtras) { + boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, + @Nullable KeyphraseRecognitionExtra[] keyphraseExtras) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data); - this.keyphraseExtras = keyphraseExtras; + this.keyphraseExtras = + keyphraseExtras != null ? keyphraseExtras : new KeyphraseRecognitionExtra[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseRecognitionEvent> CREATOR @@ -1343,8 +1380,8 @@ public class SoundTrigger { @UnsupportedAppUsage public GenericRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, - int capturePreambleMs, boolean triggerInData, AudioFormat captureFormat, - byte[] data) { + int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, + @Nullable byte[] data) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data); @@ -1408,13 +1445,14 @@ public class SoundTrigger { /** The updated sound model handle */ public final int soundModelHandle; /** New sound model data */ + @NonNull public final byte[] data; @UnsupportedAppUsage - SoundModelEvent(int status, int soundModelHandle, byte[] data) { + SoundModelEvent(int status, int soundModelHandle, @Nullable byte[] data) { this.status = status; this.soundModelHandle = soundModelHandle; - this.data = data; + this.data = data != null ? data : new byte[0]; } public static final @android.annotation.NonNull Parcelable.Creator<SoundModelEvent> CREATOR @@ -1498,8 +1536,9 @@ public class SoundTrigger { * @hide */ public static final int SERVICE_STATE_DISABLED = 1; - - /** + private static Object mServiceLock = new Object(); + private static ISoundTriggerMiddlewareService mService; + /** * @return returns current package name. */ static String getCurrentOpPackageName() { @@ -1523,25 +1562,22 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage - public static int listModules(ArrayList<ModuleProperties> modules) { - return listModules(getCurrentOpPackageName(), modules); + public static int listModules(@NonNull ArrayList<ModuleProperties> modules) { + try { + SoundTriggerModuleDescriptor[] descs = getService().listModules(); + modules.clear(); + modules.ensureCapacity(descs.length); + for (SoundTriggerModuleDescriptor desc : descs) { + modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc)); + } + return STATUS_OK; + } catch (RemoteException e) { + Log.e(TAG, "Exception caught", e); + return STATUS_DEAD_OBJECT; + } } /** - * Returns a list of descriptors for all hardware modules loaded. - * @param opPackageName - * @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 - */ - private static native int listModules(String opPackageName, - ArrayList<ModuleProperties> modules); - - /** * Get an interface on a hardware module to control sound models and recognition on * this module. * @param moduleId Sound module system identifier {@link ModuleProperties#id}. mandatory. @@ -1553,14 +1589,40 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage - public static SoundTriggerModule attachModule(int moduleId, - StatusListener listener, - Handler handler) { - if (listener == null) { + public static @NonNull SoundTriggerModule attachModule(int moduleId, + @NonNull StatusListener listener, + @Nullable Handler handler) { + Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); + try { + return new SoundTriggerModule(getService(), moduleId, listener, looper); + } catch (RemoteException e) { + Log.e(TAG, "", e); return null; } - SoundTriggerModule module = new SoundTriggerModule(moduleId, listener, handler); - return module; + } + + private static ISoundTriggerMiddlewareService getService() { + synchronized (mServiceLock) { + while (true) { + IBinder binder = null; + try { + binder = + ServiceManager.getServiceOrThrow( + Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE); + binder.linkToDeath(() -> { + synchronized (mServiceLock) { + mService = null; + } + }, 0); + mService = ISoundTriggerMiddlewareService.Stub.asInterface(binder); + break; + } catch (Exception e) { + Log.e(TAG, "Failed to bind to soundtrigger service", e); + } + } + return mService; + } + } /** diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java index b16ef5c43346..7cf560019239 100644 --- a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java +++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java @@ -16,14 +16,23 @@ package android.hardware.soundtrigger; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; -import android.hardware.soundtrigger.SoundTrigger.ModelParamRange; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.SoundModel; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; - -import java.lang.ref.WeakReference; +import android.os.RemoteException; +import android.util.Log; /** * The SoundTriggerModule provides APIs to control sound models and sound detection @@ -32,39 +41,47 @@ import java.lang.ref.WeakReference; * @hide */ public class SoundTriggerModule { - @UnsupportedAppUsage - private long mNativeContext; - - @UnsupportedAppUsage - private int mId; - private NativeEventHandlerDelegate mEventHandlerDelegate; + private static final String TAG = "SoundTriggerModule"; - // to be kept in sync with core/jni/android_hardware_SoundTrigger.cpp private static final int EVENT_RECOGNITION = 1; private static final int EVENT_SERVICE_DIED = 2; - private static final int EVENT_SOUNDMODEL = 3; - private static final int EVENT_SERVICE_STATE_CHANGE = 4; + private static final int EVENT_SERVICE_STATE_CHANGE = 3; + @UnsupportedAppUsage + private int mId; + private EventHandlerDelegate mEventHandlerDelegate; + private ISoundTriggerModule mService; - SoundTriggerModule(int moduleId, SoundTrigger.StatusListener listener, Handler handler) { + SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service, + int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper) + throws RemoteException { mId = moduleId; - mEventHandlerDelegate = new NativeEventHandlerDelegate(listener, handler); - native_setup(SoundTrigger.getCurrentOpPackageName(), - new WeakReference<SoundTriggerModule>(this)); + mEventHandlerDelegate = new EventHandlerDelegate(listener, looper); + mService = service.attach(moduleId, mEventHandlerDelegate); + mService.asBinder().linkToDeath(mEventHandlerDelegate, 0); } - private native void native_setup(String opPackageName, Object moduleThis); @Override protected void finalize() { - native_finalize(); + detach(); } - private native void native_finalize(); /** * Detach from this module. The {@link SoundTrigger.StatusListener} callback will not be called * anymore and associated resources will be released. - * */ + * All models must have been unloaded prior to detaching. + */ @UnsupportedAppUsage - public native void detach(); + public synchronized void detach() { + try { + if (mService != null) { + mService.asBinder().unlinkToDeath(mEventHandlerDelegate, 0); + mService.detach(); + mService = null; + } + } catch (Exception e) { + handleException(e); + } + } /** * Load a {@link SoundTrigger.SoundModel} to the hardware. A sound model must be loaded in @@ -82,7 +99,26 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int loadSoundModel(SoundTrigger.SoundModel model, int[] soundModelHandle); + public synchronized int loadSoundModel(@NonNull SoundTrigger.SoundModel model, + @NonNull int[] soundModelHandle) { + try { + if (model instanceof SoundTrigger.GenericSoundModel) { + SoundModel aidlModel = ConversionUtil.api2aidlGenericSoundModel( + (SoundTrigger.GenericSoundModel) model); + soundModelHandle[0] = mService.loadModel(aidlModel); + return SoundTrigger.STATUS_OK; + } + if (model instanceof SoundTrigger.KeyphraseSoundModel) { + PhraseSoundModel aidlModel = ConversionUtil.api2aidlPhraseSoundModel( + (SoundTrigger.KeyphraseSoundModel) model); + soundModelHandle[0] = mService.loadPhraseModel(aidlModel); + return SoundTrigger.STATUS_OK; + } + return SoundTrigger.STATUS_BAD_VALUE; + } catch (Exception e) { + return handleException(e); + } + } /** * Unload a {@link SoundTrigger.SoundModel} and abort any pendiong recognition @@ -97,7 +133,14 @@ public class SoundTriggerModule { * service fails */ @UnsupportedAppUsage - public native int unloadSoundModel(int soundModelHandle); + public synchronized int unloadSoundModel(int soundModelHandle) { + try { + mService.unloadModel(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Start listening to all key phrases in a {@link SoundTrigger.SoundModel}. @@ -117,7 +160,16 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int startRecognition(int soundModelHandle, SoundTrigger.RecognitionConfig config); + public synchronized int startRecognition(int soundModelHandle, + SoundTrigger.RecognitionConfig config) { + try { + mService.startRecognition(soundModelHandle, + ConversionUtil.api2aidlRecognitionConfig(config)); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Stop listening to all key phrases in a {@link SoundTrigger.SoundModel} @@ -133,12 +185,20 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int stopRecognition(int soundModelHandle); + public synchronized int stopRecognition(int soundModelHandle) { + try { + mService.stopRecognition(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Get the current state of a {@link SoundTrigger.SoundModel}. - * The state will be returned asynchronously as a {@link SoundTrigger#RecognitionEvent} - * in the callback registered in the {@link SoundTrigger.startRecognition} method. + * The state will be returned asynchronously as a {@link SoundTrigger.RecognitionEvent} + * in the callback registered in the + * {@link SoundTrigger#attachModule(int, SoundTrigger.StatusListener, Handler)} method. * @param soundModelHandle The sound model handle indicating which model's state to return * @return - {@link SoundTrigger#STATUS_OK} in case of success * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error @@ -150,46 +210,71 @@ public class SoundTriggerModule { * service fails * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ - public native int getModelState(int soundModelHandle); + public synchronized int getModelState(int soundModelHandle) { + try { + mService.forceRecognitionEvent(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Set a model specific {@link ModelParams} with the given value. This - * parameter will keep its value for the duration the model is loaded regardless of starting and + * parameter will keep its value for the duration the model is loaded regardless of starting + * and * stopping recognition. Once the model is unloaded, the value will be lost. - * {@link SoundTriggerModule#isParameterSupported} should be checked first before calling this - * method. + * {@link SoundTriggerModule#queryParameter(int, int)} should be checked first before calling + * this method. * * @param soundModelHandle handle of model to apply parameter - * @param modelParam {@link ModelParams} - * @param value Value to set + * @param modelParam {@link ModelParams} + * @param value Value to set * @return - {@link SoundTrigger#STATUS_OK} in case of success - * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached - * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter - * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or - * if API is not supported by HAL + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter + * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or + * if API is not supported by HAL */ - public native int setParameter(int soundModelHandle, - @ModelParams int modelParam, int value); + public synchronized int setParameter(int soundModelHandle, @ModelParams int modelParam, + int value) { + try { + mService.setModelParameter(soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam), value); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Get a model specific {@link ModelParams}. This parameter will keep its value * for the duration the model is loaded regardless of starting and stopping recognition. * Once the model is unloaded, the value will be lost. If the value is not set, a default * value is returned. See {@link ModelParams} for parameter default values. - * {@link SoundTriggerModule#isParameterSupported} should be checked first before + * {@link SoundTriggerModule#queryParameter(int, int)} should be checked first before * calling this method. Otherwise, an exception can be thrown. * * @param soundModelHandle handle of model to get parameter - * @param modelParam {@link ModelParams} + * @param modelParam {@link ModelParams} * @return value of parameter * @throws UnsupportedOperationException if hal or model do not support this API. - * {@link SoundTriggerModule#isParameterSupported} should be checked first. - * @throws IllegalArgumentException if invalid model handle or parameter is passed. - * {@link SoundTriggerModule#isParameterSupported} should be checked first. + * {@link SoundTriggerModule#queryParameter(int, int)} + * should + * be checked first. + * @throws IllegalArgumentException if invalid model handle or parameter is passed. + * {@link SoundTriggerModule#queryParameter(int, int)} + * should be checked first. */ - public native int getParameter(int soundModelHandle, - @ModelParams int modelParam) - throws UnsupportedOperationException, IllegalArgumentException; + public synchronized int getParameter(int soundModelHandle, @ModelParams int modelParam) + throws UnsupportedOperationException, IllegalArgumentException { + try { + return mService.getModelParameter(soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Determine if parameter control is supported for the given model handle. @@ -197,85 +282,98 @@ public class SoundTriggerModule { * {@link SoundTriggerModule#getParameter}. * * @param soundModelHandle handle of model to get parameter - * @param modelParam {@link ModelParams} + * @param modelParam {@link ModelParams} * @return supported range of parameter, null if not supported */ @Nullable - public native ModelParamRange queryParameter(int soundModelHandle, @ModelParams int modelParam); + public synchronized SoundTrigger.ModelParamRange queryParameter(int soundModelHandle, + @ModelParams int modelParam) { + try { + return ConversionUtil.aidl2apiModelParameterRange(mService.queryModelParameterSupport( + soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam))); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private int handleException(Exception e) { + Log.e(TAG, "", e); + if (e instanceof NullPointerException) { + return SoundTrigger.STATUS_NO_INIT; + } + if (e instanceof RemoteException) { + return SoundTrigger.STATUS_DEAD_OBJECT; + } + if (e instanceof IllegalArgumentException) { + return SoundTrigger.STATUS_BAD_VALUE; + } + if (e instanceof IllegalStateException) { + return SoundTrigger.STATUS_INVALID_OPERATION; + } + return SoundTrigger.STATUS_ERROR; + } - private class NativeEventHandlerDelegate { + private class EventHandlerDelegate extends ISoundTriggerCallback.Stub implements + IBinder.DeathRecipient { private final Handler mHandler; - NativeEventHandlerDelegate(final SoundTrigger.StatusListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - looper = Looper.getMainLooper(); - } + EventHandlerDelegate(@NonNull final SoundTrigger.StatusListener listener, + @NonNull Looper looper) { // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - switch(msg.what) { + // implement the event handler delegate + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { case EVENT_RECOGNITION: - if (listener != null) { - listener.onRecognition( - (SoundTrigger.RecognitionEvent)msg.obj); - } - break; - case EVENT_SOUNDMODEL: - if (listener != null) { - listener.onSoundModelUpdate( - (SoundTrigger.SoundModelEvent)msg.obj); - } + listener.onRecognition( + (SoundTrigger.RecognitionEvent) msg.obj); break; case EVENT_SERVICE_STATE_CHANGE: - if (listener != null) { - listener.onServiceStateChange(msg.arg1); - } + listener.onServiceStateChange(msg.arg1); break; case EVENT_SERVICE_DIED: - if (listener != null) { - listener.onServiceDied(); - } + listener.onServiceDied(); break; default: + Log.e(TAG, "Unknown message: " + msg.toString()); break; - } } - }; - } else { - mHandler = null; - } + } + }; } - Handler handler() { - return mHandler; + @Override + public synchronized void onRecognition(int handle, RecognitionEvent event) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_RECOGNITION, + ConversionUtil.aidl2apiRecognitionEvent(handle, event)); + mHandler.sendMessage(m); } - } - @SuppressWarnings("unused") - @UnsupportedAppUsage - private static void postEventFromNative(Object module_ref, - int what, int arg1, int arg2, Object obj) { - SoundTriggerModule module = (SoundTriggerModule)((WeakReference)module_ref).get(); - if (module == null) { - return; + @Override + public synchronized void onPhraseRecognition(int handle, PhraseRecognitionEvent event) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_RECOGNITION, + ConversionUtil.aidl2apiPhraseRecognitionEvent(handle, event)); + mHandler.sendMessage(m); } - NativeEventHandlerDelegate delegate = module.mEventHandlerDelegate; - if (delegate != null) { - Handler handler = delegate.handler(); - if (handler != null) { - Message m = handler.obtainMessage(what, arg1, arg2, obj); - handler.sendMessage(m); - } + @Override + public synchronized void onRecognitionAvailabilityChange(boolean available) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_SERVICE_STATE_CHANGE, + available ? SoundTrigger.SERVICE_STATE_ENABLED + : SoundTrigger.SERVICE_STATE_DISABLED); + mHandler.sendMessage(m); + } + + @Override + public synchronized void binderDied() { + Message m = mHandler.obtainMessage(EVENT_SERVICE_DIED); + mHandler.sendMessage(m); } } } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index b91d3595fe6a..a2f6a62157ed 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -177,7 +177,6 @@ cc_library_shared { "android_hardware_HardwareBuffer.cpp", "android_hardware_SensorManager.cpp", "android_hardware_SerialPort.cpp", - "android_hardware_SoundTrigger.cpp", "android_hardware_UsbDevice.cpp", "android_hardware_UsbDeviceConnection.cpp", "android_hardware_UsbRequest.cpp", @@ -259,7 +258,6 @@ cc_library_shared { "libpdfium", "libimg_utils", "libnetd_client", - "libsoundtrigger", "libprocessgroup", "libnativebridge_lazy", "libnativeloader_lazy", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b8fd3ad43ce5..f7a994f7071f 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -80,7 +80,6 @@ extern int register_android_hardware_camera2_DngCreator(JNIEnv *env); extern int register_android_hardware_HardwareBuffer(JNIEnv *env); extern int register_android_hardware_SensorManager(JNIEnv *env); extern int register_android_hardware_SerialPort(JNIEnv *env); -extern int register_android_hardware_SoundTrigger(JNIEnv *env); extern int register_android_hardware_UsbDevice(JNIEnv *env); extern int register_android_hardware_UsbDeviceConnection(JNIEnv *env); extern int register_android_hardware_UsbRequest(JNIEnv *env); @@ -1508,7 +1507,6 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_hardware_HardwareBuffer), REG_JNI(register_android_hardware_SensorManager), REG_JNI(register_android_hardware_SerialPort), - REG_JNI(register_android_hardware_SoundTrigger), REG_JNI(register_android_hardware_UsbDevice), REG_JNI(register_android_hardware_UsbDeviceConnection), REG_JNI(register_android_hardware_UsbRequest), diff --git a/core/jni/android_hardware_SoundTrigger.cpp b/core/jni/android_hardware_SoundTrigger.cpp deleted file mode 100644 index 4376b0b4ac42..000000000000 --- a/core/jni/android_hardware_SoundTrigger.cpp +++ /dev/null @@ -1,1071 +0,0 @@ -/* -** -** Copyright 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. -*/ - -//#define LOG_NDEBUG 0 -#define LOG_TAG "SoundTrigger-JNI" -#include <utils/Log.h> - -#include "jni.h" -#include <nativehelper/JNIHelp.h> -#include <nativehelper/ScopedUtfChars.h> -#include "core_jni_helpers.h" -#include <system/sound_trigger.h> -#include <soundtrigger/SoundTriggerCallback.h> -#include <soundtrigger/SoundTrigger.h> -#include <utils/RefBase.h> -#include <utils/Vector.h> -#include <binder/IMemory.h> -#include <binder/MemoryDealer.h> -#include "android_media_AudioFormat.h" - -using namespace android; - -static jclass gArrayListClass; -static struct { - jmethodID add; -} gArrayListMethods; - -static jclass gUUIDClass; -static struct { - jmethodID toString; -} gUUIDMethods; - -static const char* const kUnsupportedOperationExceptionClassPathName = - "java/lang/UnsupportedOperationException"; -static jclass gUnsupportedOperationExceptionClass; -static const char* const kIllegalArgumentExceptionClassPathName = - "java/lang/IllegalArgumentException"; -static jclass gIllegalArgumentExceptionClass; - -static const char* const kSoundTriggerClassPathName = "android/hardware/soundtrigger/SoundTrigger"; -static jclass gSoundTriggerClass; - -static const char* const kModuleClassPathName = "android/hardware/soundtrigger/SoundTriggerModule"; -static jclass gModuleClass; -static struct { - jfieldID mNativeContext; - jfieldID mId; -} gModuleFields; -static jmethodID gPostEventFromNative; - -static const char* const kModulePropertiesClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ModuleProperties"; -static jclass gModulePropertiesClass; -static jmethodID gModulePropertiesCstor; - -static const char* const kSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$SoundModel"; -static jclass gSoundModelClass; -static struct { - jfieldID uuid; - jfieldID vendorUuid; - jfieldID data; -} gSoundModelFields; - -static const char* const kGenericSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$GenericSoundModel"; -static jclass gGenericSoundModelClass; - -static const char* const kKeyphraseClassPathName = - "android/hardware/soundtrigger/SoundTrigger$Keyphrase"; -static jclass gKeyphraseClass; -static struct { - jfieldID id; - jfieldID recognitionModes; - jfieldID locale; - jfieldID text; - jfieldID users; -} gKeyphraseFields; - -static const char* const kKeyphraseSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseSoundModel"; -static jclass gKeyphraseSoundModelClass; -static struct { - jfieldID keyphrases; -} gKeyphraseSoundModelFields; - -static const char* const kModelParamRangeClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ModelParamRange"; -static jclass gModelParamRangeClass; -static jmethodID gModelParamRangeCstor; - -static const char* const kRecognitionConfigClassPathName = - "android/hardware/soundtrigger/SoundTrigger$RecognitionConfig"; -static jclass gRecognitionConfigClass; -static struct { - jfieldID captureRequested; - jfieldID keyphrases; - jfieldID data; -} gRecognitionConfigFields; - -static const char* const kRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$RecognitionEvent"; -static jclass gRecognitionEventClass; -static jmethodID gRecognitionEventCstor; - -static const char* const kKeyphraseRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionEvent"; -static jclass gKeyphraseRecognitionEventClass; -static jmethodID gKeyphraseRecognitionEventCstor; - -static const char* const kGenericRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$GenericRecognitionEvent"; -static jclass gGenericRecognitionEventClass; -static jmethodID gGenericRecognitionEventCstor; - -static const char* const kKeyphraseRecognitionExtraClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra"; -static jclass gKeyphraseRecognitionExtraClass; -static jmethodID gKeyphraseRecognitionExtraCstor; -static struct { - jfieldID id; - jfieldID recognitionModes; - jfieldID coarseConfidenceLevel; - jfieldID confidenceLevels; -} gKeyphraseRecognitionExtraFields; - -static const char* const kConfidenceLevelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ConfidenceLevel"; -static jclass gConfidenceLevelClass; -static jmethodID gConfidenceLevelCstor; -static struct { - jfieldID userId; - jfieldID confidenceLevel; -} gConfidenceLevelFields; - -static const char* const kAudioFormatClassPathName = - "android/media/AudioFormat"; -static jclass gAudioFormatClass; -static jmethodID gAudioFormatCstor; - -static const char* const kSoundModelEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$SoundModelEvent"; -static jclass gSoundModelEventClass; -static jmethodID gSoundModelEventCstor; - -static Mutex gLock; - -enum { - SOUNDTRIGGER_STATUS_OK = 0, - SOUNDTRIGGER_STATUS_ERROR = INT_MIN, - SOUNDTRIGGER_PERMISSION_DENIED = -1, - SOUNDTRIGGER_STATUS_NO_INIT = -19, - SOUNDTRIGGER_STATUS_BAD_VALUE = -22, - SOUNDTRIGGER_STATUS_DEAD_OBJECT = -32, - SOUNDTRIGGER_INVALID_OPERATION = -38, -}; - -enum { - SOUNDTRIGGER_EVENT_RECOGNITION = 1, - SOUNDTRIGGER_EVENT_SERVICE_DIED = 2, - SOUNDTRIGGER_EVENT_SOUNDMODEL = 3, - SOUNDTRIGGER_EVENT_SERVICE_STATE_CHANGE = 4, -}; - -static jint throwUnsupportedOperationException(JNIEnv *env) -{ - return env->ThrowNew(gUnsupportedOperationExceptionClass, nullptr); -} - -static jint throwIllegalArgumentException(JNIEnv *env) -{ - return env->ThrowNew(gIllegalArgumentExceptionClass, nullptr); -} - -// ---------------------------------------------------------------------------- -// ref-counted object for callbacks -class JNISoundTriggerCallback: public SoundTriggerCallback -{ -public: - JNISoundTriggerCallback(JNIEnv* env, jobject thiz, jobject weak_thiz); - ~JNISoundTriggerCallback(); - - virtual void onRecognitionEvent(struct sound_trigger_recognition_event *event); - virtual void onSoundModelEvent(struct sound_trigger_model_event *event); - virtual void onServiceStateChange(sound_trigger_service_state_t state); - virtual void onServiceDied(); - -private: - jclass mClass; // Reference to SoundTrigger class - jobject mObject; // Weak ref to SoundTrigger Java object to call on -}; - -JNISoundTriggerCallback::JNISoundTriggerCallback(JNIEnv* env, jobject thiz, jobject weak_thiz) -{ - - // Hold onto the SoundTriggerModule class for use in calling the static method - // that posts events to the application thread. - jclass clazz = env->GetObjectClass(thiz); - if (clazz == NULL) { - ALOGE("Can't find class %s", kModuleClassPathName); - return; - } - mClass = (jclass)env->NewGlobalRef(clazz); - - // We use a weak reference so the SoundTriggerModule object can be garbage collected. - // The reference is only used as a proxy for callbacks. - mObject = env->NewGlobalRef(weak_thiz); -} - -JNISoundTriggerCallback::~JNISoundTriggerCallback() -{ - // remove global references - JNIEnv *env = AndroidRuntime::getJNIEnv(); - env->DeleteGlobalRef(mObject); - env->DeleteGlobalRef(mClass); -} - -void JNISoundTriggerCallback::onRecognitionEvent(struct sound_trigger_recognition_event *event) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject jEvent = NULL; - jbyteArray jData = NULL; - - if (event->data_size) { - jData = env->NewByteArray(event->data_size); - jbyte *nData = env->GetByteArrayElements(jData, NULL); - memcpy(nData, (char *)event + event->data_offset, event->data_size); - env->ReleaseByteArrayElements(jData, nData, 0); - } - - jobject jAudioFormat = NULL; - if (event->trigger_in_data || event->capture_available) { - jint channelMask = (jint)audio_channel_mask_get_bits(event->audio_config.channel_mask); - jint channelIndexMask = (jint)AUDIO_CHANNEL_NONE; - - switch (audio_channel_mask_get_representation(event->audio_config.channel_mask)) { - case AUDIO_CHANNEL_REPRESENTATION_INDEX: - channelIndexMask = channelMask; - channelMask = (jint)AUDIO_CHANNEL_NONE; - break; - default: - break; - } - jAudioFormat = env->NewObject(gAudioFormatClass, - gAudioFormatCstor, - audioFormatFromNative(event->audio_config.format), - event->audio_config.sample_rate, - channelMask, - channelIndexMask); - - } - if (event->type == SOUND_MODEL_TYPE_KEYPHRASE) { - struct sound_trigger_phrase_recognition_event *phraseEvent = - (struct sound_trigger_phrase_recognition_event *)event; - - jobjectArray jExtras = env->NewObjectArray(phraseEvent->num_phrases, - gKeyphraseRecognitionExtraClass, NULL); - if (jExtras == NULL) { - return; - } - - for (size_t i = 0; i < phraseEvent->num_phrases; i++) { - jobjectArray jConfidenceLevels = env->NewObjectArray( - phraseEvent->phrase_extras[i].num_levels, - gConfidenceLevelClass, NULL); - - if (jConfidenceLevels == NULL) { - return; - } - for (size_t j = 0; j < phraseEvent->phrase_extras[i].num_levels; j++) { - jobject jConfidenceLevel = env->NewObject(gConfidenceLevelClass, - gConfidenceLevelCstor, - phraseEvent->phrase_extras[i].levels[j].user_id, - phraseEvent->phrase_extras[i].levels[j].level); - env->SetObjectArrayElement(jConfidenceLevels, j, jConfidenceLevel); - env->DeleteLocalRef(jConfidenceLevel); - } - - jobject jNewExtra = env->NewObject(gKeyphraseRecognitionExtraClass, - gKeyphraseRecognitionExtraCstor, - phraseEvent->phrase_extras[i].id, - phraseEvent->phrase_extras[i].recognition_modes, - phraseEvent->phrase_extras[i].confidence_level, - jConfidenceLevels); - - if (jNewExtra == NULL) { - return; - } - env->SetObjectArrayElement(jExtras, i, jNewExtra); - env->DeleteLocalRef(jNewExtra); - env->DeleteLocalRef(jConfidenceLevels); - } - jEvent = env->NewObject(gKeyphraseRecognitionEventClass, gKeyphraseRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData, jExtras); - env->DeleteLocalRef(jExtras); - } else if (event->type == SOUND_MODEL_TYPE_GENERIC) { - jEvent = env->NewObject(gGenericRecognitionEventClass, gGenericRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData); - } else { - jEvent = env->NewObject(gRecognitionEventClass, gRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData); - } - - if (jAudioFormat != NULL) { - env->DeleteLocalRef(jAudioFormat); - } - if (jData != NULL) { - env->DeleteLocalRef(jData); - } - - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_RECOGNITION, 0, 0, jEvent); - - env->DeleteLocalRef(jEvent); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onSoundModelEvent(struct sound_trigger_model_event *event) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject jEvent = NULL; - jbyteArray jData = NULL; - - if (event->data_size) { - jData = env->NewByteArray(event->data_size); - jbyte *nData = env->GetByteArrayElements(jData, NULL); - memcpy(nData, (char *)event + event->data_offset, event->data_size); - env->ReleaseByteArrayElements(jData, nData, 0); - } - - jEvent = env->NewObject(gSoundModelEventClass, gSoundModelEventCstor, - event->status, event->model, jData); - - env->DeleteLocalRef(jData); - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SOUNDMODEL, 0, 0, jEvent); - env->DeleteLocalRef(jEvent); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onServiceStateChange(sound_trigger_service_state_t state) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SERVICE_STATE_CHANGE, state, 0, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onServiceDied() -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SERVICE_DIED, 0, 0, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -// ---------------------------------------------------------------------------- - -static sp<SoundTrigger> getSoundTrigger(JNIEnv* env, jobject thiz) -{ - Mutex::Autolock l(gLock); - SoundTrigger* const st = (SoundTrigger*)env->GetLongField(thiz, - gModuleFields.mNativeContext); - return sp<SoundTrigger>(st); -} - -static sp<SoundTrigger> setSoundTrigger(JNIEnv* env, jobject thiz, const sp<SoundTrigger>& module) -{ - Mutex::Autolock l(gLock); - sp<SoundTrigger> old = (SoundTrigger*)env->GetLongField(thiz, - gModuleFields.mNativeContext); - if (module.get()) { - module->incStrong((void*)setSoundTrigger); - } - if (old != 0) { - old->decStrong((void*)setSoundTrigger); - } - env->SetLongField(thiz, gModuleFields.mNativeContext, (jlong)module.get()); - return old; -} - - -static jint -android_hardware_SoundTrigger_listModules(JNIEnv *env, jobject clazz, - jstring opPackageName, jobject jModules) -{ - ALOGV("listModules"); - - if (jModules == NULL) { - ALOGE("listModules NULL AudioPatch ArrayList"); - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - if (!env->IsInstanceOf(jModules, gArrayListClass)) { - ALOGE("listModules not an arraylist"); - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - - unsigned int numModules = 0; - struct sound_trigger_module_descriptor *nModules = NULL; - - ScopedUtfChars opPackageNameStr(env, opPackageName); - const String16 opPackageNameString16 = String16(opPackageNameStr.c_str()); - - status_t status = SoundTrigger::listModules(opPackageNameString16, nModules, &numModules); - if (status != NO_ERROR || numModules == 0) { - return (jint)status; - } - - nModules = (struct sound_trigger_module_descriptor *) - calloc(numModules, sizeof(struct sound_trigger_module_descriptor)); - - status = SoundTrigger::listModules(opPackageNameString16, nModules, &numModules); - ALOGV("listModules SoundTrigger::listModules status %d numModules %d", status, numModules); - - if (status != NO_ERROR) { - numModules = 0; - } - - for (size_t i = 0; i < numModules; i++) { - char str[SOUND_TRIGGER_MAX_STRING_LEN]; - - jstring implementor = env->NewStringUTF(nModules[i].properties.implementor); - jstring description = env->NewStringUTF(nModules[i].properties.description); - SoundTrigger::guidToString(&nModules[i].properties.uuid, - str, - SOUND_TRIGGER_MAX_STRING_LEN); - jstring uuid = env->NewStringUTF(str); - - ALOGV("listModules module %zu id %d description %s maxSoundModels %d", - i, nModules[i].handle, nModules[i].properties.description, - nModules[i].properties.max_sound_models); - - jobject newModuleDesc = env->NewObject(gModulePropertiesClass, gModulePropertiesCstor, - nModules[i].handle, - implementor, description, uuid, - nModules[i].properties.version, - nModules[i].properties.max_sound_models, - nModules[i].properties.max_key_phrases, - nModules[i].properties.max_users, - nModules[i].properties.recognition_modes, - nModules[i].properties.capture_transition, - nModules[i].properties.max_buffer_ms, - nModules[i].properties.concurrent_capture, - nModules[i].properties.power_consumption_mw, - nModules[i].properties.trigger_in_event); - - env->DeleteLocalRef(implementor); - env->DeleteLocalRef(description); - env->DeleteLocalRef(uuid); - if (newModuleDesc == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - env->CallBooleanMethod(jModules, gArrayListMethods.add, newModuleDesc); - } - -exit: - free(nModules); - return (jint) status; -} - -static void -android_hardware_SoundTrigger_setup(JNIEnv *env, jobject thiz, - jstring opPackageName, jobject weak_this) -{ - ALOGV("setup"); - - ScopedUtfChars opPackageNameStr(env, opPackageName); - const String16 opPackageNameString16 = String16(opPackageNameStr.c_str()); - - sp<JNISoundTriggerCallback> callback = new JNISoundTriggerCallback(env, thiz, weak_this); - - sound_trigger_module_handle_t handle = - (sound_trigger_module_handle_t)env->GetIntField(thiz, gModuleFields.mId); - - sp<SoundTrigger> module = SoundTrigger::attach(opPackageNameString16, handle, callback); - if (module == 0) { - return; - } - - setSoundTrigger(env, thiz, module); -} - -static void -android_hardware_SoundTrigger_detach(JNIEnv *env, jobject thiz) -{ - ALOGV("detach"); - sp<SoundTrigger> module = setSoundTrigger(env, thiz, 0); - ALOGV("detach module %p", module.get()); - if (module != 0) { - ALOGV("detach module->detach()"); - module->detach(); - } -} - -static void -android_hardware_SoundTrigger_finalize(JNIEnv *env, jobject thiz) -{ - ALOGV("finalize"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module != 0) { - ALOGW("SoundTrigger finalized without being detached"); - } - android_hardware_SoundTrigger_detach(env, thiz); -} - -static jint -android_hardware_SoundTrigger_loadSoundModel(JNIEnv *env, jobject thiz, - jobject jSoundModel, jintArray jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - jbyte *nData = NULL; - struct sound_trigger_sound_model *nSoundModel; - jbyteArray jData; - sp<MemoryDealer> memoryDealer; - sp<IMemory> memory; - size_t size; - sound_model_handle_t handle = 0; - jobject jUuid; - jstring jUuidString; - const char *nUuidString; - - ALOGV("loadSoundModel"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (jHandle == NULL) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - jsize jHandleLen = env->GetArrayLength(jHandle); - if (jHandleLen == 0) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - jint *nHandle = env->GetIntArrayElements(jHandle, NULL); - if (nHandle == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (!env->IsInstanceOf(jSoundModel, gSoundModelClass)) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - size_t offset; - sound_trigger_sound_model_type_t type; - if (env->IsInstanceOf(jSoundModel, gKeyphraseSoundModelClass)) { - offset = sizeof(struct sound_trigger_phrase_sound_model); - type = SOUND_MODEL_TYPE_KEYPHRASE; - } else if (env->IsInstanceOf(jSoundModel, gGenericSoundModelClass)) { - offset = sizeof(struct sound_trigger_generic_sound_model); - type = SOUND_MODEL_TYPE_GENERIC; - } else { - offset = sizeof(struct sound_trigger_sound_model); - type = SOUND_MODEL_TYPE_UNKNOWN; - } - - jUuid = env->GetObjectField(jSoundModel, gSoundModelFields.uuid); - jUuidString = (jstring)env->CallObjectMethod(jUuid, gUUIDMethods.toString); - nUuidString = env->GetStringUTFChars(jUuidString, NULL); - sound_trigger_uuid_t nUuid; - SoundTrigger::stringToGuid(nUuidString, &nUuid); - env->ReleaseStringUTFChars(jUuidString, nUuidString); - env->DeleteLocalRef(jUuidString); - - sound_trigger_uuid_t nVendorUuid; - jUuid = env->GetObjectField(jSoundModel, gSoundModelFields.vendorUuid); - if (jUuid != NULL) { - jUuidString = (jstring)env->CallObjectMethod(jUuid, gUUIDMethods.toString); - nUuidString = env->GetStringUTFChars(jUuidString, NULL); - SoundTrigger::stringToGuid(nUuidString, &nVendorUuid); - env->ReleaseStringUTFChars(jUuidString, nUuidString); - env->DeleteLocalRef(jUuidString); - } else { - SoundTrigger::stringToGuid("00000000-0000-0000-0000-000000000000", &nVendorUuid); - } - - jData = (jbyteArray)env->GetObjectField(jSoundModel, gSoundModelFields.data); - if (jData == NULL) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - size = env->GetArrayLength(jData); - - nData = env->GetByteArrayElements(jData, NULL); - if (jData == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - - memoryDealer = new MemoryDealer(offset + size, "SoundTrigge-JNI::LoadModel"); - if (memoryDealer == 0) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - memory = memoryDealer->allocate(offset + size); - if (memory == 0 || memory->unsecurePointer() == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - - nSoundModel = (struct sound_trigger_sound_model *)memory->unsecurePointer(); - - nSoundModel->type = type; - nSoundModel->uuid = nUuid; - nSoundModel->vendor_uuid = nVendorUuid; - nSoundModel->data_size = size; - nSoundModel->data_offset = offset; - memcpy((char *)nSoundModel + offset, nData, size); - if (type == SOUND_MODEL_TYPE_KEYPHRASE) { - struct sound_trigger_phrase_sound_model *phraseModel = - (struct sound_trigger_phrase_sound_model *)nSoundModel; - - jobjectArray jPhrases = - (jobjectArray)env->GetObjectField(jSoundModel, gKeyphraseSoundModelFields.keyphrases); - if (jPhrases == NULL) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - - size_t numPhrases = env->GetArrayLength(jPhrases); - phraseModel->num_phrases = numPhrases; - ALOGV("loadSoundModel numPhrases %zu", numPhrases); - for (size_t i = 0; i < numPhrases; i++) { - jobject jPhrase = env->GetObjectArrayElement(jPhrases, i); - phraseModel->phrases[i].id = - env->GetIntField(jPhrase,gKeyphraseFields.id); - phraseModel->phrases[i].recognition_mode = - env->GetIntField(jPhrase,gKeyphraseFields.recognitionModes); - - jintArray jUsers = (jintArray)env->GetObjectField(jPhrase, gKeyphraseFields.users); - phraseModel->phrases[i].num_users = env->GetArrayLength(jUsers); - jint *nUsers = env->GetIntArrayElements(jUsers, NULL); - memcpy(phraseModel->phrases[i].users, - nUsers, - phraseModel->phrases[i].num_users * sizeof(int)); - env->ReleaseIntArrayElements(jUsers, nUsers, 0); - env->DeleteLocalRef(jUsers); - - jstring jLocale = (jstring)env->GetObjectField(jPhrase, gKeyphraseFields.locale); - const char *nLocale = env->GetStringUTFChars(jLocale, NULL); - strncpy(phraseModel->phrases[i].locale, - nLocale, - SOUND_TRIGGER_MAX_LOCALE_LEN); - jstring jText = (jstring)env->GetObjectField(jPhrase, gKeyphraseFields.text); - const char *nText = env->GetStringUTFChars(jText, NULL); - strncpy(phraseModel->phrases[i].text, - nText, - SOUND_TRIGGER_MAX_STRING_LEN); - - env->ReleaseStringUTFChars(jLocale, nLocale); - env->DeleteLocalRef(jLocale); - env->ReleaseStringUTFChars(jText, nText); - env->DeleteLocalRef(jText); - ALOGV("loadSoundModel phrases %zu text %s locale %s", - i, phraseModel->phrases[i].text, phraseModel->phrases[i].locale); - env->DeleteLocalRef(jPhrase); - } - env->DeleteLocalRef(jPhrases); - } else if (type == SOUND_MODEL_TYPE_GENERIC) { - /* No initialization needed */ - } - status = module->loadSoundModel(memory, &handle); - ALOGV("loadSoundModel status %d handle %d", status, handle); - -exit: - if (nHandle != NULL) { - nHandle[0] = (jint)handle; - env->ReleaseIntArrayElements(jHandle, nHandle, NULL); - } - if (nData != NULL) { - env->ReleaseByteArrayElements(jData, nData, NULL); - } - return status; -} - -static jint -android_hardware_SoundTrigger_unloadSoundModel(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("unloadSoundModel"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->unloadSoundModel((sound_model_handle_t)jHandle); - - return status; -} - -static jint -android_hardware_SoundTrigger_startRecognition(JNIEnv *env, jobject thiz, - jint jHandle, jobject jConfig) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("startRecognition"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - - if (!env->IsInstanceOf(jConfig, gRecognitionConfigClass)) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - - jbyteArray jData = (jbyteArray)env->GetObjectField(jConfig, gRecognitionConfigFields.data); - jsize dataSize = 0; - jbyte *nData = NULL; - if (jData != NULL) { - dataSize = env->GetArrayLength(jData); - if (dataSize == 0) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - nData = env->GetByteArrayElements(jData, NULL); - if (nData == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - } - - size_t totalSize = sizeof(struct sound_trigger_recognition_config) + dataSize; - sp<MemoryDealer> memoryDealer = - new MemoryDealer(totalSize, "SoundTrigge-JNI::StartRecognition"); - if (memoryDealer == 0) { - return SOUNDTRIGGER_STATUS_ERROR; - } - sp<IMemory> memory = memoryDealer->allocate(totalSize); - if (memory == 0 || memory->unsecurePointer() == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (dataSize != 0) { - memcpy((char *)memory->unsecurePointer() + sizeof(struct sound_trigger_recognition_config), - nData, - dataSize); - env->ReleaseByteArrayElements(jData, nData, 0); - } - env->DeleteLocalRef(jData); - struct sound_trigger_recognition_config *config = - (struct sound_trigger_recognition_config *)memory->unsecurePointer(); - config->data_size = dataSize; - config->data_offset = sizeof(struct sound_trigger_recognition_config); - config->capture_requested = env->GetBooleanField(jConfig, - gRecognitionConfigFields.captureRequested); - - config->num_phrases = 0; - jobjectArray jPhrases = - (jobjectArray)env->GetObjectField(jConfig, gRecognitionConfigFields.keyphrases); - if (jPhrases != NULL) { - config->num_phrases = env->GetArrayLength(jPhrases); - } - ALOGV("startRecognition num phrases %d", config->num_phrases); - for (size_t i = 0; i < config->num_phrases; i++) { - jobject jPhrase = env->GetObjectArrayElement(jPhrases, i); - config->phrases[i].id = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.id); - config->phrases[i].recognition_modes = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.recognitionModes); - config->phrases[i].confidence_level = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.coarseConfidenceLevel); - config->phrases[i].num_levels = 0; - jobjectArray jConfidenceLevels = (jobjectArray)env->GetObjectField(jPhrase, - gKeyphraseRecognitionExtraFields.confidenceLevels); - if (jConfidenceLevels != NULL) { - config->phrases[i].num_levels = env->GetArrayLength(jConfidenceLevels); - } - ALOGV("startRecognition phrase %zu num_levels %d", i, config->phrases[i].num_levels); - for (size_t j = 0; j < config->phrases[i].num_levels; j++) { - jobject jConfidenceLevel = env->GetObjectArrayElement(jConfidenceLevels, j); - config->phrases[i].levels[j].user_id = env->GetIntField(jConfidenceLevel, - gConfidenceLevelFields.userId); - config->phrases[i].levels[j].level = env->GetIntField(jConfidenceLevel, - gConfidenceLevelFields.confidenceLevel); - env->DeleteLocalRef(jConfidenceLevel); - } - ALOGV("startRecognition phrases %zu", i); - env->DeleteLocalRef(jConfidenceLevels); - env->DeleteLocalRef(jPhrase); - } - env->DeleteLocalRef(jPhrases); - - status = module->startRecognition(jHandle, memory); - return status; -} - -static jint -android_hardware_SoundTrigger_stopRecognition(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("stopRecognition"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->stopRecognition(jHandle); - return status; -} - -static jint -android_hardware_SoundTrigger_getModelState(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("getModelState"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->getModelState(jHandle); - return status; -} - -static jint -android_hardware_SoundTrigger_setParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam, jint jValue) -{ - ALOGV("setParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_NO_INIT; - } - return module->setParameter(jHandle, (sound_trigger_model_parameter_t) jModelParam, jValue); -} - -static jint -android_hardware_SoundTrigger_getParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam) -{ - ALOGV("getParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - throwUnsupportedOperationException(env); - return -1; - } - - jint nValue; - jint status = module->getParameter(jHandle, - (sound_trigger_model_parameter_t) jModelParam, &nValue); - - switch (status) { - case 0: - return nValue; - case -EINVAL: - throwIllegalArgumentException(env); - break; - default: - throwUnsupportedOperationException(env); - break; - } - - return -1; -} - -static jobject -android_hardware_SoundTrigger_queryParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam) -{ - ALOGV("queryParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == nullptr) { - return nullptr; - } - - sound_trigger_model_parameter_range_t nRange; - jint nValue = module->queryParameter(jHandle, - (sound_trigger_model_parameter_t) jModelParam, &nRange); - - if (nValue != 0) { - ALOGE("failed to query parameter error code: %d", nValue); - return nullptr; - } - - return env->NewObject(gModelParamRangeClass, gModelParamRangeCstor, nRange.start, nRange.end); -} - -static const JNINativeMethod gMethods[] = { - {"listModules", - "(Ljava/lang/String;Ljava/util/ArrayList;)I", - (void *)android_hardware_SoundTrigger_listModules}, -}; - - -static const JNINativeMethod gModuleMethods[] = { - {"native_setup", - "(Ljava/lang/String;Ljava/lang/Object;)V", - (void *)android_hardware_SoundTrigger_setup}, - {"native_finalize", - "()V", - (void *)android_hardware_SoundTrigger_finalize}, - {"detach", - "()V", - (void *)android_hardware_SoundTrigger_detach}, - {"loadSoundModel", - "(Landroid/hardware/soundtrigger/SoundTrigger$SoundModel;[I)I", - (void *)android_hardware_SoundTrigger_loadSoundModel}, - {"unloadSoundModel", - "(I)I", - (void *)android_hardware_SoundTrigger_unloadSoundModel}, - {"startRecognition", - "(ILandroid/hardware/soundtrigger/SoundTrigger$RecognitionConfig;)I", - (void *)android_hardware_SoundTrigger_startRecognition}, - {"stopRecognition", - "(I)I", - (void *)android_hardware_SoundTrigger_stopRecognition}, - {"getModelState", - "(I)I", - (void *)android_hardware_SoundTrigger_getModelState}, - {"setParameter", - "(III)I", - (void *)android_hardware_SoundTrigger_setParameter}, - {"getParameter", - "(II)I", - (void *)android_hardware_SoundTrigger_getParameter}, - {"queryParameter", - "(II)Landroid/hardware/soundtrigger/SoundTrigger$ModelParamRange;", - (void *)android_hardware_SoundTrigger_queryParameter} -}; - -int register_android_hardware_SoundTrigger(JNIEnv *env) -{ - jclass arrayListClass = FindClassOrDie(env, "java/util/ArrayList"); - gArrayListClass = MakeGlobalRefOrDie(env, arrayListClass); - gArrayListMethods.add = GetMethodIDOrDie(env, arrayListClass, "add", "(Ljava/lang/Object;)Z"); - - jclass uuidClass = FindClassOrDie(env, "java/util/UUID"); - gUUIDClass = MakeGlobalRefOrDie(env, uuidClass); - gUUIDMethods.toString = GetMethodIDOrDie(env, uuidClass, "toString", "()Ljava/lang/String;"); - - jclass exUClass = FindClassOrDie(env, kUnsupportedOperationExceptionClassPathName); - gUnsupportedOperationExceptionClass = MakeGlobalRefOrDie(env, exUClass); - - jclass exIClass = FindClassOrDie(env, kIllegalArgumentExceptionClassPathName); - gIllegalArgumentExceptionClass = MakeGlobalRefOrDie(env, exIClass); - - jclass lClass = FindClassOrDie(env, kSoundTriggerClassPathName); - gSoundTriggerClass = MakeGlobalRefOrDie(env, lClass); - - jclass moduleClass = FindClassOrDie(env, kModuleClassPathName); - gModuleClass = MakeGlobalRefOrDie(env, moduleClass); - gPostEventFromNative = GetStaticMethodIDOrDie(env, moduleClass, "postEventFromNative", - "(Ljava/lang/Object;IIILjava/lang/Object;)V"); - gModuleFields.mNativeContext = GetFieldIDOrDie(env, moduleClass, "mNativeContext", "J"); - gModuleFields.mId = GetFieldIDOrDie(env, moduleClass, "mId", "I"); - - jclass modulePropertiesClass = FindClassOrDie(env, kModulePropertiesClassPathName); - gModulePropertiesClass = MakeGlobalRefOrDie(env, modulePropertiesClass); - gModulePropertiesCstor = GetMethodIDOrDie(env, modulePropertiesClass, "<init>", - "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIZIZIZ)V"); - - jclass soundModelClass = FindClassOrDie(env, kSoundModelClassPathName); - gSoundModelClass = MakeGlobalRefOrDie(env, soundModelClass); - gSoundModelFields.uuid = GetFieldIDOrDie(env, soundModelClass, "uuid", "Ljava/util/UUID;"); - gSoundModelFields.vendorUuid = GetFieldIDOrDie(env, soundModelClass, "vendorUuid", - "Ljava/util/UUID;"); - gSoundModelFields.data = GetFieldIDOrDie(env, soundModelClass, "data", "[B"); - - jclass genericSoundModelClass = FindClassOrDie(env, kGenericSoundModelClassPathName); - gGenericSoundModelClass = MakeGlobalRefOrDie(env, genericSoundModelClass); - - jclass keyphraseClass = FindClassOrDie(env, kKeyphraseClassPathName); - gKeyphraseClass = MakeGlobalRefOrDie(env, keyphraseClass); - gKeyphraseFields.id = GetFieldIDOrDie(env, keyphraseClass, "id", "I"); - gKeyphraseFields.recognitionModes = GetFieldIDOrDie(env, keyphraseClass, "recognitionModes", - "I"); - gKeyphraseFields.locale = GetFieldIDOrDie(env, keyphraseClass, "locale", "Ljava/lang/String;"); - gKeyphraseFields.text = GetFieldIDOrDie(env, keyphraseClass, "text", "Ljava/lang/String;"); - gKeyphraseFields.users = GetFieldIDOrDie(env, keyphraseClass, "users", "[I"); - - jclass keyphraseSoundModelClass = FindClassOrDie(env, kKeyphraseSoundModelClassPathName); - gKeyphraseSoundModelClass = MakeGlobalRefOrDie(env, keyphraseSoundModelClass); - gKeyphraseSoundModelFields.keyphrases = GetFieldIDOrDie(env, keyphraseSoundModelClass, - "keyphrases", - "[Landroid/hardware/soundtrigger/SoundTrigger$Keyphrase;"); - - jclass modelParamRangeClass = FindClassOrDie(env, kModelParamRangeClassPathName); - gModelParamRangeClass = MakeGlobalRefOrDie(env, modelParamRangeClass); - gModelParamRangeCstor = GetMethodIDOrDie(env, modelParamRangeClass, "<init>", "(II)V"); - - jclass recognitionEventClass = FindClassOrDie(env, kRecognitionEventClassPathName); - gRecognitionEventClass = MakeGlobalRefOrDie(env, recognitionEventClass); - gRecognitionEventCstor = GetMethodIDOrDie(env, recognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B)V"); - - jclass keyphraseRecognitionEventClass = FindClassOrDie(env, - kKeyphraseRecognitionEventClassPathName); - gKeyphraseRecognitionEventClass = MakeGlobalRefOrDie(env, keyphraseRecognitionEventClass); - gKeyphraseRecognitionEventCstor = GetMethodIDOrDie(env, keyphraseRecognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B[Landroid/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra;)V"); - - jclass genericRecognitionEventClass = FindClassOrDie(env, - kGenericRecognitionEventClassPathName); - gGenericRecognitionEventClass = MakeGlobalRefOrDie(env, genericRecognitionEventClass); - gGenericRecognitionEventCstor = GetMethodIDOrDie(env, genericRecognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B)V"); - - jclass keyRecognitionConfigClass = FindClassOrDie(env, kRecognitionConfigClassPathName); - gRecognitionConfigClass = MakeGlobalRefOrDie(env, keyRecognitionConfigClass); - gRecognitionConfigFields.captureRequested = GetFieldIDOrDie(env, keyRecognitionConfigClass, - "captureRequested", "Z"); - gRecognitionConfigFields.keyphrases = GetFieldIDOrDie(env, keyRecognitionConfigClass, - "keyphrases", "[Landroid/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra;"); - gRecognitionConfigFields.data = GetFieldIDOrDie(env, keyRecognitionConfigClass, "data", "[B"); - - jclass keyphraseRecognitionExtraClass = FindClassOrDie(env, - kKeyphraseRecognitionExtraClassPathName); - gKeyphraseRecognitionExtraClass = MakeGlobalRefOrDie(env, keyphraseRecognitionExtraClass); - gKeyphraseRecognitionExtraCstor = GetMethodIDOrDie(env, keyphraseRecognitionExtraClass, - "<init>", "(III[Landroid/hardware/soundtrigger/SoundTrigger$ConfidenceLevel;)V"); - gKeyphraseRecognitionExtraFields.id = GetFieldIDOrDie(env, gKeyphraseRecognitionExtraClass, - "id", "I"); - gKeyphraseRecognitionExtraFields.recognitionModes = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "recognitionModes", "I"); - gKeyphraseRecognitionExtraFields.coarseConfidenceLevel = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "coarseConfidenceLevel", "I"); - gKeyphraseRecognitionExtraFields.confidenceLevels = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "confidenceLevels", - "[Landroid/hardware/soundtrigger/SoundTrigger$ConfidenceLevel;"); - - jclass confidenceLevelClass = FindClassOrDie(env, kConfidenceLevelClassPathName); - gConfidenceLevelClass = MakeGlobalRefOrDie(env, confidenceLevelClass); - gConfidenceLevelCstor = GetMethodIDOrDie(env, confidenceLevelClass, "<init>", "(II)V"); - gConfidenceLevelFields.userId = GetFieldIDOrDie(env, confidenceLevelClass, "userId", "I"); - gConfidenceLevelFields.confidenceLevel = GetFieldIDOrDie(env, confidenceLevelClass, - "confidenceLevel", "I"); - - jclass audioFormatClass = FindClassOrDie(env, kAudioFormatClassPathName); - gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass); - gAudioFormatCstor = GetMethodIDOrDie(env, audioFormatClass, "<init>", "(IIII)V"); - - jclass soundModelEventClass = FindClassOrDie(env, kSoundModelEventClassPathName); - gSoundModelEventClass = MakeGlobalRefOrDie(env, soundModelEventClass); - gSoundModelEventCstor = GetMethodIDOrDie(env, soundModelEventClass, "<init>", "(II[B)V"); - - - RegisterMethodsOrDie(env, kSoundTriggerClassPathName, gMethods, NELEM(gMethods)); - return RegisterMethodsOrDie(env, kModuleClassPathName, gModuleMethods, NELEM(gModuleMethods)); -} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c925744fea3b..cb5b4a595ac3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4410,6 +4410,12 @@ <permission android:name="android.permission.MANAGE_SOUND_TRIGGER" android:protectionLevel="signature|privileged" /> + <!-- Allows preempting sound trigger recognitions for the sake of capturing audio on + implementations which do not support running both concurrently. + @hide --> + <permission android:name="android.permission.PREEMPT_SOUND_TRIGGER" + android:protectionLevel="signature|privileged" /> + <!-- Must be required by system/priv apps implementing sound trigger detection services @hide @SystemApi --> diff --git a/data/etc/platform.xml b/data/etc/platform.xml index 80098c5a81f5..0574775712a6 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -157,6 +157,7 @@ <assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="audioserver" /> <assign-permission name="android.permission.UPDATE_APP_OPS_STATS" uid="audioserver" /> <assign-permission name="android.permission.PACKAGE_USAGE_STATS" uid="audioserver" /> + <assign-permission name="android.permission.PREEMPT_SOUND_TRIGGER" uid="audioserver" /> <assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" /> <assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" /> diff --git a/media/Android.bp b/media/Android.bp index 4b50b7ad1aea..75ccb227e5c5 100644 --- a/media/Android.bp +++ b/media/Android.bp @@ -102,3 +102,67 @@ java_library { srcs: [":framework-media-annotation-srcs"], installable: false, } + +aidl_interface { + name: "audio_common-aidl", + local_include_dir: "java", + srcs: [ + "java/android/media/audio/common/AudioChannelMask.aidl", + "java/android/media/audio/common/AudioConfig.aidl", + "java/android/media/audio/common/AudioFormat.aidl", + "java/android/media/audio/common/AudioOffloadInfo.aidl", + "java/android/media/audio/common/AudioStreamType.aidl", + "java/android/media/audio/common/AudioUsage.aidl", + ], + backend: + { + cpp: { + enabled: true, + }, + java: { + // Already generated as part of the entire media java library. + enabled: false, + }, + }, +} + +aidl_interface { + name: "soundtrigger_middleware-aidl", + local_include_dir: "java", + srcs: [ + "java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl", + "java/android/media/soundtrigger_middleware/ModelParameter.aidl", + "java/android/media/soundtrigger_middleware/ModelParameterRange.aidl", + "java/android/media/soundtrigger_middleware/Phrase.aidl", + "java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl", + "java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl", + "java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl", + "java/android/media/soundtrigger_middleware/RecognitionConfig.aidl", + "java/android/media/soundtrigger_middleware/RecognitionEvent.aidl", + "java/android/media/soundtrigger_middleware/RecognitionMode.aidl", + "java/android/media/soundtrigger_middleware/RecognitionStatus.aidl", + "java/android/media/soundtrigger_middleware/SoundModel.aidl", + "java/android/media/soundtrigger_middleware/SoundModelType.aidl", + "java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl", + "java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl", + "java/android/media/soundtrigger_middleware/Status.aidl", + ], + backend: + { + cpp: { + enabled: true, + }, + java: { + // Already generated as part of the entire media java library. + enabled: false, + }, + ndk: { + // Not currently needed, and disabled because of b/146172425 + enabled: false, + }, + }, + imports: [ "audio_common-aidl" ], +} diff --git a/media/java/android/media/audio/common/AudioChannelMask.aidl b/media/java/android/media/audio/common/AudioChannelMask.aidl new file mode 100644 index 000000000000..b9b08e6921bc --- /dev/null +++ b/media/java/android/media/audio/common/AudioChannelMask.aidl @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * A channel mask per se only defines the presence or absence of a channel, not + * the order. + * + * The channel order convention is that channels are interleaved in order from + * least significant channel mask bit to most significant channel mask bit, + * with unused bits skipped. For example for stereo, LEFT would be first, + * followed by RIGHT. + * Any exceptions to this convention are noted at the appropriate API. + * + * AudioChannelMask is an opaque type and its internal layout should not be + * assumed as it may change in the future. Instead, always use functions + * to examine it. + * + * These are the current representations: + * + * REPRESENTATION_POSITION + * is a channel mask representation for position assignment. Each low-order + * bit corresponds to the spatial position of a transducer (output), or + * interpretation of channel (input). The user of a channel mask needs to + * know the context of whether it is for output or input. The constants + * OUT_* or IN_* apply to the bits portion. It is not permitted for no bits + * to be set. + * + * REPRESENTATION_INDEX + * is a channel mask representation for index assignment. Each low-order + * bit corresponds to a selected channel. There is no platform + * interpretation of the various bits. There is no concept of output or + * input. It is not permitted for no bits to be set. + * + * All other representations are reserved for future use. + * + * Warning: current representation distinguishes between input and output, but + * this will not the be case in future revisions of the platform. Wherever there + * is an ambiguity between input and output that is currently resolved by + * checking the channel mask, the implementer should look for ways to fix it + * with additional information outside of the mask. + * + * {@hide} + */ +@Backing(type="int") +enum AudioChannelMask { + /** + * must be 0 for compatibility + */ + REPRESENTATION_POSITION = 0, + /** + * 1 is reserved for future use + */ + REPRESENTATION_INDEX = 2, + /** + * 3 is reserved for future use + * + * + * These can be a complete value of AudioChannelMask + */ + NONE = 0x0, + INVALID = 0xC0000000, + /** + * These can be the bits portion of an AudioChannelMask + * with representation REPRESENTATION_POSITION. + * + * + * output channels + */ + OUT_FRONT_LEFT = 0x1, + OUT_FRONT_RIGHT = 0x2, + OUT_FRONT_CENTER = 0x4, + OUT_LOW_FREQUENCY = 0x8, + OUT_BACK_LEFT = 0x10, + OUT_BACK_RIGHT = 0x20, + OUT_FRONT_LEFT_OF_CENTER = 0x40, + OUT_FRONT_RIGHT_OF_CENTER = 0x80, + OUT_BACK_CENTER = 0x100, + OUT_SIDE_LEFT = 0x200, + OUT_SIDE_RIGHT = 0x400, + OUT_TOP_CENTER = 0x800, + OUT_TOP_FRONT_LEFT = 0x1000, + OUT_TOP_FRONT_CENTER = 0x2000, + OUT_TOP_FRONT_RIGHT = 0x4000, + OUT_TOP_BACK_LEFT = 0x8000, + OUT_TOP_BACK_CENTER = 0x10000, + OUT_TOP_BACK_RIGHT = 0x20000, + OUT_TOP_SIDE_LEFT = 0x40000, + OUT_TOP_SIDE_RIGHT = 0x80000, + /** + * Haptic channel characteristics are specific to a device and + * only used to play device specific resources (eg: ringtones). + * The HAL can freely map A and B to haptic controllers, the + * framework shall not interpret those values and forward them + * from the device audio assets. + */ + OUT_HAPTIC_A = 0x20000000, + OUT_HAPTIC_B = 0x10000000, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// OUT_MONO = OUT_FRONT_LEFT, +// OUT_STEREO = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT), +// OUT_2POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_LOW_FREQUENCY), +// OUT_2POINT0POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_2POINT1POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT | OUT_LOW_FREQUENCY), +// OUT_3POINT0POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_3POINT1POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT | OUT_LOW_FREQUENCY), +// OUT_QUAD = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_BACK_LEFT | OUT_BACK_RIGHT), +// OUT_QUAD_BACK = OUT_QUAD, +// /** +// * like OUT_QUAD_BACK with *_SIDE_* instead of *_BACK_* +// */ +// OUT_QUAD_SIDE = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_SURROUND = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_BACK_CENTER), +// OUT_PENTA = (OUT_QUAD | OUT_FRONT_CENTER), +// OUT_5POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT), +// OUT_5POINT1_BACK = OUT_5POINT1, +// /** +// * like OUT_5POINT1_BACK with *_SIDE_* instead of *_BACK_* +// */ +// OUT_5POINT1_SIDE = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_5POINT1POINT2 = (OUT_5POINT1 | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_5POINT1POINT4 = (OUT_5POINT1 | OUT_TOP_FRONT_LEFT | OUT_TOP_FRONT_RIGHT | OUT_TOP_BACK_LEFT | OUT_TOP_BACK_RIGHT), +// OUT_6POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT | OUT_BACK_CENTER), +// /** +// * matches the correct AudioFormat.CHANNEL_OUT_7POINT1_SURROUND +// */ +// OUT_7POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_7POINT1POINT2 = (OUT_7POINT1 | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_7POINT1POINT4 = (OUT_7POINT1 | OUT_TOP_FRONT_LEFT | OUT_TOP_FRONT_RIGHT | OUT_TOP_BACK_LEFT | OUT_TOP_BACK_RIGHT), +// OUT_MONO_HAPTIC_A = (OUT_FRONT_LEFT | OUT_HAPTIC_A), +// OUT_STEREO_HAPTIC_A = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_HAPTIC_A), +// OUT_HAPTIC_AB = (OUT_HAPTIC_A | OUT_HAPTIC_B), +// OUT_MONO_HAPTIC_AB = (OUT_FRONT_LEFT | OUT_HAPTIC_A | OUT_HAPTIC_B), +// OUT_STEREO_HAPTIC_AB = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_HAPTIC_A | OUT_HAPTIC_B), + /** + * These are bits only, not complete values + * + * + * input channels + */ + IN_LEFT = 0x4, + IN_RIGHT = 0x8, + IN_FRONT = 0x10, + IN_BACK = 0x20, + IN_LEFT_PROCESSED = 0x40, + IN_RIGHT_PROCESSED = 0x80, + IN_FRONT_PROCESSED = 0x100, + IN_BACK_PROCESSED = 0x200, + IN_PRESSURE = 0x400, + IN_X_AXIS = 0x800, + IN_Y_AXIS = 0x1000, + IN_Z_AXIS = 0x2000, + IN_BACK_LEFT = 0x10000, + IN_BACK_RIGHT = 0x20000, + IN_CENTER = 0x40000, + IN_LOW_FREQUENCY = 0x100000, + IN_TOP_LEFT = 0x200000, + IN_TOP_RIGHT = 0x400000, + IN_VOICE_UPLINK = 0x4000, + IN_VOICE_DNLINK = 0x8000, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// IN_MONO = IN_FRONT, +// IN_STEREO = (IN_LEFT | IN_RIGHT), +// IN_FRONT_BACK = (IN_FRONT | IN_BACK), +// IN_6 = (IN_LEFT | IN_RIGHT | IN_FRONT | IN_BACK | IN_LEFT_PROCESSED | IN_RIGHT_PROCESSED), +// IN_2POINT0POINT2 = (IN_LEFT | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT), +// IN_2POINT1POINT2 = (IN_LEFT | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT | IN_LOW_FREQUENCY), +// IN_3POINT0POINT2 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT), +// IN_3POINT1POINT2 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT | IN_LOW_FREQUENCY), +// IN_5POINT1 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_BACK_LEFT | IN_BACK_RIGHT | IN_LOW_FREQUENCY), +// IN_VOICE_UPLINK_MONO = (IN_VOICE_UPLINK | IN_MONO), +// IN_VOICE_DNLINK_MONO = (IN_VOICE_DNLINK | IN_MONO), +// IN_VOICE_CALL_MONO = (IN_VOICE_UPLINK_MONO | IN_VOICE_DNLINK_MONO), +// COUNT_MAX = 30, +// INDEX_HDR = REPRESENTATION_INDEX << COUNT_MAX, +// INDEX_MASK_1 = INDEX_HDR | ((1 << 1) - 1), +// INDEX_MASK_2 = INDEX_HDR | ((1 << 2) - 1), +// INDEX_MASK_3 = INDEX_HDR | ((1 << 3) - 1), +// INDEX_MASK_4 = INDEX_HDR | ((1 << 4) - 1), +// INDEX_MASK_5 = INDEX_HDR | ((1 << 5) - 1), +// INDEX_MASK_6 = INDEX_HDR | ((1 << 6) - 1), +// INDEX_MASK_7 = INDEX_HDR | ((1 << 7) - 1), +// INDEX_MASK_8 = INDEX_HDR | ((1 << 8) - 1), +// INDEX_MASK_9 = INDEX_HDR | ((1 << 9) - 1), +// INDEX_MASK_10 = INDEX_HDR | ((1 << 10) - 1), +// INDEX_MASK_11 = INDEX_HDR | ((1 << 11) - 1), +// INDEX_MASK_12 = INDEX_HDR | ((1 << 12) - 1), +// INDEX_MASK_13 = INDEX_HDR | ((1 << 13) - 1), +// INDEX_MASK_14 = INDEX_HDR | ((1 << 14) - 1), +// INDEX_MASK_15 = INDEX_HDR | ((1 << 15) - 1), +// INDEX_MASK_16 = INDEX_HDR | ((1 << 16) - 1), +// INDEX_MASK_17 = INDEX_HDR | ((1 << 17) - 1), +// INDEX_MASK_18 = INDEX_HDR | ((1 << 18) - 1), +// INDEX_MASK_19 = INDEX_HDR | ((1 << 19) - 1), +// INDEX_MASK_20 = INDEX_HDR | ((1 << 20) - 1), +// INDEX_MASK_21 = INDEX_HDR | ((1 << 21) - 1), +// INDEX_MASK_22 = INDEX_HDR | ((1 << 22) - 1), +// INDEX_MASK_23 = INDEX_HDR | ((1 << 23) - 1), +// INDEX_MASK_24 = INDEX_HDR | ((1 << 24) - 1), +} diff --git a/media/java/android/media/audio/common/AudioConfig.aidl b/media/java/android/media/audio/common/AudioConfig.aidl new file mode 100644 index 000000000000..50dd796e1fa0 --- /dev/null +++ b/media/java/android/media/audio/common/AudioConfig.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +import android.media.audio.common.AudioFormat; +import android.media.audio.common.AudioOffloadInfo; + +/** + * Commonly used audio stream configuration parameters. + * + * {@hide} + */ +parcelable AudioConfig { + int sampleRateHz; + int channelMask; + AudioFormat format; + AudioOffloadInfo offloadInfo; + long frameCount; +} diff --git a/media/java/android/media/audio/common/AudioFormat.aidl b/media/java/android/media/audio/common/AudioFormat.aidl new file mode 100644 index 000000000000..aadc8e26cce3 --- /dev/null +++ b/media/java/android/media/audio/common/AudioFormat.aidl @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * Audio format is a 32-bit word that consists of: + * main format field (upper 8 bits) + * sub format field (lower 24 bits). + * + * The main format indicates the main codec type. The sub format field indicates + * options and parameters for each format. The sub format is mainly used for + * record to indicate for instance the requested bitrate or profile. It can + * also be used for certain formats to give informations not present in the + * encoded audio stream (e.g. octet alignement for AMR). + * + * {@hide} + */ +@Backing(type="int") +enum AudioFormat { + INVALID = 0xFFFFFFFF, + DEFAULT = 0, + PCM = 0x00000000, + MP3 = 0x01000000, + AMR_NB = 0x02000000, + AMR_WB = 0x03000000, + AAC = 0x04000000, + /** + * Deprecated, Use AAC_HE_V1 + */ + HE_AAC_V1 = 0x05000000, + /** + * Deprecated, Use AAC_HE_V2 + */ + HE_AAC_V2 = 0x06000000, + VORBIS = 0x07000000, + OPUS = 0x08000000, + AC3 = 0x09000000, + E_AC3 = 0x0A000000, + DTS = 0x0B000000, + DTS_HD = 0x0C000000, + /** + * IEC61937 is encoded audio wrapped in 16-bit PCM. + */ + IEC61937 = 0x0D000000, + DOLBY_TRUEHD = 0x0E000000, + EVRC = 0x10000000, + EVRCB = 0x11000000, + EVRCWB = 0x12000000, + EVRCNW = 0x13000000, + AAC_ADIF = 0x14000000, + WMA = 0x15000000, + WMA_PRO = 0x16000000, + AMR_WB_PLUS = 0x17000000, + MP2 = 0x18000000, + QCELP = 0x19000000, + DSD = 0x1A000000, + FLAC = 0x1B000000, + ALAC = 0x1C000000, + APE = 0x1D000000, + AAC_ADTS = 0x1E000000, + SBC = 0x1F000000, + APTX = 0x20000000, + APTX_HD = 0x21000000, + AC4 = 0x22000000, + LDAC = 0x23000000, + /** + * Dolby Metadata-enhanced Audio Transmission + */ + MAT = 0x24000000, + AAC_LATM = 0x25000000, + CELT = 0x26000000, + APTX_ADAPTIVE = 0x27000000, + LHDC = 0x28000000, + LHDC_LL = 0x29000000, + APTX_TWSP = 0x2A000000, + /** + * Deprecated + */ + MAIN_MASK = 0xFF000000, + SUB_MASK = 0x00FFFFFF, + /** + * Subformats + */ + PCM_SUB_16_BIT = 0x1, + PCM_SUB_8_BIT = 0x2, + PCM_SUB_32_BIT = 0x3, + PCM_SUB_8_24_BIT = 0x4, + PCM_SUB_FLOAT = 0x5, + PCM_SUB_24_BIT_PACKED = 0x6, + MP3_SUB_NONE = 0x0, + AMR_SUB_NONE = 0x0, + AAC_SUB_MAIN = 0x1, + AAC_SUB_LC = 0x2, + AAC_SUB_SSR = 0x4, + AAC_SUB_LTP = 0x8, + AAC_SUB_HE_V1 = 0x10, + AAC_SUB_SCALABLE = 0x20, + AAC_SUB_ERLC = 0x40, + AAC_SUB_LD = 0x80, + AAC_SUB_HE_V2 = 0x100, + AAC_SUB_ELD = 0x200, + AAC_SUB_XHE = 0x300, + VORBIS_SUB_NONE = 0x0, + E_AC3_SUB_JOC = 0x1, + MAT_SUB_1_0 = 0x1, + MAT_SUB_2_0 = 0x2, + MAT_SUB_2_1 = 0x3, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// /** +// * Aliases +// * +// * +// * note != AudioFormat.ENCODING_PCM_16BIT +// */ +// PCM_16_BIT = (PCM | PCM_SUB_16_BIT), +// /** +// * note != AudioFormat.ENCODING_PCM_8BIT +// */ +// PCM_8_BIT = (PCM | PCM_SUB_8_BIT), +// PCM_32_BIT = (PCM | PCM_SUB_32_BIT), +// PCM_8_24_BIT = (PCM | PCM_SUB_8_24_BIT), +// PCM_FLOAT = (PCM | PCM_SUB_FLOAT), +// PCM_24_BIT_PACKED = (PCM | PCM_SUB_24_BIT_PACKED), +// AAC_MAIN = (AAC | AAC_SUB_MAIN), +// AAC_LC = (AAC | AAC_SUB_LC), +// AAC_SSR = (AAC | AAC_SUB_SSR), +// AAC_LTP = (AAC | AAC_SUB_LTP), +// AAC_HE_V1 = (AAC | AAC_SUB_HE_V1), +// AAC_SCALABLE = (AAC | AAC_SUB_SCALABLE), +// AAC_ERLC = (AAC | AAC_SUB_ERLC), +// AAC_LD = (AAC | AAC_SUB_LD), +// AAC_HE_V2 = (AAC | AAC_SUB_HE_V2), +// AAC_ELD = (AAC | AAC_SUB_ELD), +// AAC_XHE = (AAC | AAC_SUB_XHE), +// AAC_ADTS_MAIN = (AAC_ADTS | AAC_SUB_MAIN), +// AAC_ADTS_LC = (AAC_ADTS | AAC_SUB_LC), +// AAC_ADTS_SSR = (AAC_ADTS | AAC_SUB_SSR), +// AAC_ADTS_LTP = (AAC_ADTS | AAC_SUB_LTP), +// AAC_ADTS_HE_V1 = (AAC_ADTS | AAC_SUB_HE_V1), +// AAC_ADTS_SCALABLE = (AAC_ADTS | AAC_SUB_SCALABLE), +// AAC_ADTS_ERLC = (AAC_ADTS | AAC_SUB_ERLC), +// AAC_ADTS_LD = (AAC_ADTS | AAC_SUB_LD), +// AAC_ADTS_HE_V2 = (AAC_ADTS | AAC_SUB_HE_V2), +// AAC_ADTS_ELD = (AAC_ADTS | AAC_SUB_ELD), +// AAC_ADTS_XHE = (AAC_ADTS | AAC_SUB_XHE), +// E_AC3_JOC = (E_AC3 | E_AC3_SUB_JOC), +// MAT_1_0 = (MAT | MAT_SUB_1_0), +// MAT_2_0 = (MAT | MAT_SUB_2_0), +// MAT_2_1 = (MAT | MAT_SUB_2_1), +// AAC_LATM_LC = (AAC_LATM | AAC_SUB_LC), +// AAC_LATM_HE_V1 = (AAC_LATM | AAC_SUB_HE_V1), +// AAC_LATM_HE_V2 = (AAC_LATM | AAC_SUB_HE_V2), +} diff --git a/media/java/android/media/audio/common/AudioOffloadInfo.aidl b/media/java/android/media/audio/common/AudioOffloadInfo.aidl new file mode 100644 index 000000000000..ec10d71135ae --- /dev/null +++ b/media/java/android/media/audio/common/AudioOffloadInfo.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +import android.media.audio.common.AudioFormat; +import android.media.audio.common.AudioStreamType; +import android.media.audio.common.AudioUsage; + +/** + * Additional information about the stream passed to hardware decoders. + * + * {@hide} + */ +parcelable AudioOffloadInfo { + int sampleRateHz; + int channelMask; + AudioFormat format; + AudioStreamType streamType; + int bitRatePerSecond; + long durationMicroseconds; + boolean hasVideo; + boolean isStreaming; + int bitWidth; + int bufferSize; + AudioUsage usage; +} + diff --git a/media/java/android/media/audio/common/AudioStreamType.aidl b/media/java/android/media/audio/common/AudioStreamType.aidl new file mode 100644 index 000000000000..c54566726350 --- /dev/null +++ b/media/java/android/media/audio/common/AudioStreamType.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * Audio streams + * + * Audio stream type describing the intended use case of a stream. + * + * {@hide} + */ +@Backing(type="int") +enum AudioStreamType { + DEFAULT = -1, + MIN = 0, + VOICE_CALL = 0, + SYSTEM = 1, + RING = 2, + MUSIC = 3, + ALARM = 4, + NOTIFICATION = 5, + BLUETOOTH_SCO = 6, + ENFORCED_AUDIBLE = 7, + DTMF = 8, + TTS = 9, + ACCESSIBILITY = 10, +} diff --git a/media/java/android/media/audio/common/AudioUsage.aidl b/media/java/android/media/audio/common/AudioUsage.aidl new file mode 100644 index 000000000000..ef348165b22c --- /dev/null +++ b/media/java/android/media/audio/common/AudioUsage.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * {@hide} + */ +@Backing(type="int") +enum AudioUsage { + UNKNOWN = 0, + MEDIA = 1, + VOICE_COMMUNICATION = 2, + VOICE_COMMUNICATION_SIGNALLING = 3, + ALARM = 4, + NOTIFICATION = 5, + NOTIFICATION_TELEPHONY_RINGTONE = 6, + ASSISTANCE_ACCESSIBILITY = 11, + ASSISTANCE_NAVIGATION_GUIDANCE = 12, + ASSISTANCE_SONIFICATION = 13, + GAME = 14, + VIRTUAL_SOURCE = 15, + ASSISTANT = 16, +} diff --git a/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl b/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl new file mode 100644 index 000000000000..3dbc70556bd3 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * A recognition confidence level. + * This type is used to represent either a threshold or an actual detection confidence level. + * + * {@hide} + */ +parcelable ConfidenceLevel { + /** user ID. */ + int userId; + /** + * Confidence level in percent (0 - 100). + * <ul> + * <li>Min level for recognition configuration + * <li>Detected level for recognition event. + * </ul> + */ + int levelPercent; +} diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl new file mode 100644 index 000000000000..149c1cd0447b --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; + +/** + * Main interface for a client to get notifications of events coming from this module. + * + * {@hide} + */ +oneway interface ISoundTriggerCallback { + /** + * Invoked whenever a recognition event is triggered (typically, on recognition, but also in + * case of external aborting of a recognition or a forced recognition event - see the status + * code in the event for determining). + */ + void onRecognition(int modelHandle, in RecognitionEvent event); + /** + * Invoked whenever a phrase recognition event is triggered (typically, on recognition, but + * also in case of external aborting of a recognition or a forced recognition event - see the + * status code in the event for determining). + */ + void onPhraseRecognition(int modelHandle, in PhraseRecognitionEvent event); + /** + * Notifies the client the recognition has become available after previously having been + * unavailable, or vice versa. This method will always be invoked once immediately after + * attachment, and then every time there is a change in availability. + * When availability changes from available to unavailable, all active recognitions are aborted, + * and this event will be sent in addition to the abort event. + */ + void onRecognitionAvailabilityChange(boolean available); +} diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl new file mode 100644 index 000000000000..80333070b7ce --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; + +/** + * Main entry point into this module. + * + * Allows the client to enumerate the available soundtrigger devices and their capabilities, then + * attach to either one of them in order to use it. + * + * {@hide} + */ +interface ISoundTriggerMiddlewareService { + /** + * Query the available modules and their capabilities. + */ + SoundTriggerModuleDescriptor[] listModules(); + + /** + * Attach to one of the available modules. + * listModules() must be called prior to calling this method and the provided handle must be + * one of the handles from the returned list. + */ + ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback); + + /** + * Notify the service that external input capture is taking place. This may cause some of the + * active recognitions to be aborted. + */ + void setExternalCaptureState(boolean active); +}
\ No newline at end of file diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl new file mode 100644 index 000000000000..c4a57857dd3d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; + +/** + * A sound-trigger module. + * + * This interface allows a client to operate a sound-trigger device, intended for low-power + * detection of various sound patterns, represented by a "sound model". + * + * Basic operation is to load a sound model (either a generic one or a "phrase" model), then + * initiate recognition on this model. A trigger will be delivered asynchronously via a callback + * provided by the caller earlier, when attaching to this interface. + * + * In additon to recognition events, this module will also produce abort events in cases where + * recognition has been externally preempted. + * + * {@hide} + */ +interface ISoundTriggerModule { + /** + * Load a sound model. Will return a handle to the model on success or will throw a + * ServiceSpecificException with one of the {@link Status} error codes upon a recoverable error + * (for example, lack of resources of loading a model at the time of call. + * Model must eventually be unloaded using {@link #unloadModel(int)} prior to detaching. + * + * May throw a ServiceSpecificException with an RESOURCE_CONTENTION status to indicate that + * resources required for loading the model are currently consumed by other clients. + */ + int loadModel(in SoundModel model); + + /** + * Load a phrase sound model. Will return a handle to the model on success or will throw a + * ServiceSpecificException with one of the {@link Status} error codes upon a recoverable error + * (for example, lack of resources of loading a model at the time of call. + * Model must eventually be unloaded using unloadModel prior to detaching. + * + * May throw a ServiceSpecificException with an RESOURCE_CONTENTION status to indicate that + * resources required for loading the model are currently consumed by other clients. + */ + int loadPhraseModel(in PhraseSoundModel model); + + /** + * Unload a model, previously loaded with loadModel or loadPhraseModel. After unloading, model + * can no longer be used for recognition and the resources occupied by it are released. + * Model must not be active at the time of unloading. Cient may call stopRecognition to ensure + * that. + */ + void unloadModel(int modelHandle); + + /** + * Initiate recognition on a previously loaded model. + * Recognition event would eventually be delivered via the client-provided callback, typically + * supplied during attachment to this interface. + * + * Once a recognition event is passed to the client, the recognition automatically become + * inactive, unless the event is of the RecognitionStatus.FORCED kind. Client can also shut down + * the recognition explicitly, via stopRecognition. + */ + void startRecognition(int modelHandle, in RecognitionConfig config); + + /** + * Stop a recognition of a previously active recognition. Will NOT generate a recognition event. + * This call is idempotent - calling it on an inactive model has no effect. However, it must + * only be used with a loaded model handle. + */ + void stopRecognition(int modelHandle); + + /** + * Force generation of a recognition event. Handle must be that of a loaded model. If + * recognition is inactive, will do nothing. If recognition is active, will asynchronously + * deliever an event with RecognitionStatus.FORCED status and leave recognition in active state. + * To avoid any race conditions, once an event signalling the automatic stopping of recognition + * is sent, no more forced events will get sent (even if previously requested) until recognition + * is explicitly started again. + * + * Since not all module implementations support this feature, may throw a + * ServiceSpecificException with an OPERATION_NOT_SUPPORTED status. + */ + void forceRecognitionEvent(int modelHandle); + + /** + * Set a model specific parameter with the given value. This parameter + * will keep its value for the duration the model is loaded regardless of starting and stopping + * recognition. Once the model is unloaded, the value will be lost. + * It is expected to check if the handle supports the parameter via the + * queryModelParameterSupport API prior to calling this method. + * + * @param modelHandle The sound model handle indicating which model to modify parameters + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @param value The value to set for the given model parameter + */ + void setModelParameter(int modelHandle, ModelParameter modelParam, int value); + + /** + * Get a model specific parameter. This parameter will keep its value + * for the duration the model is loaded regardless of starting and stopping recognition. + * Once the model is unloaded, the value will be lost. If the value is not set, a default + * value is returned. See ModelParameter for parameter default values. + * It is expected to check if the handle supports the parameter via the + * queryModelParameterSupport API prior to calling this method. + * + * @param modelHandle The sound model associated with given modelParam + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @return Value set to the requested parameter. + */ + int getModelParameter(int modelHandle, ModelParameter modelParam); + + /** + * Determine if parameter control is supported for the given model handle, and its valid value + * range if it is. + * + * @param modelHandle The sound model handle indicating which model to query + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @return If parameter is supported, the return value is its valid range, otherwise null. + */ + @nullable ModelParameterRange queryModelParameterSupport(int modelHandle, + ModelParameter modelParam); + + /** + * Detach from the module, releasing any active resources. + * This will ensure the client callback is no longer called after this call returns. + * All models must have been unloaded prior to calling this method. + */ + void detach(); +}
\ No newline at end of file diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl new file mode 100644 index 000000000000..09936278e93a --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Model specific parameters to be used with parameter set and get APIs. + * + * {@hide} + */ +@Backing(type="int") +enum ModelParameter { + /** + * Placeholder for invalid model parameter used for returning error or + * passing an invalid value. + */ + INVALID = -1, + + /** + * Controls the sensitivity threshold adjustment factor for a given model. + * Negative value corresponds to less sensitive model (high threshold) and + * a positive value corresponds to a more sensitive model (low threshold). + * Default value is 0. + */ + THRESHOLD_FACTOR = 0, +} diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl new file mode 100644 index 000000000000..d6948a87dc6d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Value range for a model parameter. + * + * {@hide} + */ +parcelable ModelParameterRange { + /** Minimum (inclusive) */ + int minInclusive; + /** Maximum (inclusive) */ + int maxInclusive; +} diff --git a/media/java/android/media/soundtrigger_middleware/Phrase.aidl b/media/java/android/media/soundtrigger_middleware/Phrase.aidl new file mode 100644 index 000000000000..98a489f8a6a9 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/Phrase.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Key phrase descriptor. + * + * {@hide} + */ +parcelable Phrase { + /** Unique keyphrase ID assigned at enrollment time. */ + int id; + /** Recognition modes supported by this key phrase (bitfield of RecognitionMode enum). */ + int recognitionModes; + /** List of users IDs associated with this key phrase. */ + int[] users; + /** Locale - Java Locale style (e.g. en_US). */ + String locale; + /** Phrase text. */ + String text; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl new file mode 100644 index 000000000000..6a3ec61d1ebf --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.RecognitionEvent; + +/** + * An event that gets sent to indicate a phrase recognition (or aborting of the recognition + process). + * {@hide} + */ +parcelable PhraseRecognitionEvent { + /** Common recognition event. */ + RecognitionEvent common; + /** List of descriptors for each recognized key phrase */ + PhraseRecognitionExtra[] phraseExtras; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl new file mode 100644 index 000000000000..cb96bf37a95d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ConfidenceLevel; + +/** + * Specialized recognition event for key phrase detection. + * {@hide} + */ +parcelable PhraseRecognitionExtra { + // TODO(ytai): Constants / enums. + + /** keyphrase ID */ + int id; + /** recognition modes used for this keyphrase */ + int recognitionModes; + /** confidence level for mode RECOGNITION_MODE_VOICE_TRIGGER */ + int confidenceLevel; + /** number of user confidence levels */ + ConfidenceLevel[] levels; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl b/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl new file mode 100644 index 000000000000..81028c1608ea --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.Phrase; + +/** + * Specialized sound model for key phrase detection. + * Proprietary representation of key phrases in binary data must match + * information indicated by phrases field. + * {@hide} + */ +parcelable PhraseSoundModel { + /** Common part of sound model descriptor */ + SoundModel common; + /** List of descriptors for key phrases supported by this sound model */ + Phrase[] phrases; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl new file mode 100644 index 000000000000..c7642e8c1241 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; + +/** + * Configuration for tuning behavior of an active recognition process. + * {@hide} + */ +parcelable RecognitionConfig { + /* Capture and buffer audio for this recognition instance. */ + boolean captureRequested; + + /* Configuration for each key phrase. */ + PhraseRecognitionExtra[] phraseRecognitionExtras; + + /** Opaque capture configuration data. */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl new file mode 100644 index 000000000000..de4d060ce484 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.audio.common.AudioConfig; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModelType; + +/** + * An event that gets sent to indicate a recognition (or aborting of the recognition process). + * {@hide} + */ +parcelable RecognitionEvent { + /** Recognition status. */ + RecognitionStatus status; + /** Event type, same as sound model type. */ + SoundModelType type; + /** Is it possible to capture audio from this utterance buffered by the implementation. */ + boolean captureAvailable; + /* Audio session ID. framework use. */ + 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 key phrase is also available for Capture. + */ + int captureDelayMs; + /** Duration in ms of audio captured before the start of the trigger. 0 if none. */ + int capturePreambleMs; + /** If true, the 'data' field below contains the capture of the trigger sound. */ + boolean triggerInData; + /** + * Audio format of either the trigger in event data or to use for capture of the rest of the + * utterance. + */ + AudioConfig audioConfig; + /** Additional data. */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl new file mode 100644 index 000000000000..d8bfff4bec6f --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Recognition mode. + * {@hide} + */ +@Backing(type="int") +enum RecognitionMode { + /** Simple voice trigger. */ + VOICE_TRIGGER = 0x1, + /** Trigger only if one user in model identified. */ + USER_IDENTIFICATION = 0x2, + /** Trigger only if one user in model authenticated. */ + USER_AUTHENTICATION = 0x4, + /** Generic sound trigger. */ + GENERIC_TRIGGER = 0x8, +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl new file mode 100644 index 000000000000..d563edca547d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * A status for indicating the type of a recognition event. + * {@hide} + */ +@Backing(type="int") +enum RecognitionStatus { + /** Recognition success. */ + SUCCESS = 0, + /** Recognition aborted (e.g. capture preempted by another use-case. */ + ABORTED = 1, + /** Recognition failure. */ + FAILURE = 2, + /** + * Recognition event was triggered by a forceRecognitionEvent request, not by the DSP. + * Note that forced detections *do not* stop the active recognition, unlike the other types. + */ + FORCED = 3 +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundModel.aidl b/media/java/android/media/soundtrigger_middleware/SoundModel.aidl new file mode 100644 index 000000000000..fba1ee507836 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundModel.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundModelType; + +/** + * Base sound model descriptor. This struct can be extended for various specific types by way of + * aggregation. + * {@hide} + */ +parcelable SoundModel { + /** Model type. */ + SoundModelType type; + /** Unique sound model ID. */ + String uuid; + /** + * Unique vendor ID. Identifies the engine the sound model + * was build for */ + String vendorUuid; + /** Opaque data transparent to Android framework */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl b/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl new file mode 100644 index 000000000000..f2abc9af7780 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Sound model type. + * {@hide} + */ +@Backing(type="int") +enum SoundModelType { + /** Unspecified sound model type */ + UNKNOWN = -1, + /** Key phrase sound models */ + KEYPHRASE = 0, + /** All models other than keyphrase */ + GENERIC = 1, +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl new file mode 100644 index 000000000000..667135ff61b9 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; + +/** + * A descriptor of an available sound trigger module, containing the handle used to reference the + * module, as well its capabilities. + * {@hide} + */ +parcelable SoundTriggerModuleDescriptor { + /** Module handle to be used for attaching to it. */ + int handle; + /** Module capabilities. */ + SoundTriggerModuleProperties properties; +} + diff --git a/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl new file mode 100644 index 000000000000..1a3b40261a62 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * Capabilities of a sound trigger module. + * {@hide} + */ +parcelable SoundTriggerModuleProperties { + /** Implementor name */ + String implementor; + /** Implementation description */ + String description; + /** Implementation version */ + int version; + /** + * Unique implementation ID. The UUID must change with each version of + the engine implementation */ + String uuid; + /** Maximum number of concurrent sound models loaded */ + int maxSoundModels; + /** Maximum number of key phrases */ + int maxKeyPhrases; + /** Maximum number of concurrent users detected */ + int maxUsers; + /** All supported modes. e.g RecognitionMode.VOICE_TRIGGER */ + int recognitionModes; + /** Supports seamless transition from detection to capture */ + boolean captureTransition; + /** Maximum buffering capacity in ms if captureTransition is true */ + int maxBufferMs; + /** Supports capture by other use cases while detection is active */ + boolean concurrentCapture; + /** Returns the trigger capture in event */ + boolean triggerInEvent; + /** + * Rated power consumption when detection is active with TDB + * silence/sound/speech ratio */ + int powerConsumptionMw; +} diff --git a/media/java/android/media/soundtrigger_middleware/Status.aidl b/media/java/android/media/soundtrigger_middleware/Status.aidl new file mode 100644 index 000000000000..d8f9d8f7e891 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/Status.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 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.soundtrigger_middleware; + +/** + * {@hide} + */ +@Backing(type="int") +enum Status { + /** Success. */ + SUCCESS = 0, + /** Failure due to resource contention. This is typically a temporary condition. */ + RESOURCE_CONTENTION = 1, + /** Operation is not supported in this implementation. This is a permanent condition. */ + OPERATION_NOT_SUPPORTED = 2, +} diff --git a/services/core/Android.bp b/services/core/Android.bp index 203bc61c2022..b7adfa4a3ff1 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -116,6 +116,7 @@ java_library_static { "android.hardware.oemlock-V1.0-java", "android.hardware.configstore-V1.0-java", "android.hardware.contexthub-V1.0-java", + "android.hardware.soundtrigger-V2.3-java", "android.hidl.manager-V1.2-java", "dnsresolver_aidl_interface-V2-java", "netd_event_listener_interface-java", diff --git a/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java new file mode 100644 index 000000000000..3fa52301d9a0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +/** + * An implementation of SoundTriggerMiddlewareImpl.AudioSessionProvider that ties to native + * AudioSystem module via JNI. + */ +class AudioSessionProviderImpl extends SoundTriggerMiddlewareImpl.AudioSessionProvider { + @Override + public native AudioSession acquireSession(); + + @Override + public native void releaseSession(int sessionHandle); +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java new file mode 100644 index 000000000000..9b22f33a20b0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.audio.common.V2_0.Uuid; +import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; +import android.hardware.soundtrigger.V2_3.ISoundTriggerHw; +import android.media.audio.common.AudioConfig; +import android.media.audio.common.AudioOffloadInfo; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.os.HidlMemoryUtil; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilities for type conversion between SoundTrigger HAL types and SoundTriggerMiddleware service + * types. + * + * @hide + */ +class ConversionUtil { + static @NonNull + SoundTriggerModuleProperties hidl2aidlProperties( + @NonNull ISoundTriggerHw.Properties hidlProperties) { + SoundTriggerModuleProperties aidlProperties = new SoundTriggerModuleProperties(); + aidlProperties.implementor = hidlProperties.implementor; + aidlProperties.description = hidlProperties.description; + aidlProperties.version = hidlProperties.version; + aidlProperties.uuid = hidl2aidlUuid(hidlProperties.uuid); + aidlProperties.maxSoundModels = hidlProperties.maxSoundModels; + aidlProperties.maxKeyPhrases = hidlProperties.maxKeyPhrases; + aidlProperties.maxUsers = hidlProperties.maxUsers; + aidlProperties.recognitionModes = hidlProperties.recognitionModes; + aidlProperties.captureTransition = hidlProperties.captureTransition; + aidlProperties.maxBufferMs = hidlProperties.maxBufferMs; + aidlProperties.concurrentCapture = hidlProperties.concurrentCapture; + aidlProperties.triggerInEvent = hidlProperties.triggerInEvent; + aidlProperties.powerConsumptionMw = hidlProperties.powerConsumptionMw; + return aidlProperties; + } + + static @NonNull + String hidl2aidlUuid(@NonNull Uuid hidlUuid) { + if (hidlUuid.node == null || hidlUuid.node.length != 6) { + throw new IllegalArgumentException("UUID.node must be of length 6."); + } + return String.format(UuidUtil.FORMAT, + hidlUuid.timeLow, + hidlUuid.timeMid, + hidlUuid.versionAndTimeHigh, + hidlUuid.variantAndClockSeqHigh, + hidlUuid.node[0], + hidlUuid.node[1], + hidlUuid.node[2], + hidlUuid.node[3], + hidlUuid.node[4], + hidlUuid.node[5]); + } + + static @NonNull + Uuid aidl2hidlUuid(@NonNull String aidlUuid) { + Matcher matcher = UuidUtil.PATTERN.matcher(aidlUuid); + if (!matcher.matches()) { + throw new IllegalArgumentException("Illegal format for UUID: " + aidlUuid); + } + Uuid hidlUuid = new Uuid(); + hidlUuid.timeLow = Integer.parseUnsignedInt(matcher.group(1), 16); + hidlUuid.timeMid = (short) Integer.parseUnsignedInt(matcher.group(2), 16); + hidlUuid.versionAndTimeHigh = (short) Integer.parseUnsignedInt(matcher.group(3), 16); + hidlUuid.variantAndClockSeqHigh = (short) Integer.parseUnsignedInt(matcher.group(4), 16); + hidlUuid.node = new byte[]{(byte) Integer.parseUnsignedInt(matcher.group(5), 16), + (byte) Integer.parseUnsignedInt(matcher.group(6), 16), + (byte) Integer.parseUnsignedInt(matcher.group(7), 16), + (byte) Integer.parseUnsignedInt(matcher.group(8), 16), + (byte) Integer.parseUnsignedInt(matcher.group(9), 16), + (byte) Integer.parseUnsignedInt(matcher.group(10), 16)}; + return hidlUuid; + } + + static int aidl2hidlSoundModelType(int aidlType) { + switch (aidlType) { + case SoundModelType.GENERIC: + return android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC; + case SoundModelType.KEYPHRASE: + return android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE; + default: + throw new IllegalArgumentException("Unknown sound model type: " + aidlType); + } + } + + static int hidl2aidlSoundModelType(int hidlType) { + switch (hidlType) { + case android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC: + return SoundModelType.GENERIC; + case android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE: + return SoundModelType.KEYPHRASE; + default: + throw new IllegalArgumentException("Unknown sound model type: " + hidlType); + } + } + + static @NonNull + ISoundTriggerHw.Phrase aidl2hidlPhrase(@NonNull Phrase aidlPhrase) { + ISoundTriggerHw.Phrase hidlPhrase = new ISoundTriggerHw.Phrase(); + hidlPhrase.id = aidlPhrase.id; + hidlPhrase.recognitionModes = aidl2hidlRecognitionModes(aidlPhrase.recognitionModes); + for (int aidlUser : aidlPhrase.users) { + hidlPhrase.users.add(aidlUser); + } + hidlPhrase.locale = aidlPhrase.locale; + hidlPhrase.text = aidlPhrase.text; + return hidlPhrase; + } + + static int aidl2hidlRecognitionModes(int aidlModes) { + int hidlModes = 0; + + if ((aidlModes & RecognitionMode.VOICE_TRIGGER) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER; + } + if ((aidlModes & RecognitionMode.USER_IDENTIFICATION) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION; + } + if ((aidlModes & RecognitionMode.USER_AUTHENTICATION) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION; + } + if ((aidlModes & RecognitionMode.GENERIC_TRIGGER) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + } + return hidlModes; + } + + static int hidl2aidlRecognitionModes(int hidlModes) { + int aidlModes = 0; + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER) != 0) { + aidlModes |= RecognitionMode.VOICE_TRIGGER; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION) + != 0) { + aidlModes |= RecognitionMode.USER_IDENTIFICATION; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION) + != 0) { + aidlModes |= RecognitionMode.USER_AUTHENTICATION; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER) != 0) { + aidlModes |= RecognitionMode.GENERIC_TRIGGER; + } + return aidlModes; + } + + static @NonNull + ISoundTriggerHw.SoundModel aidl2hidlSoundModel(@NonNull SoundModel aidlModel) { + ISoundTriggerHw.SoundModel hidlModel = new ISoundTriggerHw.SoundModel(); + hidlModel.header.type = aidl2hidlSoundModelType(aidlModel.type); + hidlModel.header.uuid = aidl2hidlUuid(aidlModel.uuid); + hidlModel.header.vendorUuid = aidl2hidlUuid(aidlModel.vendorUuid); + hidlModel.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlModel.data, + "SoundTrigger SoundModel"); + return hidlModel; + } + + static @NonNull + ISoundTriggerHw.PhraseSoundModel aidl2hidlPhraseSoundModel( + @NonNull PhraseSoundModel aidlModel) { + ISoundTriggerHw.PhraseSoundModel hidlModel = new ISoundTriggerHw.PhraseSoundModel(); + hidlModel.common = aidl2hidlSoundModel(aidlModel.common); + for (Phrase aidlPhrase : aidlModel.phrases) { + hidlModel.phrases.add(aidl2hidlPhrase(aidlPhrase)); + } + return hidlModel; + } + + static @NonNull + ISoundTriggerHw.RecognitionConfig aidl2hidlRecognitionConfig( + @NonNull RecognitionConfig aidlConfig) { + ISoundTriggerHw.RecognitionConfig hidlConfig = new ISoundTriggerHw.RecognitionConfig(); + hidlConfig.header.captureRequested = aidlConfig.captureRequested; + for (PhraseRecognitionExtra aidlPhraseExtra : aidlConfig.phraseRecognitionExtras) { + hidlConfig.header.phrases.add(aidl2hidlPhraseRecognitionExtra(aidlPhraseExtra)); + } + hidlConfig.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlConfig.data, + "SoundTrigger RecognitionConfig"); + return hidlConfig; + } + + static @NonNull + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra aidl2hidlPhraseRecognitionExtra( + @NonNull PhraseRecognitionExtra aidlExtra) { + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + hidlExtra.id = aidlExtra.id; + hidlExtra.recognitionModes = aidl2hidlRecognitionModes(aidlExtra.recognitionModes); + hidlExtra.confidenceLevel = aidlExtra.confidenceLevel; + hidlExtra.levels.ensureCapacity(aidlExtra.levels.length); + for (ConfidenceLevel aidlLevel : aidlExtra.levels) { + hidlExtra.levels.add(aidl2hidlConfidenceLevel(aidlLevel)); + } + return hidlExtra; + } + + static @NonNull + PhraseRecognitionExtra hidl2aidlPhraseRecognitionExtra( + @NonNull android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra) { + PhraseRecognitionExtra aidlExtra = new PhraseRecognitionExtra(); + aidlExtra.id = hidlExtra.id; + aidlExtra.recognitionModes = hidl2aidlRecognitionModes(hidlExtra.recognitionModes); + aidlExtra.confidenceLevel = hidlExtra.confidenceLevel; + aidlExtra.levels = new ConfidenceLevel[hidlExtra.levels.size()]; + for (int i = 0; i < hidlExtra.levels.size(); ++i) { + aidlExtra.levels[i] = hidl2aidlConfidenceLevel(hidlExtra.levels.get(i)); + } + return aidlExtra; + } + + static @NonNull + android.hardware.soundtrigger.V2_0.ConfidenceLevel aidl2hidlConfidenceLevel( + @NonNull ConfidenceLevel aidlLevel) { + android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + hidlLevel.userId = aidlLevel.userId; + hidlLevel.levelPercent = aidlLevel.levelPercent; + return hidlLevel; + } + + static @NonNull + ConfidenceLevel hidl2aidlConfidenceLevel( + @NonNull android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel) { + ConfidenceLevel aidlLevel = new ConfidenceLevel(); + aidlLevel.userId = hidlLevel.userId; + aidlLevel.levelPercent = hidlLevel.levelPercent; + return aidlLevel; + } + + static int hidl2aidlRecognitionStatus(int hidlStatus) { + switch (hidlStatus) { + case ISoundTriggerHwCallback.RecognitionStatus.SUCCESS: + return RecognitionStatus.SUCCESS; + case ISoundTriggerHwCallback.RecognitionStatus.ABORT: + return RecognitionStatus.ABORTED; + case ISoundTriggerHwCallback.RecognitionStatus.FAILURE: + return RecognitionStatus.FAILURE; + case 3: // This doesn't have a constant in HIDL. + return RecognitionStatus.FORCED; + default: + throw new IllegalArgumentException("Unknown recognition status: " + hidlStatus); + } + } + + static @NonNull + RecognitionEvent hidl2aidlRecognitionEvent(@NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent hidlEvent) { + RecognitionEvent aidlEvent = new RecognitionEvent(); + aidlEvent.status = hidl2aidlRecognitionStatus(hidlEvent.status); + aidlEvent.type = hidl2aidlSoundModelType(hidlEvent.type); + aidlEvent.captureAvailable = hidlEvent.captureAvailable; + // hidlEvent.captureSession is never a valid field. + aidlEvent.captureSession = -1; + aidlEvent.captureDelayMs = hidlEvent.captureDelayMs; + aidlEvent.capturePreambleMs = hidlEvent.capturePreambleMs; + aidlEvent.triggerInData = hidlEvent.triggerInData; + aidlEvent.audioConfig = hidl2aidlAudioConfig(hidlEvent.audioConfig); + aidlEvent.data = new byte[hidlEvent.data.size()]; + for (int i = 0; i < aidlEvent.data.length; ++i) { + aidlEvent.data[i] = hidlEvent.data.get(i); + } + return aidlEvent; + } + + static @NonNull + RecognitionEvent hidl2aidlRecognitionEvent( + @NonNull ISoundTriggerHwCallback.RecognitionEvent hidlEvent) { + RecognitionEvent aidlEvent = hidl2aidlRecognitionEvent(hidlEvent.header); + // Data needs to get overridden with 2.1 data. + aidlEvent.data = HidlMemoryUtil.hidlMemoryToByteArray(hidlEvent.data); + return aidlEvent; + } + + static @NonNull + PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent(@NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) { + PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent(); + aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common); + aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()]; + for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) { + aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra( + hidlEvent.phraseExtras.get(i)); + } + return aidlEvent; + } + + static @NonNull + PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent( + @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) { + PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent(); + aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common); + aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()]; + for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) { + aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra( + hidlEvent.phraseExtras.get(i)); + } + return aidlEvent; + } + + static @NonNull + AudioConfig hidl2aidlAudioConfig( + @NonNull android.hardware.audio.common.V2_0.AudioConfig hidlConfig) { + AudioConfig aidlConfig = new AudioConfig(); + // TODO(ytai): channelMask and format might need a more careful conversion to make sure the + // constants match. + aidlConfig.sampleRateHz = hidlConfig.sampleRateHz; + aidlConfig.channelMask = hidlConfig.channelMask; + aidlConfig.format = hidlConfig.format; + aidlConfig.offloadInfo = hidl2aidlOffloadInfo(hidlConfig.offloadInfo); + aidlConfig.frameCount = hidlConfig.frameCount; + return aidlConfig; + } + + static @NonNull + AudioOffloadInfo hidl2aidlOffloadInfo( + @NonNull android.hardware.audio.common.V2_0.AudioOffloadInfo hidlInfo) { + AudioOffloadInfo aidlInfo = new AudioOffloadInfo(); + // TODO(ytai): channelMask, format, streamType and usage might need a more careful + // conversion to make sure the constants match. + aidlInfo.sampleRateHz = hidlInfo.sampleRateHz; + aidlInfo.channelMask = hidlInfo.channelMask; + aidlInfo.format = hidlInfo.format; + aidlInfo.streamType = hidlInfo.streamType; + aidlInfo.bitRatePerSecond = hidlInfo.bitRatePerSecond; + aidlInfo.durationMicroseconds = hidlInfo.durationMicroseconds; + aidlInfo.hasVideo = hidlInfo.hasVideo; + aidlInfo.isStreaming = hidlInfo.isStreaming; + aidlInfo.bitWidth = hidlInfo.bitWidth; + aidlInfo.bufferSize = hidlInfo.bufferSize; + aidlInfo.usage = hidlInfo.usage; + return aidlInfo; + } + + @Nullable + static ModelParameterRange hidl2aidlModelParameterRange( + android.hardware.soundtrigger.V2_3.ModelParameterRange hidlRange) { + if (hidlRange == null) { + return null; + } + ModelParameterRange aidlRange = new ModelParameterRange(); + aidlRange.minInclusive = hidlRange.start; + aidlRange.maxInclusive = hidlRange.end; + return aidlRange; + } + + static int aidl2hidlModelParameter(int aidlParam) { + switch (aidlParam) { + case ModelParameter.THRESHOLD_FACTOR: + return android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR; + + default: + return android.hardware.soundtrigger.V2_3.ModelParameter.INVALID; + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/HalException.java b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java new file mode 100644 index 000000000000..8b3e70875183 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * This exception represents a non-zero status code returned by a HAL invocation. + * Depending on the operation that threw the error, the integrity of the HAL implementation and the + * client's tolerance to error, this error may or may not be recoverable. The HAL itself is expected + * to retain the state it had prior to the invocation (so, unless the error is a result of a HAL + * bug, normal operation may resume). + * <p> + * The reason why this is a RuntimeException, even though the HAL interface allows returning them + * is because we expect none of them to actually occur as part of correct usage of the HAL. + * + * @hide + */ +public class HalException extends RuntimeException { + public final int errorCode; + + public HalException(int errorCode, @NonNull String message) { + super(message); + this.errorCode = errorCode; + } + + public HalException(int errorCode) { + this.errorCode = errorCode; + } + + @Override + public @NonNull String toString() { + return super.toString() + " (code " + errorCode + ")"; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java new file mode 100644 index 000000000000..f0a0d8305bc6 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.os.HidlMemoryUtil; + +import java.util.ArrayList; + +/** + * Utilities for maintaining data compatibility between different minor versions of soundtrigger@2.x + * HAL. + * Note that some of these conversion utilities are destructive, i.e. mutate their input (for the + * sake of simplifying code and reducing copies). + */ +class Hw2CompatUtil { + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel convertSoundModel_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 = soundModel.header; + // Note: this mutates the input! + model_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(soundModel.data); + return model_2_0; + } + + static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent convertRecognitionEvent_2_0_to_2_1( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent(); + event_2_1.header = event; + event_2_1.data = HidlMemoryUtil.byteListToHidlMemory(event_2_1.header.data, + "SoundTrigger RecognitionEvent"); + // Note: this mutates the input! + event_2_1.header.data = new ArrayList<>(); + return event_2_1; + } + + static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent convertPhraseRecognitionEvent_2_0_to_2_1( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent + event_2_1 = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + event_2_1.common = convertRecognitionEvent_2_0_to_2_1(event.common); + event_2_1.phraseExtras = event.phraseExtras; + return event_2_1; + } + + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel convertPhraseSoundModel_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel(); + model_2_0.common = convertSoundModel_2_1_to_2_0(soundModel.common); + model_2_0.phrases = soundModel.phrases; + return model_2_0; + } + + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig convertRecognitionConfig_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 = + config.header; + // Note: this mutates the input! + config_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(config.data); + return config_2_0; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java new file mode 100644 index 000000000000..81252c9a8c14 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.hardware.soundtrigger.V2_3.ISoundTriggerHw; +import android.hardware.soundtrigger.V2_3.ModelParameterRange; +import android.hidl.base.V1_0.IBase; +import android.os.IHwBinder; + +/** + * This interface mimics android.hardware.soundtrigger.V2_x.ISoundTriggerHw and + * android.hardware.soundtrigger.V2_x.ISoundTriggerHwCallback, with a few key differences: + * <ul> + * <li>Methods in the original interface generally have a status return value and potentially a + * second return value which is the actual return value. This is reflected via a synchronous + * callback, which is not very pleasant to work with. This interface replaces that pattern with + * the convention that a HalException is thrown for non-OK status, and then we can use the + * return value for the actual return value. + * <li>This interface will always include all the methods from the latest 2.x version (and thus + * from every 2.x version) interface, with the convention that unsupported methods throw a + * {@link RecoverableException} with a + * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED} + * code. + * <li>Cases where the original interface had multiple versions of a method representing the exact + * thing, or there exists a trivial conversion between the new and old version, this interface + * represents only the latest version, without any _version suffixes. + * <li>Removes some of the obscure IBinder methods. + * <li>No RemoteExceptions are specified. Some implementations of this interface may rethrow + * RemoteExceptions as RuntimeExceptions, some can guarantee handling them somehow and never throw + * them. + * <li>soundModelCallback has been removed, since nobody cares about it. Implementations are free + * to silently discard it. + * </ul> + * For cases where the client wants to explicitly handle specific versions of the underlying driver + * interface, they may call {@link #interfaceDescriptor()}. + * <p> + * <b>Note to maintainers</b>: This class must always be kept in sync with the latest 2.x version, + * so that clients have access to the entire functionality without having to burden themselves with + * compatibility, as much as possible. + */ +public interface ISoundTriggerHw2 { + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getProperties(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback + */ + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties(); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback) + */ + int loadSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadPhraseSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback) + */ + int loadPhraseSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#unloadSoundModel(int) + */ + void unloadSoundModel(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopRecognition(int) + */ + void stopRecognition(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopAllRecognitions() + */ + void stopAllRecognitions(); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#startRecognition_2_1(int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int) + */ + void startRecognition(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getModelState(int) + */ + void getModelState(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#getParameter(int, int, + * ISoundTriggerHw.getParameterCallback) + */ + int getModelParameter(int modelHandle, int param); + + /** + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#setParameter(int, int, int) + */ + void setModelParameter(int modelHandle, int param, int value); + + /** + * @return null if not supported. + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#queryParameter(int, int, + * ISoundTriggerHw.queryParameterCallback) + */ + ModelParameterRange queryParameter(int modelHandle, int param); + + /** + * @see IHwBinder#linkToDeath(IHwBinder.DeathRecipient, long) + */ + boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie); + + /** + * @see IHwBinder#unlinkToDeath(IHwBinder.DeathRecipient) + */ + boolean unlinkToDeath(IHwBinder.DeathRecipient recipient); + + /** + * @see IBase#interfaceDescriptor() + */ + String interfaceDescriptor() throws android.os.RemoteException; + + interface Callback { + /** + * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#recognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent, + * int) + */ + void recognitionCallback( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie); + + /** + * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#phraseRecognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent, + * int) + */ + void phraseRecognitionCallback( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie); + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java new file mode 100644 index 000000000000..e1fb2266b7c6 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * An internal server error. + * <p> + * This exception wraps any exception thrown from a service implementation, which is a result of a + * bug in the server implementation (or any of its dependencies). + * <p> + * Specifically, this type is excluded from the set of whitelisted exceptions that binder would + * tunnel to the client process, since these exceptions are ambiguous regarding whether the client + * had done something wrong or the server is buggy. For example, a client getting an + * IllegalArgumentException cannot easily determine whether they had provided illegal arguments to + * the method they were calling, or whether the method implementation provided illegal arguments to + * some method it was calling due to a bug. + * + * @hide + */ +public class InternalServerError extends RuntimeException { + public InternalServerError(@NonNull Throwable cause) { + super(cause); + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java new file mode 100644 index 000000000000..83618505814e --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * This exception represents a fault which: + * <ul> + * <li>Could not have been anticipated by a caller (i.e. is not a violation of any preconditions). + * <li>Is guaranteed to not have been caused any meaningful state change in the callee. The caller + * may continue operation as if the call has never been made. + * </ul> + * <p> + * Some recoverable faults are permanent and some are transient / circumstantial, the specific error + * code can provide more information about the possible recovery options. + * <p> + * The reason why this is a RuntimeException is to allow it to go through interfaces defined by + * AIDL, which we have no control over. + * + * @hide + */ +public class RecoverableException extends RuntimeException { + public final int errorCode; + + public RecoverableException(int errorCode, @NonNull String message) { + super(message); + this.errorCode = errorCode; + } + + public RecoverableException(int errorCode) { + this.errorCode = errorCode; + } + + @Override + public @NonNull String toString() { + return super.toString() + " (code " + errorCode + ")"; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java new file mode 100644 index 000000000000..4a852c4b68e8 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.soundtrigger_middleware.Status; +import android.os.IHwBinder; +import android.os.RemoteException; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An implementation of {@link ISoundTriggerHw2}, on top of any + * android.hardware.soundtrigger.V2_x.ISoundTriggerHw implementation. This class hides away some of + * the details involved with retaining backward compatibility and adapts to the more pleasant syntax + * exposed by {@link ISoundTriggerHw2}, compared to the bare driver interface. + * <p> + * Exception handling: + * <ul> + * <li>All {@link RemoteException}s get rethrown as {@link RuntimeException}. + * <li>All HAL malfunctions get thrown as {@link HalException}. + * <li>All unsupported operations get thrown as {@link RecoverableException} with a + * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED} + * code. + * </ul> + */ +final class SoundTriggerHw2Compat implements ISoundTriggerHw2 { + private final @NonNull + IHwBinder mBinder; + private final @NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHw mUnderlying_2_0; + private final @Nullable + android.hardware.soundtrigger.V2_1.ISoundTriggerHw mUnderlying_2_1; + private final @Nullable + android.hardware.soundtrigger.V2_2.ISoundTriggerHw mUnderlying_2_2; + private final @Nullable + android.hardware.soundtrigger.V2_3.ISoundTriggerHw mUnderlying_2_3; + + public SoundTriggerHw2Compat( + @NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw underlying) { + this(underlying.asBinder()); + } + + public SoundTriggerHw2Compat(IHwBinder binder) { + Objects.requireNonNull(binder); + + mBinder = binder; + + // We want to share the proxy instances rather than create a separate proxy for every + // version, so we go down the versions in descending order to find the latest one supported, + // and then simply up-cast it to obtain all the versions that are earlier. + + // Attempt 2.3 + android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3 = + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.asInterface(binder); + if (as2_3 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = as2_3; + return; + } + + // Attempt 2.2 + android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2 = + android.hardware.soundtrigger.V2_2.ISoundTriggerHw.asInterface(binder); + if (as2_2 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = as2_2; + mUnderlying_2_3 = null; + return; + } + + // Attempt 2.1 + android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1 = + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.asInterface(binder); + if (as2_1 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = as2_1; + mUnderlying_2_2 = mUnderlying_2_3 = null; + return; + } + + // Attempt 2.0 + android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0 = + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.asInterface(binder); + if (as2_0 != null) { + mUnderlying_2_0 = as2_0; + mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = null; + return; + } + + throw new RuntimeException("Binder doesn't support ISoundTriggerHw@2.0"); + } + + private static void handleHalStatus(int status, String methodName) { + if (status != 0) { + throw new HalException(status, methodName); + } + } + + @Override + public android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties() { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicReference<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties> + properties = + new AtomicReference<>(); + as2_0().getProperties( + (r, p) -> { + retval.set(r); + properties.set(p); + }); + handleHalStatus(retval.get(), "getProperties"); + return properties.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public int loadSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + Callback callback, int cookie) { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + try { + as2_1().loadSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + return loadSoundModel_2_0(soundModel, callback, cookie); + } + handleHalStatus(retval.get(), "loadSoundModel_2_1"); + return handle.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public int loadPhraseSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + Callback callback, int cookie) { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + try { + as2_1().loadPhraseSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), + cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + return loadPhraseSoundModel_2_0(soundModel, callback, cookie); + } + handleHalStatus(retval.get(), "loadSoundModel_2_1"); + return handle.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void unloadSoundModel(int modelHandle) { + try { + int retval = as2_0().unloadSoundModel(modelHandle); + handleHalStatus(retval, "unloadSoundModel"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void stopRecognition(int modelHandle) { + try { + int retval = as2_0().stopRecognition(modelHandle); + handleHalStatus(retval, "stopRecognition"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + + } + + @Override + public void stopAllRecognitions() { + try { + int retval = as2_0().stopAllRecognitions(); + handleHalStatus(retval, "stopAllRecognitions"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void startRecognition(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + Callback callback, int cookie) { + try { + try { + int retval = as2_1().startRecognition_2_1(modelHandle, config, + new SoundTriggerCallback(callback), cookie); + handleHalStatus(retval, "startRecognition_2_1"); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + startRecognition_2_0(modelHandle, config, callback, cookie); + } + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void getModelState(int modelHandle) { + try { + int retval = as2_2().getModelState(modelHandle); + handleHalStatus(retval, "getModelState"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + } + + @Override + public int getModelParameter(int modelHandle, int param) { + AtomicInteger status = new AtomicInteger(-1); + AtomicInteger value = new AtomicInteger(0); + try { + as2_3().getParameter(modelHandle, param, + (s, v) -> { + status.set(s); + value.set(v); + }); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + handleHalStatus(status.get(), "getParameter"); + return value.get(); + } + + @Override + public void setModelParameter(int modelHandle, int param, int value) { + try { + int retval = as2_3().setParameter(modelHandle, param, value); + handleHalStatus(retval, "setParameter"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + } + + @Override + public android.hardware.soundtrigger.V2_3.ModelParameterRange queryParameter(int modelHandle, + int param) { + AtomicInteger status = new AtomicInteger(-1); + AtomicReference<android.hardware.soundtrigger.V2_3.OptionalModelParameterRange> + optionalRange = + new AtomicReference<>(); + try { + as2_3().queryParameter(modelHandle, param, + (s, r) -> { + status.set(s); + optionalRange.set(r); + }); + } catch (NotSupported e) { + // For older drivers, we consider no model parameter to be supported. + return null; + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + handleHalStatus(status.get(), "queryParameter"); + return (optionalRange.get().getDiscriminator() + == android.hardware.soundtrigger.V2_3.OptionalModelParameterRange.hidl_discriminator.range) + ? + optionalRange.get().range() : null; + } + + @Override + public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) { + return mBinder.linkToDeath(recipient, cookie); + } + + @Override + public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) { + return mBinder.unlinkToDeath(recipient); + } + + @Override + public String interfaceDescriptor() throws RemoteException { + return as2_0().interfaceDescriptor(); + } + + private int loadSoundModel_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + Callback callback, int cookie) + throws RemoteException { + // Convert the soundModel to V2.0. + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 = + Hw2CompatUtil.convertSoundModel_2_1_to_2_0(soundModel); + + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + as2_0().loadSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, (r, h) -> { + retval.set(r); + handle.set(h); + }); + handleHalStatus(retval.get(), "loadSoundModel"); + return handle.get(); + } + + private int loadPhraseSoundModel_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + Callback callback, int cookie) + throws RemoteException { + // Convert the soundModel to V2.0. + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 = + Hw2CompatUtil.convertPhraseSoundModel_2_1_to_2_0(soundModel); + + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + as2_0().loadPhraseSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + handleHalStatus(retval.get(), "loadSoundModel"); + return handle.get(); + } + + private void startRecognition_2_0(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + Callback callback, int cookie) + throws RemoteException { + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 = + Hw2CompatUtil.convertRecognitionConfig_2_1_to_2_0(config); + int retval = as2_0().startRecognition(modelHandle, config_2_0, + new SoundTriggerCallback(callback), cookie); + handleHalStatus(retval, "startRecognition"); + } + + private @NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0() { + return mUnderlying_2_0; + } + + private @NonNull + android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1() throws NotSupported { + if (mUnderlying_2_1 == null) { + throw new NotSupported("Underlying driver version < 2.1"); + } + return mUnderlying_2_1; + } + + private @NonNull + android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2() throws NotSupported { + if (mUnderlying_2_2 == null) { + throw new NotSupported("Underlying driver version < 2.2"); + } + return mUnderlying_2_2; + } + + private @NonNull + android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3() throws NotSupported { + if (mUnderlying_2_3 == null) { + throw new NotSupported("Underlying driver version < 2.3"); + } + return mUnderlying_2_3; + } + + /** + * A checked exception representing the requested interface version not being supported. + * At the public interface layer, use {@link #throwAsRecoverableException()} to propagate it to + * the caller if the request cannot be fulfilled. + */ + private static class NotSupported extends Exception { + NotSupported(String message) { + super(message); + } + + /** + * Throw this as a recoverable exception. + * + * @return Never actually returns anything. Always throws. Used so that caller can write + * throw e.throwAsRecoverableException(). + */ + RecoverableException throwAsRecoverableException() { + throw new RecoverableException(Status.OPERATION_NOT_SUPPORTED, getMessage()); + } + } + + private static class SoundTriggerCallback extends + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.Stub { + private final @NonNull + Callback mDelegate; + + private SoundTriggerCallback( + @NonNull Callback delegate) { + mDelegate = Objects.requireNonNull(delegate); + } + + @Override + public void recognitionCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie) { + mDelegate.recognitionCallback(event, cookie); + } + + @Override + public void phraseRecognitionCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie) { + mDelegate.phraseRecognitionCallback(event, cookie); + } + + @Override + public void soundModelCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent event, + int cookie) { + // Nobody cares. + } + + @Override + public void recognitionCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 = + Hw2CompatUtil.convertRecognitionEvent_2_0_to_2_1(event); + mDelegate.recognitionCallback(event_2_1, cookie); + } + + @Override + public void phraseRecognitionCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent + event_2_1 = Hw2CompatUtil.convertPhraseRecognitionEvent_2_0_to_2_1(event); + mDelegate.phraseRecognitionCallback(event_2_1, cookie); + } + + @Override + public void soundModelCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent event, + int cookie) { + // Nobody cares. + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java new file mode 100644 index 000000000000..9d51b65ea152 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.hardware.soundtrigger.V2_0.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.os.IBinder; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an implementation of the ISoundTriggerMiddlewareService interface. + * <p> + * <b>Important conventions:</b> + * <ul> + * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle invalid + * usage, and such usage will result in undefined behavior. If this service is to be offered to an + * untrusted client, it must be wrapped with input and state validation. + * <li>There is no binder instance associated with this implementation. Do not call asBinder(). + * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, + * recoverable faults. The error code would one of the + * {@link android.media.soundtrigger_middleware.Status} + * constants. Any other exception thrown should be regarded as a bug in the implementation or one + * of its dependencies (assuming correct usage). + * <li>The implementation is designed for testibility by featuring dependency injection (the + * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies on + * Android runtime. + * <li>The implementation is thread-safe. + * </ul> + * + * @hide + */ +public class SoundTriggerMiddlewareImpl implements ISoundTriggerMiddlewareService { + static private final String TAG = "SoundTriggerMiddlewareImpl"; + private final SoundTriggerModule[] mModules; + + /** + * Interface to the audio system, which can allocate capture session handles. + * SoundTrigger uses those sessions in order to associate a recognition session with an optional + * capture from the same device that triggered the recognition. + */ + public static abstract class AudioSessionProvider { + public static final class AudioSession { + final int mSessionHandle; + final int mIoHandle; + final int mDeviceHandle; + + AudioSession(int sessionHandle, int ioHandle, int deviceHandle) { + mSessionHandle = sessionHandle; + mIoHandle = ioHandle; + mDeviceHandle = deviceHandle; + } + } + + public abstract AudioSession acquireSession(); + + public abstract void releaseSession(int sessionHandle); + } + + /** + * Most generic constructor - gets an array of HAL driver instances. + */ + public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw[] halServices, + @NonNull AudioSessionProvider audioSessionProvider) { + List<SoundTriggerModule> modules = new ArrayList<>(halServices.length); + + for (int i = 0; i < halServices.length; ++i) { + ISoundTriggerHw service = halServices[i]; + try { + modules.add(new SoundTriggerModule(service, audioSessionProvider)); + } catch (Exception e) { + Log.e(TAG, "Failed to a SoundTriggerModule instance", e); + } + } + + mModules = modules.toArray(new SoundTriggerModule[modules.size()]); + } + + /** + * Convenience constructor - gets a single HAL driver instance. + */ + public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw halService, + @NonNull AudioSessionProvider audioSessionProvider) { + this(new ISoundTriggerHw[]{halService}, audioSessionProvider); + } + + @Override + public @NonNull + SoundTriggerModuleDescriptor[] listModules() { + SoundTriggerModuleDescriptor[] result = new SoundTriggerModuleDescriptor[mModules.length]; + + for (int i = 0; i < mModules.length; ++i) { + SoundTriggerModuleDescriptor desc = new SoundTriggerModuleDescriptor(); + desc.handle = i; + desc.properties = mModules[i].getProperties(); + result[i] = desc; + } + return result; + } + + @Override + public @NonNull + ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) { + return mModules[handle].attach(callback); + } + + @Override + public void setExternalCaptureState(boolean active) { + for (SoundTriggerModule module : mModules) { + module.setExternalCaptureState(active); + } + } + + @Override + public @NonNull + IBinder asBinder() { + throw new UnsupportedOperationException( + "This implementation is not inteded to be used directly with Binder."); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java new file mode 100644 index 000000000000..a7cfe1037f11 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.soundtrigger.V2_0.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.util.Preconditions; +import com.android.server.SystemService; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes + * it as a Binder service and enforces permissions and correct usage by the client, as well as makes + * sure that exceptions representing a server malfunction do not get sent to the client. + * <p> + * This is intended to extract the non-business logic out of the underlying implementation and thus + * make it easier to maintain each one of those separate aspects. A design trade-off is being made + * here, in that this class would need to essentially eavesdrop on all the client-server + * communication and retain all state known to the client, while the client doesn't necessarily care + * about all of it, and while the server has its own representation of this information. However, + * in this case, this is a small amount of data, and the benefits in code elegance seem worth it. + * There is also some additional cost in employing a simplistic locking mechanism here, but + * following the same line of reasoning, the benefits in code simplicity outweigh it. + * <p> + * Every public method in this class, overriding an interface method, must follow the following + * pattern: + * <code><pre> + * @Override public T method(S arg) { + * // Permission check. + * checkPermissions(); + * // Input validation. + * ValidationUtil.validateS(arg); + * synchronized (this) { + * // State validation. + * if (...state is not valid for this call...) { + * throw new IllegalStateException("State is invalid because..."); + * } + * // From here on, every exception isn't client's fault. + * try { + * T result = mDelegate.method(arg); + * // Update state.; + * ... + * return result; + * } catch (Exception e) { + * throw handleException(e); + * } + * } + * } + * </pre></code> + * Following this patterns ensures a consistent and rigorous handling of all aspects associated + * with client-server separation. + * <p> + * <b>Exception handling approach:</b><br> + * We make sure all client faults (permissions, argument and state validation) happen first, and + * would throw {@link SecurityException}, {@link IllegalArgumentException}/ + * {@link NullPointerException} or {@link + * IllegalStateException}, respectively. All those exceptions are treated specially by Binder and + * will get sent back to the client.<br> + * Once this is done, any subsequent fault is considered a server fault. Only {@link + * RecoverableException}s thrown by the implementation are special-cased: they would get sent back + * to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other + * exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type + * that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level + * exception handler on the server side, typically resulting in rebooting the server. + * <p> + * <b>Exposing this service as a System Service:</b><br> + * Insert this line into {@link com.android.server.SystemServer}: + * <code><pre> + * mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class); + * </pre></code> + * + * {@hide} + */ +public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub { + static private final String TAG = "SoundTriggerMiddlewareService"; + + final ISoundTriggerMiddlewareService mDelegate; + final Context mContext; + Set<Integer> mModuleHandles; + + /** + * Constructor for internal use only. Could be exposed for testing purposes in the future. + * Users should access this class via {@link Lifecycle}. + */ + private SoundTriggerMiddlewareService( + @NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) { + mDelegate = delegate; + mContext = context; + } + + /** + * Generic exception handling for exceptions thrown by the underlying implementation. + * + * Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed + * by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError} + * (<b>not</b> passed by Binder to the caller). + * <p> + * Typical usage: + * <code><pre> + * try { + * ... Do server operations ... + * } catch (Exception e) { + * throw handleException(e); + * } + * </pre></code> + */ + private static @NonNull + RuntimeException handleException(@NonNull Exception e) { + if (e instanceof RecoverableException) { + throw new ServiceSpecificException(((RecoverableException) e).errorCode, + e.getMessage()); + } + throw new InternalServerError(e); + } + + @Override + public @NonNull + SoundTriggerModuleDescriptor[] listModules() { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation (always valid). + + // From here on, every exception isn't client's fault. + try { + SoundTriggerModuleDescriptor[] result = mDelegate.listModules(); + mModuleHandles = new HashSet<>(result.length); + for (SoundTriggerModuleDescriptor desc : result) { + mModuleHandles.add(desc.handle); + } + return result; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public @NonNull + ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) { + // Permission check. + checkPermissions(); + // Input validation. + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(callback.asBinder()); + + synchronized (this) { + // State validation. + if (mModuleHandles == null) { + throw new IllegalStateException( + "Client must call listModules() prior to attaching."); + } + if (!mModuleHandles.contains(handle)) { + throw new IllegalArgumentException("Invalid handle: " + handle); + } + + // From here on, every exception isn't client's fault. + try { + ModuleService moduleService = new ModuleService(callback); + moduleService.attach(mDelegate.attach(handle, moduleService)); + return moduleService; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void setExternalCaptureState(boolean active) { + // Permission check. + checkPreemptPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation (always valid). + + // From here on, every exception isn't client's fault. + try { + mDelegate.setExternalCaptureState(active); + } catch (Exception e) { + throw handleException(e); + } + } + } + + /** + * Throws a {@link SecurityException} if caller doesn't have the right permissions to use this + * service. + */ + private void checkPermissions() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO, + "Caller must have the android.permission.RECORD_AUDIO permission."); + mContext.enforceCallingOrSelfPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, + "Caller must have the android.permission.CAPTURE_AUDIO_HOTWORD permission."); + } + + /** + * Throws a {@link SecurityException} if caller doesn't have the right permissions to preempt + * active sound trigger sessions. + */ + private void checkPreemptPermissions() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.PREEMPT_SOUND_TRIGGER, + "Caller must have the android.permission.PREEMPT_SOUND_TRIGGER permission."); + } + + /** State of a sound model. */ + static class ModelState { + /** Activity state of a sound model. */ + enum Activity { + /** Model is loaded, recognition is inactive. */ + LOADED, + /** Model is loaded, recognition is active. */ + ACTIVE + } + + /** Activity state. */ + public Activity activityState = Activity.LOADED; + + /** + * A map of known parameter support. A missing key means we don't know yet whether the + * parameter is supported. A null value means it is known to not be supported. A non-null + * value indicates the valid value range. + */ + private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>(); + + /** + * Check that the given parameter is known to be supported for this model. + * + * @param modelParam The parameter key. + */ + public void checkSupported(int modelParam) { + if (!parameterSupport.containsKey(modelParam)) { + throw new IllegalStateException("Parameter has not been checked for support."); + } + ModelParameterRange range = parameterSupport.get(modelParam); + if (range == null) { + throw new IllegalArgumentException("Paramater is not supported."); + } + } + + /** + * Check that the given parameter is known to be supported for this model and that the given + * value is a valid value for it. + * + * @param modelParam The parameter key. + * @param value The value. + */ + public void checkSupported(int modelParam, int value) { + if (!parameterSupport.containsKey(modelParam)) { + throw new IllegalStateException("Parameter has not been checked for support."); + } + ModelParameterRange range = parameterSupport.get(modelParam); + if (range == null) { + throw new IllegalArgumentException("Paramater is not supported."); + } + Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive, + "value"); + } + + /** + * Update support state for the given parameter for this model. + * + * @param modelParam The parameter key. + * @param range The parameter value range, or null if not supported. + */ + public void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) { + parameterSupport.put(modelParam, range); + } + } + + /** + * Entry-point to this module: exposes the module as a {@link SystemService}. + */ + public static final class Lifecycle extends SystemService { + private SoundTriggerMiddlewareService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + ISoundTriggerHw[] services; + try { + services = new ISoundTriggerHw[]{ISoundTriggerHw.getService(true)}; + Log.d(TAG, "Connected to default ISoundTriggerHw"); + } catch (Exception e) { + Log.e(TAG, "Failed to connect to default ISoundTriggerHw", e); + services = new ISoundTriggerHw[0]; + } + + mService = new SoundTriggerMiddlewareService( + new SoundTriggerMiddlewareImpl(services, new AudioSessionProviderImpl()), + getContext()); + publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, mService); + } + } + + /** + * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects + * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions. + */ + private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback, + DeathRecipient { + private final ISoundTriggerCallback mCallback; + private ISoundTriggerModule mDelegate; + private Map<Integer, ModelState> mLoadedModels = new HashMap<>(); + + ModuleService(@NonNull ISoundTriggerCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + void attach(@NonNull ISoundTriggerModule delegate) { + mDelegate = delegate; + } + + @Override + public int loadModel(@NonNull SoundModel model) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateGenericModel(model); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + + // From here on, every exception isn't client's fault. + try { + int handle = mDelegate.loadModel(model); + mLoadedModels.put(handle, new ModelState()); + return handle; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public int loadPhraseModel(@NonNull PhraseSoundModel model) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validatePhraseModel(model); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + + // From here on, every exception isn't client's fault. + try { + int handle = mDelegate.loadPhraseModel(model); + mLoadedModels.put(handle, new ModelState()); + return handle; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void unloadModel(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + if (modelState.activityState != ModelState.Activity.LOADED) { + throw new IllegalStateException("Model with handle: " + modelHandle + + " has invalid state for unloading: " + modelState.activityState); + } + + // From here on, every exception isn't client's fault. + try { + mDelegate.unloadModel(modelHandle); + mLoadedModels.remove(modelHandle); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateRecognitionConfig(config); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + if (modelState.activityState != ModelState.Activity.LOADED) { + throw new IllegalStateException("Model with handle: " + modelHandle + + " has invalid state for starting recognition: " + + modelState.activityState); + } + + // From here on, every exception isn't client's fault. + try { + mDelegate.startRecognition(modelHandle, config); + modelState.activityState = ModelState.Activity.ACTIVE; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void stopRecognition(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + // stopRecognition is idempotent - no need to check model state. + + // From here on, every exception isn't client's fault. + try { + mDelegate.stopRecognition(modelHandle); + modelState.activityState = ModelState.Activity.LOADED; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void forceRecognitionEvent(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + // forceRecognitionEvent is idempotent - no need to check model state. + + // From here on, every exception isn't client's fault. + try { + mDelegate.forceRecognitionEvent(modelHandle); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void setModelParameter(int modelHandle, int modelParam, int value) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + modelState.checkSupported(modelParam, value); + + // From here on, every exception isn't client's fault. + try { + mDelegate.setModelParameter(modelHandle, modelParam, value); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public int getModelParameter(int modelHandle, int modelParam) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + modelState.checkSupported(modelParam); + + // From here on, every exception isn't client's fault. + try { + return mDelegate.getModelParameter(modelHandle, modelParam); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + @Nullable + public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + + // From here on, every exception isn't client's fault. + try { + ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle, + modelParam); + modelState.updateParameterSupport(modelParam, result); + return result; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void detach() { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has already been detached."); + } + if (!mLoadedModels.isEmpty()) { + throw new IllegalStateException("Cannot detach while models are loaded."); + } + + // From here on, every exception isn't client's fault. + try { + detachInternal(); + } catch (Exception e) { + throw handleException(e); + } + } + } + + private void detachInternal() { + try { + mDelegate.detach(); + mDelegate = null; + mCallback.asBinder().unlinkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////// + // Callbacks + + @Override + public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) { + synchronized (this) { + if (event.status != RecognitionStatus.FORCED) { + mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED; + } + try { + mCallback.onRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) { + synchronized (this) { + if (event.common.status != RecognitionStatus.FORCED) { + mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED; + } + try { + mCallback.onPhraseRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void onRecognitionAvailabilityChange(boolean available) throws RemoteException { + synchronized (this) { + try { + mCallback.onRecognitionAvailabilityChange(available); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void binderDied() { + // This is called whenever our client process dies. + synchronized (this) { + try { + // Gracefully stop all active recognitions and unload the models. + for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) { + if (entry.getValue().activityState == ModelState.Activity.ACTIVE) { + mDelegate.stopRecognition(entry.getKey()); + } + mDelegate.unloadModel(entry.getKey()); + } + // Detach. + detachInternal(); + } catch (Exception e) { + throw handleException(e); + } + } + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java new file mode 100644 index 000000000000..81789e1362c0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; +import android.hardware.soundtrigger.V2_2.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.media.soundtrigger_middleware.Status; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface, + * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate + * clients. + * <p> + * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use + * the module through an {@link ISoundTriggerModule} instance, obtained via {@link + * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared + * between sessions (i.e. cannot use a handle obtained from one session through another). + * <p> + * <b>Important conventions:</b> + * <ul> + * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle + * invalid usage, and such usage will result in undefined behavior. If this service is to be + * offered to an untrusted client, it must be wrapped with input and state validation. + * <li>The underlying driver is assumed to be correct. This implementation does not attempt to + * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this + * service is to used with an untrusted driver, the driver must be wrapped with validation / error + * recovery code. + * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not + * considered recoverable faults and should not occur in a properly functioning system. + * <li>There is no binder instance associated with this implementation. Do not call asBinder(). + * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, + * recoverable faults. The error code would one of the + * {@link android.media.soundtrigger_middleware.Status} constants. Any other exception + * thrown should be regarded as a bug in the implementation or one of its dependencies + * (assuming correct usage). + * <li>The implementation is designed for testability by featuring dependency injection (the + * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies + * on Android runtime. + * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry- + * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance + * for their entire scope. Any other method can be assumed to be running with the lock already + * obtained, so no further locking should be done. While this is not necessarily the most efficient + * synchronization strategy, it is very easy to reason about and this code is likely not on any + * performance-critical + * path. + * </ul> + * + * @hide + */ +class SoundTriggerModule { + static private final String TAG = "SoundTriggerModule"; + @NonNull private final ISoundTriggerHw2 mHalService; + @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider; + private final Set<Session> mActiveSessions = new HashSet<>(); + private int mNumLoadedModels = 0; + private SoundTriggerModuleProperties mProperties = null; + private boolean mRecognitionAvailable; + + /** + * Ctor. + * + * @param halService The underlying HAL driver. + */ + SoundTriggerModule(@NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw halService, + @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) { + assert halService != null; + mHalService = new SoundTriggerHw2Compat(halService); + mAudioSessionProvider = audioSessionProvider; + mProperties = ConversionUtil.hidl2aidlProperties(mHalService.getProperties()); + + // We conservatively assume that external capture is active until explicitly told otherwise. + mRecognitionAvailable = mProperties.concurrentCapture; + } + + /** + * Establish a client session with this module. + * + * This module may be shared by multiple clients, each will get its own session. While resources + * are shared between the clients, each session has its own state and data should not be shared + * across sessions. + * + * @param callback The client callback, which will be used for all messages. This is a oneway + * callback, so will never block, throw an unchecked exception or return a + * value. + * @return The interface through which this module can be controlled. + */ + synchronized @NonNull + Session attach(@NonNull ISoundTriggerCallback callback) { + Log.d(TAG, "attach()"); + Session session = new Session(callback); + mActiveSessions.add(session); + return session; + } + + /** + * Query the module's properties. + * + * @return The properties structure. + */ + synchronized @NonNull + SoundTriggerModuleProperties getProperties() { + return mProperties; + } + + /** + * Notify the module that external capture has started / finished, using the same input device + * used for recognition. + * If the underlying driver does not support recognition while capturing, capture will be + * aborted, and the recognition callback will receive and abort event. In addition, all active + * clients will be notified of the change in state. + * + * @param active true iff external capture is active. + */ + synchronized void setExternalCaptureState(boolean active) { + Log.d(TAG, String.format("setExternalCaptureState(active=%b)", active)); + if (mProperties.concurrentCapture) { + // If we support concurrent capture, we don't care about any of this. + return; + } + mRecognitionAvailable = !active; + if (!mRecognitionAvailable) { + // Our module does not support recognition while a capture is active - + // need to abort all active recognitions. + for (Session session : mActiveSessions) { + session.abortActiveRecognitions(); + } + } + for (Session session : mActiveSessions) { + session.notifyRecognitionAvailability(); + } + } + + /** + * Remove session from the list of active sessions. + * + * @param session The session to remove. + */ + private void removeSession(@NonNull Session session) { + mActiveSessions.remove(session); + } + + /** State of a single sound model. */ + private enum ModelState { + /** Initial state, until load() is called. */ + INIT, + /** Model is loaded, but recognition is not active. */ + LOADED, + /** Model is loaded and recognition is active. */ + ACTIVE + } + + /** + * A single client session with this module. + * + * This is the main interface used to interact with this module. + */ + private class Session implements ISoundTriggerModule { + private ISoundTriggerCallback mCallback; + private Map<Integer, Model> mLoadedModels = new HashMap<>(); + + /** + * Ctor. + * + * @param callback The client callback interface. + */ + private Session(@NonNull ISoundTriggerCallback callback) { + mCallback = callback; + notifyRecognitionAvailability(); + } + + @Override + public void detach() { + Log.d(TAG, "detach()"); + synchronized (SoundTriggerModule.this) { + removeSession(this); + } + } + + @Override + public int loadModel(@NonNull SoundModel model) { + Log.d(TAG, String.format("loadModel(model=%s)", model)); + synchronized (SoundTriggerModule.this) { + if (mNumLoadedModels == mProperties.maxSoundModels) { + throw new RecoverableException(Status.RESOURCE_CONTENTION, + "Maximum number of models loaded."); + } + Model loadedModel = new Model(); + int result = loadedModel.load(model); + ++mNumLoadedModels; + return result; + } + } + + @Override + public int loadPhraseModel(@NonNull PhraseSoundModel model) { + Log.d(TAG, String.format("loadPhraseModel(model=%s)", model)); + synchronized (SoundTriggerModule.this) { + if (mNumLoadedModels == mProperties.maxSoundModels) { + throw new RecoverableException(Status.RESOURCE_CONTENTION, + "Maximum number of models loaded."); + } + Model loadedModel = new Model(); + int result = loadedModel.load(model); + ++mNumLoadedModels; + Log.d(TAG, String.format("loadPhraseModel()->%d", result)); + return result; + } + } + + @Override + public void unloadModel(int modelHandle) { + Log.d(TAG, String.format("unloadModel(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).unload(); + --mNumLoadedModels; + } + } + + @Override + public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { + Log.d(TAG, + String.format("startRecognition(handle=%d, config=%s)", modelHandle, config)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).startRecognition(config); + } + } + + @Override + public void stopRecognition(int modelHandle) { + Log.d(TAG, String.format("stopRecognition(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).stopRecognition(); + } + } + + @Override + public void forceRecognitionEvent(int modelHandle) { + Log.d(TAG, String.format("forceRecognitionEvent(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).forceRecognitionEvent(); + } + } + + @Override + public void setModelParameter(int modelHandle, int modelParam, int value) + throws RemoteException { + Log.d(TAG, + String.format("setModelParameter(handle=%d, param=%d, value=%d)", modelHandle, + modelParam, value)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).setParameter(modelParam, value); + } + } + + @Override + public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { + Log.d(TAG, String.format("getModelParameter(handle=%d, param=%d)", modelHandle, + modelParam)); + synchronized (SoundTriggerModule.this) { + return mLoadedModels.get(modelHandle).getParameter(modelParam); + } + } + + @Override + @Nullable + public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { + Log.d(TAG, String.format("queryModelParameterSupport(handle=%d, param=%d)", modelHandle, + modelParam)); + synchronized (SoundTriggerModule.this) { + return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam); + } + } + + /** + * Abort all currently active recognitions. + */ + private void abortActiveRecognitions() { + for (Model model : mLoadedModels.values()) { + model.abortActiveRecognition(); + } + } + + private void notifyRecognitionAvailability() { + try { + mCallback.onRecognitionAvailabilityChange(mRecognitionAvailable); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + + @Override + public @NonNull + IBinder asBinder() { + throw new UnsupportedOperationException( + "This implementation is not intended to be used directly with Binder."); + } + + /** + * A single sound model in the system. + * + * All model-based operations are delegated to this class and implemented here. + */ + private class Model implements ISoundTriggerHw2.Callback { + public int mHandle; + private ModelState mState = ModelState.INIT; + private int mModelType = SoundModelType.UNKNOWN; + private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession; + + private @NonNull + ModelState getState() { + return mState; + } + + private void setState(@NonNull ModelState state) { + mState = state; + SoundTriggerModule.this.notifyAll(); + } + + private void waitStateChange() throws InterruptedException { + SoundTriggerModule.this.wait(); + } + + private int load(@NonNull SoundModel model) { + mModelType = model.type; + ISoundTriggerHw.SoundModel hidlModel = ConversionUtil.aidl2hidlSoundModel(model); + + mSession = mAudioSessionProvider.acquireSession(); + try { + mHandle = mHalService.loadSoundModel(hidlModel, this, 0); + } catch (Exception e) { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + throw e; + } + + setState(ModelState.LOADED); + mLoadedModels.put(mHandle, this); + return mHandle; + } + + private int load(@NonNull PhraseSoundModel model) { + mModelType = model.common.type; + ISoundTriggerHw.PhraseSoundModel hidlModel = + ConversionUtil.aidl2hidlPhraseSoundModel(model); + + mSession = mAudioSessionProvider.acquireSession(); + try { + mHandle = mHalService.loadPhraseSoundModel(hidlModel, this, 0); + } catch (Exception e) { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + throw e; + } + + setState(ModelState.LOADED); + mLoadedModels.put(mHandle, this); + return mHandle; + } + + private void unload() { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + mHalService.unloadSoundModel(mHandle); + mLoadedModels.remove(mHandle); + } + + private void startRecognition(@NonNull RecognitionConfig config) { + if (!mRecognitionAvailable) { + // Recognition is unavailable - send an abort event immediately. + notifyAbort(); + return; + } + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig hidlConfig = + ConversionUtil.aidl2hidlRecognitionConfig(config); + hidlConfig.header.captureDevice = mSession.mDeviceHandle; + hidlConfig.header.captureHandle = mSession.mIoHandle; + mHalService.startRecognition(mHandle, hidlConfig, this, 0); + setState(ModelState.ACTIVE); + } + + private void stopRecognition() { + if (getState() == ModelState.LOADED) { + // This call is idempotent in order to avoid races. + return; + } + mHalService.stopRecognition(mHandle); + setState(ModelState.LOADED); + } + + /** Request a forced recognition event. Will do nothing if recognition is inactive. */ + private void forceRecognitionEvent() { + if (getState() != ModelState.ACTIVE) { + // This call is idempotent in order to avoid races. + return; + } + mHalService.getModelState(mHandle); + } + + + private void setParameter(int modelParam, int value) { + mHalService.setModelParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam), value); + } + + private int getParameter(int modelParam) { + return mHalService.getModelParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam)); + } + + @Nullable + private ModelParameterRange queryModelParameterSupport(int modelParam) { + return ConversionUtil.hidl2aidlModelParameterRange( + mHalService.queryParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam))); + } + + /** Abort the recognition, if active. */ + private void abortActiveRecognition() { + // If we're inactive, do nothing. + if (getState() != ModelState.ACTIVE) { + return; + } + // Stop recognition. + stopRecognition(); + + // Notify the client that recognition has been aborted. + notifyAbort(); + } + + /** Notify the client that recognition has been aborted. */ + private void notifyAbort() { + try { + switch (mModelType) { + case SoundModelType.GENERIC: { + android.media.soundtrigger_middleware.RecognitionEvent event = + new android.media.soundtrigger_middleware.RecognitionEvent(); + event.status = + android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + mCallback.onRecognition(mHandle, event); + } + break; + + case SoundModelType.KEYPHRASE: { + android.media.soundtrigger_middleware.PhraseRecognitionEvent event = + new android.media.soundtrigger_middleware.PhraseRecognitionEvent(); + event.common = + new android.media.soundtrigger_middleware.RecognitionEvent(); + event.common.status = + android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + mCallback.onPhraseRecognition(mHandle, event); + } + break; + + default: + Log.e(TAG, "Unknown model type: " + mModelType); + + } + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + + @Override + public void recognitionCallback( + @NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent, + int cookie) { + Log.d(TAG, String.format("recognitionCallback_2_1(event=%s, cookie=%d)", + recognitionEvent, cookie)); + synchronized (SoundTriggerModule.this) { + android.media.soundtrigger_middleware.RecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent); + aidlEvent.captureSession = mSession.mSessionHandle; + try { + mCallback.onRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + if (aidlEvent.status + != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) { + setState(ModelState.LOADED); + } + } + } + + @Override + public void phraseRecognitionCallback( + @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent, + int cookie) { + Log.d(TAG, String.format("phraseRecognitionCallback_2_1(event=%s, cookie=%d)", + phraseRecognitionEvent, cookie)); + synchronized (SoundTriggerModule.this) { + android.media.soundtrigger_middleware.PhraseRecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent); + aidlEvent.common.captureSession = mSession.mSessionHandle; + try { + mCallback.onPhraseRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + if (aidlEvent.common.status + != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) { + setState(ModelState.LOADED); + } + } + } + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING new file mode 100644 index 000000000000..9ed894bc1ca9 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.soundtrigger_middleware" + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java new file mode 100644 index 000000000000..80f69d08c089 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import java.util.regex.Pattern; + +/** + * Utilities for representing UUIDs as strings. + * + * @hide + */ +public class UuidUtil { + /** + * Regex pattern that can be used to validate / extract the various fields of a string-formatted + * UUID. + */ + static final Pattern PATTERN = Pattern.compile("^([a-fA-F0-9]{8})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})$"); + + /** Printf-style pattern for formatting the various fields of a UUID as a string. */ + static final String FORMAT = "%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x"; +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java new file mode 100644 index 000000000000..4898e6b59ab2 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.Nullable; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilities for asserting the validity of various data types used by this module. + * Each of the methods below would throw an {@link IllegalArgumentException} if its input is + * invalid. The input's validity is determined irrespective of any context. In cases where the valid + * value space is further limited by state, it is the caller's responsibility to assert. + * + * @hide + */ +public class ValidationUtil { + static void validateUuid(@Nullable String uuid) { + Preconditions.checkNotNull(uuid); + Matcher matcher = UuidUtil.PATTERN.matcher(uuid); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Illegal format for UUID: " + uuid); + } + } + + static void validateGenericModel(@Nullable SoundModel model) { + validateModel(model, SoundModelType.GENERIC); + } + + static void validateModel(@Nullable SoundModel model, int expectedType) { + Preconditions.checkNotNull(model); + if (model.type != expectedType) { + throw new IllegalArgumentException("Invalid type"); + } + validateUuid(model.uuid); + validateUuid(model.vendorUuid); + Preconditions.checkNotNull(model.data); + } + + static void validatePhraseModel(@Nullable PhraseSoundModel model) { + Preconditions.checkNotNull(model); + validateModel(model.common, SoundModelType.KEYPHRASE); + Preconditions.checkNotNull(model.phrases); + for (Phrase phrase : model.phrases) { + Preconditions.checkNotNull(phrase); + if ((phrase.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER + | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION + | RecognitionMode.GENERIC_TRIGGER)) != 0) { + throw new IllegalArgumentException("Invalid recognitionModes"); + } + Preconditions.checkNotNull(phrase.users); + Preconditions.checkNotNull(phrase.locale); + Preconditions.checkNotNull(phrase.text); + } + } + + static void validateRecognitionConfig(@Nullable RecognitionConfig config) { + Preconditions.checkNotNull(config); + Preconditions.checkNotNull(config.phraseRecognitionExtras); + for (PhraseRecognitionExtra extra : config.phraseRecognitionExtras) { + Preconditions.checkNotNull(extra); + if ((extra.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER + | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION + | RecognitionMode.GENERIC_TRIGGER)) != 0) { + throw new IllegalArgumentException("Invalid recognitionModes"); + } + if (extra.confidenceLevel < 0 || extra.confidenceLevel > 100) { + throw new IllegalArgumentException("Invalid confidenceLevel"); + } + Preconditions.checkNotNull(extra.levels); + for (ConfidenceLevel level : extra.levels) { + Preconditions.checkNotNull(level); + if (level.levelPercent < 0 || level.levelPercent > 100) { + throw new IllegalArgumentException("Invalid confidenceLevel"); + } + } + } + Preconditions.checkNotNull(config.data); + } + + static void validateModelParameter(int modelParam) { + switch (modelParam) { + case ModelParameter.THRESHOLD_FACTOR: + return; + + default: + throw new IllegalArgumentException("Invalid model parameter"); + } + } +} diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index fd8094cc43dd..a34b7fdb911c 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -35,6 +35,7 @@ cc_library_static { "com_android_server_power_PowerManagerService.cpp", "com_android_server_security_VerityUtils.cpp", "com_android_server_SerialService.cpp", + "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp", "com_android_server_storage_AppFuseBridge.cpp", "com_android_server_SystemServer.cpp", "com_android_server_TestNetworkService.cpp", diff --git a/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp new file mode 100644 index 000000000000..774534f23b8c --- /dev/null +++ b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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. + */ + +#include <sstream> + +#include "core_jni_helpers.h" +#include <media/AudioSystem.h> + +namespace android { + +namespace { + +#define PACKAGE "com/android/server/soundtrigger_middleware" +#define CLASSNAME PACKAGE "/AudioSessionProviderImpl" +#define SESSION_CLASSNAME PACKAGE "/SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession" + +jobject acquireAudioSession( + JNIEnv* env, + jobject clazz) { + + audio_session_t session; + audio_io_handle_t ioHandle; + audio_devices_t device; + + status_t status = AudioSystem::acquireSoundTriggerSession(&session, + &ioHandle, + &device); + if (status != 0) { + std::ostringstream message; + message + << "AudioSystem::acquireSoundTriggerSession returned an error code: " + << status; + env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"), + message.str().c_str()); + return nullptr; + } + + jclass cls = FindClassOrDie(env, SESSION_CLASSNAME); + jmethodID ctor = GetMethodIDOrDie(env, cls, "<init>", "(III)V"); + return env->NewObject(cls, + ctor, + static_cast<int>(session), + static_cast<int>(ioHandle), + static_cast<int>(device)); +} + +void releaseAudioSession(JNIEnv* env, jobject clazz, jint handle) { + status_t status = + AudioSystem::releaseSoundTriggerSession(static_cast<audio_session_t>(handle)); + + if (status != 0) { + std::ostringstream message; + message + << "AudioSystem::releaseAudioSystemSession returned an error code: " + << status; + env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"), + message.str().c_str()); + } +} + +const JNINativeMethod g_methods[] = { + {"acquireSession", "()L" SESSION_CLASSNAME ";", + reinterpret_cast<void*>(acquireAudioSession)}, + {"releaseSession", "(I)V", + reinterpret_cast<void*>(releaseAudioSession)}, +}; + +} // namespace + +int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + JNIEnv* env) { + return RegisterMethodsOrDie(env, + CLASSNAME, + g_methods, + NELEM(g_methods)); +} + +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 692c9d25baa9..165edf15ca23 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -56,6 +56,8 @@ int register_android_server_net_NetworkStatsService(JNIEnv* env); int register_android_server_security_VerityUtils(JNIEnv* env); int register_android_server_am_AppCompactor(JNIEnv* env); int register_android_server_am_LowMemDetector(JNIEnv* env); +int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + JNIEnv* env); }; using namespace android; @@ -105,5 +107,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_security_VerityUtils(env); register_android_server_am_AppCompactor(env); register_android_server_am_LowMemDetector(env); + register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + env); return JNI_VERSION_1_4; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index f3ac7d6d3cc9..7d6b0c99c5b5 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -148,6 +148,7 @@ import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.signedconfig.SignedConfigService; import com.android.server.soundtrigger.SoundTriggerService; +import com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; import com.android.server.telecom.TelecomLoaderService; @@ -1555,6 +1556,10 @@ public final class SystemServer { } t.traceEnd(); + t.traceBegin("StartSoundTriggerMiddlewareService"); + mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class); + t.traceEnd(); + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BROADCAST_RADIO)) { t.traceBegin("StartBroadcastRadioService"); mSystemServiceManager.startService(BroadcastRadioService.class); diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java new file mode 100644 index 000000000000..5a2ce4540b82 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import static org.junit.Assert.assertEquals; + +import android.hardware.audio.common.V2_0.Uuid; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ConversionUtilTest { + private static final String TAG = "ConversionUtilTest"; + + @Test + public void testUuidRoundTrip() { + Uuid hidl = new Uuid(); + hidl.timeLow = 0xFEDCBA98; + hidl.timeMid = (short) 0xEDCB; + hidl.versionAndTimeHigh = (short) 0xDCBA; + hidl.variantAndClockSeqHigh = (short) 0xCBA9; + hidl.node = new byte[] { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }; + + String aidl = ConversionUtil.hidl2aidlUuid(hidl); + assertEquals("fedcba98-edcb-dcba-cba9-111213141516", aidl); + + Uuid reconstructed = ConversionUtil.aidl2hidlUuid(aidl); + assertEquals(hidl, reconstructed); + } +} diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java new file mode 100644 index 000000000000..82f32f88d3a2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java @@ -0,0 +1,1306 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.audio.common.V2_0.AudioConfig; +import android.hardware.audio.common.V2_0.Uuid; +import android.hardware.soundtrigger.V2_3.OptionalModelParameterRange; +import android.media.audio.common.AudioChannelMask; +import android.media.audio.common.AudioFormat; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.os.HidlMemoryUtil; +import android.os.HwParcel; +import android.os.IHwBinder; +import android.os.IHwInterface; +import android.os.RemoteException; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +@RunWith(Parameterized.class) +public class SoundTriggerMiddlewareImplTest { + private static final String TAG = "SoundTriggerMiddlewareImplTest"; + + // We run the test once for every version of the underlying driver. + @Parameterized.Parameters + public static Object[] data() { + return new Object[]{ + mock(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_2.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_3.ISoundTriggerHw.class), + }; + } + + @Mock + @Parameterized.Parameter + public android.hardware.soundtrigger.V2_0.ISoundTriggerHw mHalDriver; + + @Mock + private SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider = mock( + SoundTriggerMiddlewareImpl.AudioSessionProvider.class); + + private SoundTriggerMiddlewareImpl mService; + + private static ISoundTriggerCallback createCallbackMock() { + return mock(ISoundTriggerCallback.Stub.class, Mockito.CALLS_REAL_METHODS); + } + + private static SoundModel createGenericSoundModel() { + return createSoundModel(SoundModelType.GENERIC); + } + + private static SoundModel createSoundModel(int type) { + SoundModel model = new SoundModel(); + model.type = type; + model.uuid = "12345678-2345-3456-4567-abcdef987654"; + model.vendorUuid = "87654321-5432-6543-7654-456789fedcba"; + model.data = new byte[]{91, 92, 93, 94, 95}; + return model; + } + + private static PhraseSoundModel createPhraseSoundModel() { + PhraseSoundModel model = new PhraseSoundModel(); + model.common = createSoundModel(SoundModelType.KEYPHRASE); + model.phrases = new Phrase[1]; + model.phrases[0] = new Phrase(); + model.phrases[0].id = 123; + model.phrases[0].users = new int[]{5, 6, 7}; + model.phrases[0].locale = "locale"; + model.phrases[0].text = "text"; + model.phrases[0].recognitionModes = + RecognitionMode.USER_AUTHENTICATION | RecognitionMode.USER_IDENTIFICATION; + return model; + } + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties createDefaultProperties( + boolean supportConcurrentCapture) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties(); + properties.implementor = "implementor"; + properties.description = "description"; + properties.version = 123; + properties.uuid = new Uuid(); + properties.uuid.timeLow = 1; + properties.uuid.timeMid = 2; + properties.uuid.versionAndTimeHigh = 3; + properties.uuid.variantAndClockSeqHigh = 4; + properties.uuid.node = new byte[]{5, 6, 7, 8, 9, 10}; + + properties.maxSoundModels = 456; + properties.maxKeyPhrases = 567; + properties.maxUsers = 678; + properties.recognitionModes = 789; + properties.captureTransition = true; + properties.maxBufferMs = 321; + properties.concurrentCapture = supportConcurrentCapture; + properties.triggerInEvent = true; + properties.powerConsumptionMw = 432; + return properties; + } + + private static void validateDefaultProperties(SoundTriggerModuleProperties properties, + boolean supportConcurrentCapture) { + assertEquals("implementor", properties.implementor); + assertEquals("description", properties.description); + assertEquals(123, properties.version); + assertEquals("00000001-0002-0003-0004-05060708090a", properties.uuid); + assertEquals(456, properties.maxSoundModels); + assertEquals(567, properties.maxKeyPhrases); + assertEquals(678, properties.maxUsers); + assertEquals(789, properties.recognitionModes); + assertTrue(properties.captureTransition); + assertEquals(321, properties.maxBufferMs); + assertEquals(supportConcurrentCapture, properties.concurrentCapture); + assertTrue(properties.triggerInEvent); + assertEquals(432, properties.powerConsumptionMw); + } + + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_0( + int hwHandle, + int status) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent(); + halEvent.status = status; + halEvent.type = SoundModelType.GENERIC; + halEvent.model = hwHandle; + halEvent.captureAvailable = true; + // This field is ignored. + halEvent.captureSession = 123; + halEvent.captureDelayMs = 234; + halEvent.capturePreambleMs = 345; + halEvent.triggerInData = true; + halEvent.audioConfig = new AudioConfig(); + halEvent.audioConfig.sampleRateHz = 456; + halEvent.audioConfig.channelMask = AudioChannelMask.IN_LEFT; + halEvent.audioConfig.format = AudioFormat.MP3; + // hwEvent.audioConfig.offloadInfo is irrelevant. + halEvent.data.add((byte) 31); + halEvent.data.add((byte) 32); + halEvent.data.add((byte) 33); + return halEvent; + } + + private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_1( + int hwHandle, + int status) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent(); + halEvent.header = createRecognitionEvent_2_0(hwHandle, status); + halEvent.header.data.clear(); + halEvent.data = HidlMemoryUtil.byteArrayToHidlMemory(new byte[]{31, 32, 33}); + return halEvent; + } + + private static void validateRecognitionEvent(RecognitionEvent event, int status) { + assertEquals(status, event.status); + assertEquals(SoundModelType.GENERIC, event.type); + assertTrue(event.captureAvailable); + assertEquals(101, event.captureSession); + assertEquals(234, event.captureDelayMs); + assertEquals(345, event.capturePreambleMs); + assertTrue(event.triggerInData); + assertEquals(456, event.audioConfig.sampleRateHz); + assertEquals(AudioChannelMask.IN_LEFT, event.audioConfig.channelMask); + assertEquals(AudioFormat.MP3, event.audioConfig.format); + } + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_0( + int hwHandle, int status) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + halEvent.common = createRecognitionEvent_2_0(hwHandle, status); + + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + halExtra.id = 123; + halExtra.confidenceLevel = 52; + halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER + | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + halLevel.userId = 31; + halLevel.levelPercent = 43; + halExtra.levels.add(halLevel); + halEvent.phraseExtras.add(halExtra); + return halEvent; + } + + private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_1( + int hwHandle, int status) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + halEvent.common = createRecognitionEvent_2_1(hwHandle, status); + + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + halExtra.id = 123; + halExtra.confidenceLevel = 52; + halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER + | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + halLevel.userId = 31; + halLevel.levelPercent = 43; + halExtra.levels.add(halLevel); + halEvent.phraseExtras.add(halExtra); + return halEvent; + } + + private static void validatePhraseRecognitionEvent(PhraseRecognitionEvent event, int status) { + validateRecognitionEvent(event.common, status); + + assertEquals(1, event.phraseExtras.length); + assertEquals(123, event.phraseExtras[0].id); + assertEquals(52, event.phraseExtras[0].confidenceLevel); + assertEquals(RecognitionMode.VOICE_TRIGGER | RecognitionMode.GENERIC_TRIGGER, + event.phraseExtras[0].recognitionModes); + assertEquals(1, event.phraseExtras[0].levels.length); + assertEquals(31, event.phraseExtras[0].levels[0].userId); + assertEquals(43, event.phraseExtras[0].levels[0].levelPercent); + } + + private void initService(boolean supportConcurrentCapture) throws RemoteException { + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties = + createDefaultProperties( + supportConcurrentCapture); + ((android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback) invocation.getArgument( + 0)).onValues(0, + properties); + return null; + }).when(mHalDriver).getProperties(any()); + mService = new SoundTriggerMiddlewareImpl(mHalDriver, mAudioSessionProvider); + } + + private int loadGenericModel_2_0(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + SoundModel model = createGenericSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel> modelCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback = + invocation.getArgument(1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadSoundModelCallback + resultCallback = invocation.getArgument(3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.status = + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.model = hwHandle; + callback.soundModelCallback(modelEvent, callbackCookie); + return null; + }).when(mHalDriver).loadSoundModel(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadModel(model); + verify(mHalDriver).loadSoundModel(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel hidlModel = + modelCaptor.getValue(); + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC, + hidlModel.type); + assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.uuid)); + assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.vendorUuid)); + assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.data.toArray()); + + return handle; + } + + private int loadGenericModel_2_1(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + SoundModel model = createGenericSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel> modelCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback = + invocation.getArgument(1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback + resultCallback = invocation.getArgument(3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.header.status = + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.header.model = hwHandle; + callback.soundModelCallback_2_1(modelEvent, callbackCookie); + return null; + }).when(driver).loadSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadModel(model); + verify(driver).loadSoundModel_2_1(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel hidlModel = + modelCaptor.getValue(); + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC, + hidlModel.header.type); + assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.uuid)); + assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.vendorUuid)); + assertArrayEquals(new byte[]{91, 92, 93, 94, 95}, + HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.data)); + + return handle; + } + + private int loadGenericModel(ISoundTriggerModule module, int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return loadGenericModel_2_1(module, hwHandle); + } else { + return loadGenericModel_2_0(module, hwHandle); + } + } + + private int loadPhraseModel_2_0(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + PhraseSoundModel model = createPhraseSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel> + modelCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback = + invocation.getArgument( + 1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadPhraseSoundModelCallback + resultCallback = + invocation.getArgument( + 3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.status = + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.model = hwHandle; + callback.soundModelCallback(modelEvent, callbackCookie); + return null; + }).when(mHalDriver).loadPhraseSoundModel(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadPhraseModel(model); + verify(mHalDriver).loadPhraseSoundModel(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel hidlModel = + modelCaptor.getValue(); + + // Validate common part. + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE, + hidlModel.common.type); + assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.uuid)); + assertEquals(model.common.vendorUuid, + ConversionUtil.hidl2aidlUuid(hidlModel.common.vendorUuid)); + assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.common.data.toArray()); + + // Validate phrase part. + assertEquals(1, hidlModel.phrases.size()); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Phrase hidlPhrase = + hidlModel.phrases.get(0); + assertEquals(123, hidlPhrase.id); + assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray()); + assertEquals("locale", hidlPhrase.locale); + assertEquals("text", hidlPhrase.text); + assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION + | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION, + hidlPhrase.recognitionModes); + + return handle; + } + + private int loadPhraseModel_2_1(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + + PhraseSoundModel model = createPhraseSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel> + modelCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback = + invocation.getArgument( + 1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback + resultCallback = + invocation.getArgument( + 3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.header.status = + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.header.model = hwHandle; + callback.soundModelCallback_2_1(modelEvent, callbackCookie); + return null; + }).when(driver).loadPhraseSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadPhraseModel(model); + verify(driver).loadPhraseSoundModel_2_1(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel hidlModel = + modelCaptor.getValue(); + + // Validate common part. + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE, + hidlModel.common.header.type); + assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.header.uuid)); + assertEquals(model.common.vendorUuid, + ConversionUtil.hidl2aidlUuid(hidlModel.common.header.vendorUuid)); + assertArrayEquals(new byte[]{91, 92, 93, 94, 95}, + HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.common.data)); + + // Validate phrase part. + assertEquals(1, hidlModel.phrases.size()); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Phrase hidlPhrase = + hidlModel.phrases.get(0); + assertEquals(123, hidlPhrase.id); + assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray()); + assertEquals("locale", hidlPhrase.locale); + assertEquals("text", hidlPhrase.text); + assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION + | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION, + hidlPhrase.recognitionModes); + + return handle; + } + + private int loadPhraseModel(ISoundTriggerModule module, int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return loadPhraseModel_2_1(module, hwHandle); + } else { + return loadPhraseModel_2_0(module, hwHandle); + } + } + + private void unloadModel(ISoundTriggerModule module, int handle, int hwHandle) + throws RemoteException { + module.unloadModel(handle); + verify(mHalDriver).unloadSoundModel(hwHandle); + verify(mAudioSessionProvider).releaseSession(101); + } + + private SoundTriggerHwCallback startRecognition_2_0(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig> + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback> callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.class); + ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); + + when(mHalDriver.startRecognition(eq(hwHandle), configCaptor.capture(), + callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0); + + RecognitionConfig config = createRecognitionConfig(); + + module.startRecognition(handle, config); + verify(mHalDriver).startRecognition(eq(hwHandle), any(), any(), anyInt()); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig halConfig = + configCaptor.getValue(); + assertTrue(halConfig.captureRequested); + assertEquals(102, halConfig.captureHandle); + assertEquals(103, halConfig.captureDevice); + assertEquals(1, halConfig.phrases.size()); + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra = + halConfig.phrases.get(0); + assertEquals(123, halPhraseExtra.id); + assertEquals(4, halPhraseExtra.confidenceLevel); + assertEquals(5, halPhraseExtra.recognitionModes); + assertEquals(1, halPhraseExtra.levels.size()); + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0); + assertEquals(234, halLevel.userId); + assertEquals(34, halLevel.levelPercent); + assertArrayEquals(new Byte[]{5, 4, 3, 2, 1}, halConfig.data.toArray()); + + return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue()); + } + + private SoundTriggerHwCallback startRecognition_2_1(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig> + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback> callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.class); + ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); + + when(driver.startRecognition_2_1(eq(hwHandle), configCaptor.capture(), + callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0); + + RecognitionConfig config = createRecognitionConfig(); + + module.startRecognition(handle, config); + verify(driver).startRecognition_2_1(eq(hwHandle), any(), any(), anyInt()); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig halConfig = + configCaptor.getValue(); + assertTrue(halConfig.header.captureRequested); + assertEquals(102, halConfig.header.captureHandle); + assertEquals(103, halConfig.header.captureDevice); + assertEquals(1, halConfig.header.phrases.size()); + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra = + halConfig.header.phrases.get(0); + assertEquals(123, halPhraseExtra.id); + assertEquals(4, halPhraseExtra.confidenceLevel); + assertEquals(5, halPhraseExtra.recognitionModes); + assertEquals(1, halPhraseExtra.levels.size()); + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0); + assertEquals(234, halLevel.userId); + assertEquals(34, halLevel.levelPercent); + assertArrayEquals(new byte[]{5, 4, 3, 2, 1}, + HidlMemoryUtil.hidlMemoryToByteArray(halConfig.data)); + + return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue()); + } + + private SoundTriggerHwCallback startRecognition(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return startRecognition_2_1(module, handle, hwHandle); + } else { + return startRecognition_2_0(module, handle, hwHandle); + } + } + + private RecognitionConfig createRecognitionConfig() { + RecognitionConfig config = new RecognitionConfig(); + config.captureRequested = true; + config.phraseRecognitionExtras = new PhraseRecognitionExtra[]{new PhraseRecognitionExtra()}; + config.phraseRecognitionExtras[0].id = 123; + config.phraseRecognitionExtras[0].confidenceLevel = 4; + config.phraseRecognitionExtras[0].recognitionModes = 5; + config.phraseRecognitionExtras[0].levels = new ConfidenceLevel[]{new ConfidenceLevel()}; + config.phraseRecognitionExtras[0].levels[0].userId = 234; + config.phraseRecognitionExtras[0].levels[0].levelPercent = 34; + config.data = new byte[]{5, 4, 3, 2, 1}; + return config; + } + + private void stopRecognition(ISoundTriggerModule module, int handle, int hwHandle) + throws RemoteException { + when(mHalDriver.stopRecognition(hwHandle)).thenReturn(0); + module.stopRecognition(handle); + verify(mHalDriver).stopRecognition(hwHandle); + } + + private void verifyNotStartRecognition() throws RemoteException { + verify(mHalDriver, never()).startRecognition(anyInt(), any(), any(), anyInt()); + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + verify((android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver, + never()).startRecognition_2_1(anyInt(), any(), any(), anyInt()); + } + } + + + @Before + public void setUp() throws Exception { + clearInvocations(mHalDriver); + clearInvocations(mAudioSessionProvider); + + // This binder is associated with the mock, so it can be cast to either version of the + // HAL interface. + final IHwBinder binder = new IHwBinder() { + @Override + public void transact(int code, HwParcel request, HwParcel reply, int flags) + throws RemoteException { + // This is a little hacky, but a very easy way to gracefully reject a request for + // an unsupported interface (after queryLocalInterface() returns null, the client + // will attempt a remote transaction to obtain the interface. RemoteException will + // cause it to give up). + throw new RemoteException(); + } + + @Override + public IHwInterface queryLocalInterface(String descriptor) { + if (descriptor.equals("android.hardware.soundtrigger@2.0::ISoundTriggerHw") + || descriptor.equals("android.hardware.soundtrigger@2.1::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw + || descriptor.equals("android.hardware.soundtrigger@2.2::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw + || descriptor.equals("android.hardware.soundtrigger@2.3::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) { + return mHalDriver; + } + return null; + } + + @Override + public boolean linkToDeath(DeathRecipient recipient, long cookie) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean unlinkToDeath(DeathRecipient recipient) { + throw new UnsupportedOperationException(); + } + }; + + when(mHalDriver.asBinder()).thenReturn(binder); + } + + @Test + public void testSetUpAndTearDown() { + } + + @Test + public void testListModules() throws Exception { + initService(true); + // Note: input and output properties are NOT the same type, even though they are in any way + // equivalent. One is a type that's exposed by the HAL and one is a type that's exposed by + // the service. The service actually performs a (trivial) conversion between the two. + SoundTriggerModuleDescriptor[] allDescriptors = mService.listModules(); + assertEquals(1, allDescriptors.length); + + SoundTriggerModuleProperties properties = allDescriptors[0].properties; + + validateDefaultProperties(properties, true); + } + + @Test + public void testAttachDetach() throws Exception { + // Normal attachment / detachment. + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + assertNotNull(module); + module.detach(); + } + + @Test + public void testAttachDetachNotAvailable() throws Exception { + // Attachment / detachment during external capture, with a module not supporting concurrent + // capture. + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(false); + assertNotNull(module); + module.detach(); + } + + @Test + public void testAttachDetachAvailable() throws Exception { + // Attachment / detachment during external capture, with a module supporting concurrent + // capture. + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + assertNotNull(module); + module.detach(); + } + + @Test + public void testLoadUnloadModel() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testLoadUnloadPhraseModel() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + final int hwHandle = 73; + int handle = loadPhraseModel(module, hwHandle); + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testStartStopRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testStartStopPhraseRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 67; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Signal a capture from the driver. + hwCallback.sendRecognitionEvent(hwHandle, + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testPhraseRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Signal a capture from the driver. + hwCallback.sendPhraseRecognitionEvent(hwHandle, + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testForceRecognition() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver; + + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 17; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Force a trigger. + module.forceRecognitionEvent(handle); + verify(driver).getModelState(hwHandle); + + // Signal a capture from the driver. + // '3' means 'forced', there's no constant for that in the HAL. + hwCallback.sendRecognitionEvent(hwHandle, 3); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testForcePhraseRecognition() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver; + + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 17; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Force a trigger. + module.forceRecognitionEvent(handle); + verify(driver).getModelState(hwHandle); + + // Signal a capture from the driver. + // '3' means 'forced', there's no constant for that in the HAL. + hwCallback.sendPhraseRecognitionEvent(hwHandle, 3); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testAbortRecognition() throws Exception { + // Make sure the HAL doesn't support concurrent capture. + initService(false); + mService.setExternalCaptureState(false); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + + // Load the model. + final int hwHandle = 11; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Abort. + mService.setExternalCaptureState(true); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status); + + // Make sure we are notified of the lost availability. + verify(callback).onRecognitionAvailabilityChange(false); + + // Attempt to start a new recognition - should get an abort event immediately, without + // involving the HAL. + clearInvocations(callback); + clearInvocations(mHalDriver); + module.startRecognition(handle, createRecognitionConfig()); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status); + verifyNotStartRecognition(); + + // Now enable it and make sure we are notified. + mService.setExternalCaptureState(false); + verify(callback).onRecognitionAvailabilityChange(true); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testAbortPhraseRecognition() throws Exception { + // Make sure the HAL doesn't support concurrent capture. + initService(false); + mService.setExternalCaptureState(false); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + + // Load the model. + final int hwHandle = 11; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Abort. + mService.setExternalCaptureState(true); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status); + + // Make sure we are notified of the lost availability. + verify(callback).onRecognitionAvailabilityChange(false); + + // Attempt to start a new recognition - should get an abort event immediately, without + // involving the HAL. + clearInvocations(callback); + clearInvocations(mHalDriver); + module.startRecognition(handle, createRecognitionConfig()); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status); + verifyNotStartRecognition(); + + // Now enable it and make sure we are notified. + mService.setExternalCaptureState(false); + verify(callback).onRecognitionAvailabilityChange(true); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testNotAbortRecognitionConcurrent() throws Exception { + // Make sure the HAL supports concurrent capture. + initService(true); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + clearInvocations(callback); + + // Load the model. + final int hwHandle = 13; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Signal concurrent capture. Shouldn't abort. + mService.setExternalCaptureState(true); + verify(callback, never()).onRecognition(anyInt(), any()); + verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean()); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Initiating a new one should work fine. + clearInvocations(mHalDriver); + startRecognition(module, handle, hwHandle); + verify(callback, never()).onRecognition(anyInt(), any()); + stopRecognition(module, handle, hwHandle); + + // Unload the model. + module.unloadModel(handle); + module.detach(); + } + + @Test + public void testNotAbortPhraseRecognitionConcurrent() throws Exception { + // Make sure the HAL supports concurrent capture. + initService(true); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + clearInvocations(callback); + + // Load the model. + final int hwHandle = 13; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Signal concurrent capture. Shouldn't abort. + mService.setExternalCaptureState(true); + verify(callback, never()).onPhraseRecognition(anyInt(), any()); + verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean()); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Initiating a new one should work fine. + clearInvocations(mHalDriver); + startRecognition(module, handle, hwHandle); + verify(callback, never()).onRecognition(anyInt(), any()); + stopRecognition(module, handle, hwHandle); + + // Unload the model. + module.unloadModel(handle); + module.detach(); + } + + @Test + public void testParameterSupported() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 12; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer((Answer<Void>) invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback + resultCallback = invocation.getArgument(2); + android.hardware.soundtrigger.V2_3.ModelParameterRange range = + new android.hardware.soundtrigger.V2_3.ModelParameterRange(); + range.start = 23; + range.end = 45; + OptionalModelParameterRange optionalRange = new OptionalModelParameterRange(); + optionalRange.range(range); + resultCallback.onValues(0, optionalRange); + return null; + }).when(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + verify(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertEquals(23, range.minInclusive); + assertEquals(45, range.maxInclusive); + } + + @Test + public void testParameterNotSupportedOld() throws Exception { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) { + return; + } + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 13; + int modelHandle = loadGenericModel(module, hwHandle); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + assertNull(range); + } + + @Test + public void testParameterNotSupported() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 13; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback + resultCallback = invocation.getArgument(2); + // This is the return of this method. + resultCallback.onValues(0, new OptionalModelParameterRange()); + return null; + }).when(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + verify(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertNull(range); + } + + @Test + public void testGetParameter() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 14; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.getParameterCallback + resultCallback = invocation.getArgument(2); + // This is the return of this method. + resultCallback.onValues(0, 234); + return null; + }).when(driver).getParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + int value = module.getModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR); + + verify(driver).getParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertEquals(234, value); + } + + @Test + public void testSetParameter() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 17; + int modelHandle = loadGenericModel(module, hwHandle); + + when(driver.setParameter(hwHandle, + android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR, + 456)).thenReturn(0); + + module.setModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR, 456); + + verify(driver).setParameter(hwHandle, + android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR, 456); + } + + private static class SoundTriggerHwCallback { + private final android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback mCallback; + private final int mCookie; + + SoundTriggerHwCallback(android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback, + int cookie) { + mCallback = callback; + mCookie = cookie; + } + + private void sendRecognitionEvent(int hwHandle, int status) throws RemoteException { + if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) { + ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).recognitionCallback_2_1( + createRecognitionEvent_2_1(hwHandle, status), mCookie); + } else { + mCallback.recognitionCallback(createRecognitionEvent_2_0(hwHandle, status), + mCookie); + } + } + + private void sendPhraseRecognitionEvent(int hwHandle, int status) throws RemoteException { + if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) { + ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).phraseRecognitionCallback_2_1( + createPhraseRecognitionEvent_2_1(hwHandle, status), mCookie); + } else { + mCallback.phraseRecognitionCallback( + createPhraseRecognitionEvent_2_0(hwHandle, status), mCookie); + } + } + } +} |