diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 7b580c3bde793..671b589a881a1 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4324,6 +4324,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. * diff --git a/media/Android.bp b/media/Android.bp index 20a9656e0479f..97d3138148526 100644 --- a/media/Android.bp +++ b/media/Android.bp @@ -141,6 +141,8 @@ aidl_interface { "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", diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl index 202595a9d89c7..c4a57857dd3da 100644 --- a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl @@ -15,6 +15,8 @@ */ 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; @@ -96,6 +98,47 @@ interface ISoundTriggerModule { */ 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. 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 0000000000000..09936278e93a7 --- /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 0000000000000..d6948a87dc6d7 --- /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/services/core/Android.bp b/services/core/Android.bp index 203bc61c20224..b7adfa4a3ff14 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 0000000000000..3fa52301d9a0f --- /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 0000000000000..9b22f33a20b0b --- /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 0000000000000..8b3e70875183f --- /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). + *

+ * 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 0000000000000..f0a0d8305bc6a --- /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 0000000000000..81252c9a8c146 --- /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: + *

+ * For cases where the client wants to explicitly handle specific versions of the underlying driver + * interface, they may call {@link #interfaceDescriptor()}. + *

+ * Note to maintainers: 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 0000000000000..e1fb2266b7c6e --- /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. + *

+ * 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). + *

+ * 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 0000000000000..83618505814e4 --- /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: + *

+ *

+ * Some recoverable faults are permanent and some are transient / circumstantial, the specific error + * code can provide more information about the possible recovery options. + *

+ * 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 0000000000000..4a852c4b68e83 --- /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. + *

+ * Exception handling: + *

+ */ +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 + 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 + 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 0000000000000..9d51b65ea1527 --- /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. + *

+ * Important conventions: + *

+ * + * @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 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 0000000000000..a7cfe1037f11a --- /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. + *

+ * 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. + *

+ * Every public method in this class, overriding an interface method, must follow the following + * pattern: + *

+ * @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);
+ *         }
+ *     }
+ * }
+ * 
+ * Following this patterns ensures a consistent and rigorous handling of all aspects associated + * with client-server separation. + *

+ * Exception handling approach:
+ * 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.
+ * 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 does NOT 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. + *

+ * Exposing this service as a System Service:
+ * Insert this line into {@link com.android.server.SystemServer}: + *

+ * mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class);
+ * 
+ * + * {@hide} + */ +public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub { + static private final String TAG = "SoundTriggerMiddlewareService"; + + final ISoundTriggerMiddlewareService mDelegate; + final Context mContext; + Set 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 any other exception as {@link InternalServerError} + * (not passed by Binder to the caller). + *

+ * Typical usage: + *

+     * try {
+     *     ... Do server operations ...
+     * } catch (Exception e) {
+     *     throw handleException(e);
+     * }
+     * 
+ */ + 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 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 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 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 0000000000000..3444be9353951 --- /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. + *

+ * 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). + *

+ * Important conventions: + *

    + *
  • 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. + *
  • 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. + *
  • RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not + * considered recoverable faults and should not occur in a properly functioning system. + *
  • There is no binder instance associated with this implementation. Do not call asBinder(). + *
  • 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). + *
  • 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. + *
  • 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. + *
+ * + * @hide + */ +class SoundTriggerModule { + static private final String TAG = "SoundTriggerModule"; + @NonNull private final ISoundTriggerHw2 mHalService; + @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider; + private final Set 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 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 0000000000000..9ed894bc1ca97 --- /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 0000000000000..80f69d08c0898 --- /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 0000000000000..4898e6b59ab28 --- /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 fd8094cc43ddd..a34b7fdb911c1 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 0000000000000..774534f23b8c2 --- /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 + +#include "core_jni_helpers.h" +#include + +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, "", "(III)V"); + return env->NewObject(cls, + ctor, + static_cast(session), + static_cast(ioHandle), + static_cast(device)); +} + +void releaseAudioSession(JNIEnv* env, jobject clazz, jint handle) { + status_t status = + AudioSystem::releaseSoundTriggerSession(static_cast(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(acquireAudioSession)}, + {"releaseSession", "(I)V", + reinterpret_cast(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 692c9d25baa99..165edf15ca231 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 b6e501a785efc..7c43972dfed71 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -147,6 +147,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; @@ -1544,6 +1545,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 0000000000000..5a2ce4540b82d --- /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 0000000000000..82f32f88d3a2e --- /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 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 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 + 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 + 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 + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.class); + ArgumentCaptor 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 + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.class); + ArgumentCaptor 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 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 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 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 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 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 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) 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); + } + } + } +}