Implement the soundtrigger_middlewware service
This service is intended to replace: frameworks/av/include/soundtrigger/ISoundTriggerHwService.h This change only adds the replacement service, follow up changes migrate the clients to use the new service and remove the old one. The new service is feature-equivalent to the new one, but offers the following advantages: - AIDL interface (as opposed to hand-written parceling code). - Pure Java implementation all the way to the HAL. - Better documentation. - Rigorous error handling. - Unit tests. - Reduced code complexity (less layers, better separation of concerns). - Permission-based security model (as opposed to some baked-in assumptions about process affinity). Change-Id: I79f4eff105d3e6245990be068b933d4d48c35a0d Bug: 142070343
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* This exception represents a non-zero status code returned by a HAL invocation.
|
||||
* Depending on the operation that threw the error, the integrity of the HAL implementation and the
|
||||
* client's tolerance to error, this error may or may not be recoverable. The HAL itself is expected
|
||||
* to retain the state it had prior to the invocation (so, unless the error is a result of a HAL
|
||||
* bug, normal operation may resume).
|
||||
* <p>
|
||||
* The reason why this is a RuntimeException, even though the HAL interface allows returning them
|
||||
* is because we expect none of them to actually occur as part of correct usage of the HAL.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class HalException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public HalException(int errorCode, @NonNull String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HalException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return super.toString() + " (code " + errorCode + ")";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.hardware.soundtrigger.V2_3.ISoundTriggerHw;
|
||||
import android.hardware.soundtrigger.V2_3.ModelParameterRange;
|
||||
import android.hidl.base.V1_0.IBase;
|
||||
import android.os.IHwBinder;
|
||||
|
||||
/**
|
||||
* This interface mimics android.hardware.soundtrigger.V2_x.ISoundTriggerHw and
|
||||
* android.hardware.soundtrigger.V2_x.ISoundTriggerHwCallback, with a few key differences:
|
||||
* <ul>
|
||||
* <li>Methods in the original interface generally have a status return value and potentially a
|
||||
* second return value which is the actual return value. This is reflected via a synchronous
|
||||
* callback, which is not very pleasant to work with. This interface replaces that pattern with
|
||||
* the convention that a HalException is thrown for non-OK status, and then we can use the
|
||||
* return value for the actual return value.
|
||||
* <li>This interface will always include all the methods from the latest 2.x version (and thus
|
||||
* from every 2.x version) interface, with the convention that unsupported methods throw a
|
||||
* {@link RecoverableException} with a
|
||||
* {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED}
|
||||
* code.
|
||||
* <li>Cases where the original interface had multiple versions of a method representing the exact
|
||||
* thing, or there exists a trivial conversion between the new and old version, this interface
|
||||
* represents only the latest version, without any _version suffixes.
|
||||
* <li>Removes some of the obscure IBinder methods.
|
||||
* <li>No RemoteExceptions are specified. Some implementations of this interface may rethrow
|
||||
* RemoteExceptions as RuntimeExceptions, some can guarantee handling them somehow and never throw
|
||||
* them.
|
||||
* <li>soundModelCallback has been removed, since nobody cares about it. Implementations are free
|
||||
* to silently discard it.
|
||||
* </ul>
|
||||
* For cases where the client wants to explicitly handle specific versions of the underlying driver
|
||||
* interface, they may call {@link #interfaceDescriptor()}.
|
||||
* <p>
|
||||
* <b>Note to maintainers</b>: This class must always be kept in sync with the latest 2.x version,
|
||||
* so that clients have access to the entire functionality without having to burden themselves with
|
||||
* compatibility, as much as possible.
|
||||
*/
|
||||
public interface ISoundTriggerHw2 {
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getProperties(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback
|
||||
*/
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties();
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback)
|
||||
*/
|
||||
int loadSoundModel(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
|
||||
SoundTriggerHw2Compat.Callback callback, int cookie);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadPhraseSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback)
|
||||
*/
|
||||
int loadPhraseSoundModel(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
|
||||
SoundTriggerHw2Compat.Callback callback, int cookie);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#unloadSoundModel(int)
|
||||
*/
|
||||
void unloadSoundModel(int modelHandle);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopRecognition(int)
|
||||
*/
|
||||
void stopRecognition(int modelHandle);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopAllRecognitions()
|
||||
*/
|
||||
void stopAllRecognitions();
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#startRecognition_2_1(int,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig,
|
||||
* android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int)
|
||||
*/
|
||||
void startRecognition(int modelHandle,
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
|
||||
SoundTriggerHw2Compat.Callback callback, int cookie);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getModelState(int)
|
||||
*/
|
||||
void getModelState(int modelHandle);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#getParameter(int, int,
|
||||
* ISoundTriggerHw.getParameterCallback)
|
||||
*/
|
||||
int getModelParameter(int modelHandle, int param);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#setParameter(int, int, int)
|
||||
*/
|
||||
void setModelParameter(int modelHandle, int param, int value);
|
||||
|
||||
/**
|
||||
* @return null if not supported.
|
||||
* @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#queryParameter(int, int,
|
||||
* ISoundTriggerHw.queryParameterCallback)
|
||||
*/
|
||||
ModelParameterRange queryParameter(int modelHandle, int param);
|
||||
|
||||
/**
|
||||
* @see IHwBinder#linkToDeath(IHwBinder.DeathRecipient, long)
|
||||
*/
|
||||
boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie);
|
||||
|
||||
/**
|
||||
* @see IHwBinder#unlinkToDeath(IHwBinder.DeathRecipient)
|
||||
*/
|
||||
boolean unlinkToDeath(IHwBinder.DeathRecipient recipient);
|
||||
|
||||
/**
|
||||
* @see IBase#interfaceDescriptor()
|
||||
*/
|
||||
String interfaceDescriptor() throws android.os.RemoteException;
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#recognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent,
|
||||
* int)
|
||||
*/
|
||||
void recognitionCallback(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event,
|
||||
int cookie);
|
||||
|
||||
/**
|
||||
* @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#phraseRecognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent,
|
||||
* int)
|
||||
*/
|
||||
void phraseRecognitionCallback(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
|
||||
int cookie);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* An internal server error.
|
||||
* <p>
|
||||
* This exception wraps any exception thrown from a service implementation, which is a result of a
|
||||
* bug in the server implementation (or any of its dependencies).
|
||||
* <p>
|
||||
* Specifically, this type is excluded from the set of whitelisted exceptions that binder would
|
||||
* tunnel to the client process, since these exceptions are ambiguous regarding whether the client
|
||||
* had done something wrong or the server is buggy. For example, a client getting an
|
||||
* IllegalArgumentException cannot easily determine whether they had provided illegal arguments to
|
||||
* the method they were calling, or whether the method implementation provided illegal arguments to
|
||||
* some method it was calling due to a bug.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class InternalServerError extends RuntimeException {
|
||||
public InternalServerError(@NonNull Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* This exception represents a fault which:
|
||||
* <ul>
|
||||
* <li>Could not have been anticipated by a caller (i.e. is not a violation of any preconditions).
|
||||
* <li>Is guaranteed to not have been caused any meaningful state change in the callee. The caller
|
||||
* may continue operation as if the call has never been made.
|
||||
* </ul>
|
||||
* <p>
|
||||
* Some recoverable faults are permanent and some are transient / circumstantial, the specific error
|
||||
* code can provide more information about the possible recovery options.
|
||||
* <p>
|
||||
* The reason why this is a RuntimeException is to allow it to go through interfaces defined by
|
||||
* AIDL, which we have no control over.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class RecoverableException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public RecoverableException(int errorCode, @NonNull String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public RecoverableException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return super.toString() + " (code " + errorCode + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.media.soundtrigger_middleware.Status;
|
||||
import android.os.IHwBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* An implementation of {@link ISoundTriggerHw2}, on top of any
|
||||
* android.hardware.soundtrigger.V2_x.ISoundTriggerHw implementation. This class hides away some of
|
||||
* the details involved with retaining backward compatibility and adapts to the more pleasant syntax
|
||||
* exposed by {@link ISoundTriggerHw2}, compared to the bare driver interface.
|
||||
* <p>
|
||||
* Exception handling:
|
||||
* <ul>
|
||||
* <li>All {@link RemoteException}s get rethrown as {@link RuntimeException}.
|
||||
* <li>All HAL malfunctions get thrown as {@link HalException}.
|
||||
* <li>All unsupported operations get thrown as {@link RecoverableException} with a
|
||||
* {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED}
|
||||
* code.
|
||||
* </ul>
|
||||
*/
|
||||
final class SoundTriggerHw2Compat implements ISoundTriggerHw2 {
|
||||
private final @NonNull
|
||||
IHwBinder mBinder;
|
||||
private final @NonNull
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw mUnderlying_2_0;
|
||||
private final @Nullable
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw mUnderlying_2_1;
|
||||
private final @Nullable
|
||||
android.hardware.soundtrigger.V2_2.ISoundTriggerHw mUnderlying_2_2;
|
||||
private final @Nullable
|
||||
android.hardware.soundtrigger.V2_3.ISoundTriggerHw mUnderlying_2_3;
|
||||
|
||||
public SoundTriggerHw2Compat(
|
||||
@NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw underlying) {
|
||||
this(underlying.asBinder());
|
||||
}
|
||||
|
||||
public SoundTriggerHw2Compat(IHwBinder binder) {
|
||||
Objects.requireNonNull(binder);
|
||||
|
||||
mBinder = binder;
|
||||
|
||||
// We want to share the proxy instances rather than create a separate proxy for every
|
||||
// version, so we go down the versions in descending order to find the latest one supported,
|
||||
// and then simply up-cast it to obtain all the versions that are earlier.
|
||||
|
||||
// Attempt 2.3
|
||||
android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3 =
|
||||
android.hardware.soundtrigger.V2_3.ISoundTriggerHw.asInterface(binder);
|
||||
if (as2_3 != null) {
|
||||
mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = as2_3;
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt 2.2
|
||||
android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2 =
|
||||
android.hardware.soundtrigger.V2_2.ISoundTriggerHw.asInterface(binder);
|
||||
if (as2_2 != null) {
|
||||
mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = as2_2;
|
||||
mUnderlying_2_3 = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt 2.1
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1 =
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.asInterface(binder);
|
||||
if (as2_1 != null) {
|
||||
mUnderlying_2_0 = mUnderlying_2_1 = as2_1;
|
||||
mUnderlying_2_2 = mUnderlying_2_3 = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt 2.0
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0 =
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.asInterface(binder);
|
||||
if (as2_0 != null) {
|
||||
mUnderlying_2_0 = as2_0;
|
||||
mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = null;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Binder doesn't support ISoundTriggerHw@2.0");
|
||||
}
|
||||
|
||||
private static void handleHalStatus(int status, String methodName) {
|
||||
if (status != 0) {
|
||||
throw new HalException(status, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties() {
|
||||
try {
|
||||
AtomicInteger retval = new AtomicInteger(-1);
|
||||
AtomicReference<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties>
|
||||
properties =
|
||||
new AtomicReference<>();
|
||||
as2_0().getProperties(
|
||||
(r, p) -> {
|
||||
retval.set(r);
|
||||
properties.set(p);
|
||||
});
|
||||
handleHalStatus(retval.get(), "getProperties");
|
||||
return properties.get();
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadSoundModel(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
|
||||
Callback callback, int cookie) {
|
||||
try {
|
||||
AtomicInteger retval = new AtomicInteger(-1);
|
||||
AtomicInteger handle = new AtomicInteger(0);
|
||||
try {
|
||||
as2_1().loadSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), cookie,
|
||||
(r, h) -> {
|
||||
retval.set(r);
|
||||
handle.set(h);
|
||||
});
|
||||
} catch (NotSupported e) {
|
||||
// Fall-back to the 2.0 version:
|
||||
return loadSoundModel_2_0(soundModel, callback, cookie);
|
||||
}
|
||||
handleHalStatus(retval.get(), "loadSoundModel_2_1");
|
||||
return handle.get();
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadPhraseSoundModel(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
|
||||
Callback callback, int cookie) {
|
||||
try {
|
||||
AtomicInteger retval = new AtomicInteger(-1);
|
||||
AtomicInteger handle = new AtomicInteger(0);
|
||||
try {
|
||||
as2_1().loadPhraseSoundModel_2_1(soundModel, new SoundTriggerCallback(callback),
|
||||
cookie,
|
||||
(r, h) -> {
|
||||
retval.set(r);
|
||||
handle.set(h);
|
||||
});
|
||||
} catch (NotSupported e) {
|
||||
// Fall-back to the 2.0 version:
|
||||
return loadPhraseSoundModel_2_0(soundModel, callback, cookie);
|
||||
}
|
||||
handleHalStatus(retval.get(), "loadSoundModel_2_1");
|
||||
return handle.get();
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unloadSoundModel(int modelHandle) {
|
||||
try {
|
||||
int retval = as2_0().unloadSoundModel(modelHandle);
|
||||
handleHalStatus(retval, "unloadSoundModel");
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecognition(int modelHandle) {
|
||||
try {
|
||||
int retval = as2_0().stopRecognition(modelHandle);
|
||||
handleHalStatus(retval, "stopRecognition");
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopAllRecognitions() {
|
||||
try {
|
||||
int retval = as2_0().stopAllRecognitions();
|
||||
handleHalStatus(retval, "stopAllRecognitions");
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRecognition(int modelHandle,
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
|
||||
Callback callback, int cookie) {
|
||||
try {
|
||||
try {
|
||||
int retval = as2_1().startRecognition_2_1(modelHandle, config,
|
||||
new SoundTriggerCallback(callback), cookie);
|
||||
handleHalStatus(retval, "startRecognition_2_1");
|
||||
} catch (NotSupported e) {
|
||||
// Fall-back to the 2.0 version:
|
||||
startRecognition_2_0(modelHandle, config, callback, cookie);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getModelState(int modelHandle) {
|
||||
try {
|
||||
int retval = as2_2().getModelState(modelHandle);
|
||||
handleHalStatus(retval, "getModelState");
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
} catch (NotSupported e) {
|
||||
throw e.throwAsRecoverableException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelParameter(int modelHandle, int param) {
|
||||
AtomicInteger status = new AtomicInteger(-1);
|
||||
AtomicInteger value = new AtomicInteger(0);
|
||||
try {
|
||||
as2_3().getParameter(modelHandle, param,
|
||||
(s, v) -> {
|
||||
status.set(s);
|
||||
value.set(v);
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
} catch (NotSupported e) {
|
||||
throw e.throwAsRecoverableException();
|
||||
}
|
||||
handleHalStatus(status.get(), "getParameter");
|
||||
return value.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModelParameter(int modelHandle, int param, int value) {
|
||||
try {
|
||||
int retval = as2_3().setParameter(modelHandle, param, value);
|
||||
handleHalStatus(retval, "setParameter");
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
} catch (NotSupported e) {
|
||||
throw e.throwAsRecoverableException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.hardware.soundtrigger.V2_3.ModelParameterRange queryParameter(int modelHandle,
|
||||
int param) {
|
||||
AtomicInteger status = new AtomicInteger(-1);
|
||||
AtomicReference<android.hardware.soundtrigger.V2_3.OptionalModelParameterRange>
|
||||
optionalRange =
|
||||
new AtomicReference<>();
|
||||
try {
|
||||
as2_3().queryParameter(modelHandle, param,
|
||||
(s, r) -> {
|
||||
status.set(s);
|
||||
optionalRange.set(r);
|
||||
});
|
||||
} catch (NotSupported e) {
|
||||
// For older drivers, we consider no model parameter to be supported.
|
||||
return null;
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
handleHalStatus(status.get(), "queryParameter");
|
||||
return (optionalRange.get().getDiscriminator()
|
||||
== android.hardware.soundtrigger.V2_3.OptionalModelParameterRange.hidl_discriminator.range)
|
||||
?
|
||||
optionalRange.get().range() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
|
||||
return mBinder.linkToDeath(recipient, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
|
||||
return mBinder.unlinkToDeath(recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceDescriptor() throws RemoteException {
|
||||
return as2_0().interfaceDescriptor();
|
||||
}
|
||||
|
||||
private int loadSoundModel_2_0(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
|
||||
Callback callback, int cookie)
|
||||
throws RemoteException {
|
||||
// Convert the soundModel to V2.0.
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 =
|
||||
Hw2CompatUtil.convertSoundModel_2_1_to_2_0(soundModel);
|
||||
|
||||
AtomicInteger retval = new AtomicInteger(-1);
|
||||
AtomicInteger handle = new AtomicInteger(0);
|
||||
as2_0().loadSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, (r, h) -> {
|
||||
retval.set(r);
|
||||
handle.set(h);
|
||||
});
|
||||
handleHalStatus(retval.get(), "loadSoundModel");
|
||||
return handle.get();
|
||||
}
|
||||
|
||||
private int loadPhraseSoundModel_2_0(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
|
||||
Callback callback, int cookie)
|
||||
throws RemoteException {
|
||||
// Convert the soundModel to V2.0.
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 =
|
||||
Hw2CompatUtil.convertPhraseSoundModel_2_1_to_2_0(soundModel);
|
||||
|
||||
AtomicInteger retval = new AtomicInteger(-1);
|
||||
AtomicInteger handle = new AtomicInteger(0);
|
||||
as2_0().loadPhraseSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie,
|
||||
(r, h) -> {
|
||||
retval.set(r);
|
||||
handle.set(h);
|
||||
});
|
||||
handleHalStatus(retval.get(), "loadSoundModel");
|
||||
return handle.get();
|
||||
}
|
||||
|
||||
private void startRecognition_2_0(int modelHandle,
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
|
||||
Callback callback, int cookie)
|
||||
throws RemoteException {
|
||||
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 =
|
||||
Hw2CompatUtil.convertRecognitionConfig_2_1_to_2_0(config);
|
||||
int retval = as2_0().startRecognition(modelHandle, config_2_0,
|
||||
new SoundTriggerCallback(callback), cookie);
|
||||
handleHalStatus(retval, "startRecognition");
|
||||
}
|
||||
|
||||
private @NonNull
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0() {
|
||||
return mUnderlying_2_0;
|
||||
}
|
||||
|
||||
private @NonNull
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1() throws NotSupported {
|
||||
if (mUnderlying_2_1 == null) {
|
||||
throw new NotSupported("Underlying driver version < 2.1");
|
||||
}
|
||||
return mUnderlying_2_1;
|
||||
}
|
||||
|
||||
private @NonNull
|
||||
android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2() throws NotSupported {
|
||||
if (mUnderlying_2_2 == null) {
|
||||
throw new NotSupported("Underlying driver version < 2.2");
|
||||
}
|
||||
return mUnderlying_2_2;
|
||||
}
|
||||
|
||||
private @NonNull
|
||||
android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3() throws NotSupported {
|
||||
if (mUnderlying_2_3 == null) {
|
||||
throw new NotSupported("Underlying driver version < 2.3");
|
||||
}
|
||||
return mUnderlying_2_3;
|
||||
}
|
||||
|
||||
/**
|
||||
* A checked exception representing the requested interface version not being supported.
|
||||
* At the public interface layer, use {@link #throwAsRecoverableException()} to propagate it to
|
||||
* the caller if the request cannot be fulfilled.
|
||||
*/
|
||||
private static class NotSupported extends Exception {
|
||||
NotSupported(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw this as a recoverable exception.
|
||||
*
|
||||
* @return Never actually returns anything. Always throws. Used so that caller can write
|
||||
* throw e.throwAsRecoverableException().
|
||||
*/
|
||||
RecoverableException throwAsRecoverableException() {
|
||||
throw new RecoverableException(Status.OPERATION_NOT_SUPPORTED, getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SoundTriggerCallback extends
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.Stub {
|
||||
private final @NonNull
|
||||
Callback mDelegate;
|
||||
|
||||
private SoundTriggerCallback(
|
||||
@NonNull Callback delegate) {
|
||||
mDelegate = Objects.requireNonNull(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recognitionCallback_2_1(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event,
|
||||
int cookie) {
|
||||
mDelegate.recognitionCallback(event, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void phraseRecognitionCallback_2_1(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
|
||||
int cookie) {
|
||||
mDelegate.phraseRecognitionCallback(event, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void soundModelCallback_2_1(
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent event,
|
||||
int cookie) {
|
||||
// Nobody cares.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recognitionCallback(
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event,
|
||||
int cookie) {
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 =
|
||||
Hw2CompatUtil.convertRecognitionEvent_2_0_to_2_1(event);
|
||||
mDelegate.recognitionCallback(event_2_1, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void phraseRecognitionCallback(
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
|
||||
int cookie) {
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent
|
||||
event_2_1 = Hw2CompatUtil.convertPhraseRecognitionEvent_2_0_to_2_1(event);
|
||||
mDelegate.phraseRecognitionCallback(event_2_1, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void soundModelCallback(
|
||||
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent event,
|
||||
int cookie) {
|
||||
// Nobody cares.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerModule;
|
||||
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is an implementation of the ISoundTriggerMiddlewareService interface.
|
||||
* <p>
|
||||
* <b>Important conventions:</b>
|
||||
* <ul>
|
||||
* <li>Correct usage is assumed. This implementation does not attempt to gracefully handle invalid
|
||||
* usage, and such usage will result in undefined behavior. If this service is to be offered to an
|
||||
* untrusted client, it must be wrapped with input and state validation.
|
||||
* <li>There is no binder instance associated with this implementation. Do not call asBinder().
|
||||
* <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal,
|
||||
* recoverable faults. The error code would one of the
|
||||
* {@link android.media.soundtrigger_middleware.Status}
|
||||
* constants. Any other exception thrown should be regarded as a bug in the implementation or one
|
||||
* of its dependencies (assuming correct usage).
|
||||
* <li>The implementation is designed for testibility by featuring dependency injection (the
|
||||
* underlying HAL driver instances are passed to the ctor) and by minimizing dependencies on
|
||||
* Android runtime.
|
||||
* <li>The implementation is thread-safe.
|
||||
* </ul>
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class SoundTriggerMiddlewareImpl implements ISoundTriggerMiddlewareService {
|
||||
static private final String TAG = "SoundTriggerMiddlewareImpl";
|
||||
private final SoundTriggerModule[] mModules;
|
||||
|
||||
/**
|
||||
* Interface to the audio system, which can allocate capture session handles.
|
||||
* SoundTrigger uses those sessions in order to associate a recognition session with an optional
|
||||
* capture from the same device that triggered the recognition.
|
||||
*/
|
||||
public static abstract class AudioSessionProvider {
|
||||
public static final class AudioSession {
|
||||
final int mSessionHandle;
|
||||
final int mIoHandle;
|
||||
final int mDeviceHandle;
|
||||
|
||||
AudioSession(int sessionHandle, int ioHandle, int deviceHandle) {
|
||||
mSessionHandle = sessionHandle;
|
||||
mIoHandle = ioHandle;
|
||||
mDeviceHandle = deviceHandle;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract AudioSession acquireSession();
|
||||
|
||||
public abstract void releaseSession(int sessionHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Most generic constructor - gets an array of HAL driver instances.
|
||||
*/
|
||||
public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw[] halServices,
|
||||
@NonNull AudioSessionProvider audioSessionProvider) {
|
||||
List<SoundTriggerModule> modules = new ArrayList<>(halServices.length);
|
||||
|
||||
for (int i = 0; i < halServices.length; ++i) {
|
||||
ISoundTriggerHw service = halServices[i];
|
||||
try {
|
||||
modules.add(new SoundTriggerModule(service, audioSessionProvider));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to a SoundTriggerModule instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
mModules = modules.toArray(new SoundTriggerModule[modules.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor - gets a single HAL driver instance.
|
||||
*/
|
||||
public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw halService,
|
||||
@NonNull AudioSessionProvider audioSessionProvider) {
|
||||
this(new ISoundTriggerHw[]{halService}, audioSessionProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
SoundTriggerModuleDescriptor[] listModules() {
|
||||
SoundTriggerModuleDescriptor[] result = new SoundTriggerModuleDescriptor[mModules.length];
|
||||
|
||||
for (int i = 0; i < mModules.length; ++i) {
|
||||
SoundTriggerModuleDescriptor desc = new SoundTriggerModuleDescriptor();
|
||||
desc.handle = i;
|
||||
desc.properties = mModules[i].getProperties();
|
||||
result[i] = desc;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) {
|
||||
return mModules[handle].attach(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExternalCaptureState(boolean active) {
|
||||
for (SoundTriggerModule module : mModules) {
|
||||
module.setExternalCaptureState(active);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
IBinder asBinder() {
|
||||
throw new UnsupportedOperationException(
|
||||
"This implementation is not inteded to be used directly with Binder.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerModule;
|
||||
import android.media.soundtrigger_middleware.ModelParameterRange;
|
||||
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
|
||||
import android.media.soundtrigger_middleware.PhraseSoundModel;
|
||||
import android.media.soundtrigger_middleware.RecognitionConfig;
|
||||
import android.media.soundtrigger_middleware.RecognitionEvent;
|
||||
import android.media.soundtrigger_middleware.RecognitionStatus;
|
||||
import android.media.soundtrigger_middleware.SoundModel;
|
||||
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceSpecificException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.server.SystemService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes
|
||||
* it as a Binder service and enforces permissions and correct usage by the client, as well as makes
|
||||
* sure that exceptions representing a server malfunction do not get sent to the client.
|
||||
* <p>
|
||||
* This is intended to extract the non-business logic out of the underlying implementation and thus
|
||||
* make it easier to maintain each one of those separate aspects. A design trade-off is being made
|
||||
* here, in that this class would need to essentially eavesdrop on all the client-server
|
||||
* communication and retain all state known to the client, while the client doesn't necessarily care
|
||||
* about all of it, and while the server has its own representation of this information. However,
|
||||
* in this case, this is a small amount of data, and the benefits in code elegance seem worth it.
|
||||
* There is also some additional cost in employing a simplistic locking mechanism here, but
|
||||
* following the same line of reasoning, the benefits in code simplicity outweigh it.
|
||||
* <p>
|
||||
* Every public method in this class, overriding an interface method, must follow the following
|
||||
* pattern:
|
||||
* <code><pre>
|
||||
* @Override public T method(S arg) {
|
||||
* // Permission check.
|
||||
* checkPermissions();
|
||||
* // Input validation.
|
||||
* ValidationUtil.validateS(arg);
|
||||
* synchronized (this) {
|
||||
* // State validation.
|
||||
* if (...state is not valid for this call...) {
|
||||
* throw new IllegalStateException("State is invalid because...");
|
||||
* }
|
||||
* // From here on, every exception isn't client's fault.
|
||||
* try {
|
||||
* T result = mDelegate.method(arg);
|
||||
* // Update state.;
|
||||
* ...
|
||||
* return result;
|
||||
* } catch (Exception e) {
|
||||
* throw handleException(e);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre></code>
|
||||
* Following this patterns ensures a consistent and rigorous handling of all aspects associated
|
||||
* with client-server separation.
|
||||
* <p>
|
||||
* <b>Exception handling approach:</b><br>
|
||||
* We make sure all client faults (permissions, argument and state validation) happen first, and
|
||||
* would throw {@link SecurityException}, {@link IllegalArgumentException}/
|
||||
* {@link NullPointerException} or {@link
|
||||
* IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
|
||||
* will get sent back to the client.<br>
|
||||
* Once this is done, any subsequent fault is considered a server fault. Only {@link
|
||||
* RecoverableException}s thrown by the implementation are special-cased: they would get sent back
|
||||
* to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other
|
||||
* exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type
|
||||
* that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level
|
||||
* exception handler on the server side, typically resulting in rebooting the server.
|
||||
* <p>
|
||||
* <b>Exposing this service as a System Service:</b><br>
|
||||
* Insert this line into {@link com.android.server.SystemServer}:
|
||||
* <code><pre>
|
||||
* mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class);
|
||||
* </pre></code>
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub {
|
||||
static private final String TAG = "SoundTriggerMiddlewareService";
|
||||
|
||||
final ISoundTriggerMiddlewareService mDelegate;
|
||||
final Context mContext;
|
||||
Set<Integer> mModuleHandles;
|
||||
|
||||
/**
|
||||
* Constructor for internal use only. Could be exposed for testing purposes in the future.
|
||||
* Users should access this class via {@link Lifecycle}.
|
||||
*/
|
||||
private SoundTriggerMiddlewareService(
|
||||
@NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) {
|
||||
mDelegate = delegate;
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic exception handling for exceptions thrown by the underlying implementation.
|
||||
*
|
||||
* Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed
|
||||
* by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError}
|
||||
* (<b>not</b> passed by Binder to the caller).
|
||||
* <p>
|
||||
* Typical usage:
|
||||
* <code><pre>
|
||||
* try {
|
||||
* ... Do server operations ...
|
||||
* } catch (Exception e) {
|
||||
* throw handleException(e);
|
||||
* }
|
||||
* </pre></code>
|
||||
*/
|
||||
private static @NonNull
|
||||
RuntimeException handleException(@NonNull Exception e) {
|
||||
if (e instanceof RecoverableException) {
|
||||
throw new ServiceSpecificException(((RecoverableException) e).errorCode,
|
||||
e.getMessage());
|
||||
}
|
||||
throw new InternalServerError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
SoundTriggerModuleDescriptor[] listModules() {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation (always valid).
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
|
||||
mModuleHandles = new HashSet<>(result.length);
|
||||
for (SoundTriggerModuleDescriptor desc : result) {
|
||||
mModuleHandles.add(desc.handle);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
Preconditions.checkNotNull(callback);
|
||||
Preconditions.checkNotNull(callback.asBinder());
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mModuleHandles == null) {
|
||||
throw new IllegalStateException(
|
||||
"Client must call listModules() prior to attaching.");
|
||||
}
|
||||
if (!mModuleHandles.contains(handle)) {
|
||||
throw new IllegalArgumentException("Invalid handle: " + handle);
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
ModuleService moduleService = new ModuleService(callback);
|
||||
moduleService.attach(mDelegate.attach(handle, moduleService));
|
||||
return moduleService;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExternalCaptureState(boolean active) {
|
||||
// Permission check.
|
||||
checkPreemptPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation (always valid).
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.setExternalCaptureState(active);
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a {@link SecurityException} if caller doesn't have the right permissions to use this
|
||||
* service.
|
||||
*/
|
||||
private void checkPermissions() {
|
||||
mContext.enforceCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO,
|
||||
"Caller must have the android.permission.RECORD_AUDIO permission.");
|
||||
mContext.enforceCallingOrSelfPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD,
|
||||
"Caller must have the android.permission.CAPTURE_AUDIO_HOTWORD permission.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a {@link SecurityException} if caller doesn't have the right permissions to preempt
|
||||
* active sound trigger sessions.
|
||||
*/
|
||||
private void checkPreemptPermissions() {
|
||||
mContext.enforceCallingOrSelfPermission(Manifest.permission.PREEMPT_SOUND_TRIGGER,
|
||||
"Caller must have the android.permission.PREEMPT_SOUND_TRIGGER permission.");
|
||||
}
|
||||
|
||||
/** State of a sound model. */
|
||||
static class ModelState {
|
||||
/** Activity state of a sound model. */
|
||||
enum Activity {
|
||||
/** Model is loaded, recognition is inactive. */
|
||||
LOADED,
|
||||
/** Model is loaded, recognition is active. */
|
||||
ACTIVE
|
||||
}
|
||||
|
||||
/** Activity state. */
|
||||
public Activity activityState = Activity.LOADED;
|
||||
|
||||
/**
|
||||
* A map of known parameter support. A missing key means we don't know yet whether the
|
||||
* parameter is supported. A null value means it is known to not be supported. A non-null
|
||||
* value indicates the valid value range.
|
||||
*/
|
||||
private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Check that the given parameter is known to be supported for this model.
|
||||
*
|
||||
* @param modelParam The parameter key.
|
||||
*/
|
||||
public void checkSupported(int modelParam) {
|
||||
if (!parameterSupport.containsKey(modelParam)) {
|
||||
throw new IllegalStateException("Parameter has not been checked for support.");
|
||||
}
|
||||
ModelParameterRange range = parameterSupport.get(modelParam);
|
||||
if (range == null) {
|
||||
throw new IllegalArgumentException("Paramater is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the given parameter is known to be supported for this model and that the given
|
||||
* value is a valid value for it.
|
||||
*
|
||||
* @param modelParam The parameter key.
|
||||
* @param value The value.
|
||||
*/
|
||||
public void checkSupported(int modelParam, int value) {
|
||||
if (!parameterSupport.containsKey(modelParam)) {
|
||||
throw new IllegalStateException("Parameter has not been checked for support.");
|
||||
}
|
||||
ModelParameterRange range = parameterSupport.get(modelParam);
|
||||
if (range == null) {
|
||||
throw new IllegalArgumentException("Paramater is not supported.");
|
||||
}
|
||||
Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive,
|
||||
"value");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update support state for the given parameter for this model.
|
||||
*
|
||||
* @param modelParam The parameter key.
|
||||
* @param range The parameter value range, or null if not supported.
|
||||
*/
|
||||
public void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) {
|
||||
parameterSupport.put(modelParam, range);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry-point to this module: exposes the module as a {@link SystemService}.
|
||||
*/
|
||||
public static final class Lifecycle extends SystemService {
|
||||
private SoundTriggerMiddlewareService mService;
|
||||
|
||||
public Lifecycle(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
ISoundTriggerHw[] services;
|
||||
try {
|
||||
services = new ISoundTriggerHw[]{ISoundTriggerHw.getService(true)};
|
||||
Log.d(TAG, "Connected to default ISoundTriggerHw");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to connect to default ISoundTriggerHw", e);
|
||||
services = new ISoundTriggerHw[0];
|
||||
}
|
||||
|
||||
mService = new SoundTriggerMiddlewareService(
|
||||
new SoundTriggerMiddlewareImpl(services, new AudioSessionProviderImpl()),
|
||||
getContext());
|
||||
publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, mService);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
|
||||
* mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
|
||||
*/
|
||||
private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback,
|
||||
DeathRecipient {
|
||||
private final ISoundTriggerCallback mCallback;
|
||||
private ISoundTriggerModule mDelegate;
|
||||
private Map<Integer, ModelState> mLoadedModels = new HashMap<>();
|
||||
|
||||
ModuleService(@NonNull ISoundTriggerCallback callback) {
|
||||
mCallback = callback;
|
||||
try {
|
||||
mCallback.asBinder().linkToDeath(this, 0);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
void attach(@NonNull ISoundTriggerModule delegate) {
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadModel(@NonNull SoundModel model) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validateGenericModel(model);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
int handle = mDelegate.loadModel(model);
|
||||
mLoadedModels.put(handle, new ModelState());
|
||||
return handle;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validatePhraseModel(model);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
int handle = mDelegate.loadPhraseModel(model);
|
||||
mLoadedModels.put(handle, new ModelState());
|
||||
return handle;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unloadModel(int modelHandle) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
if (modelState.activityState != ModelState.Activity.LOADED) {
|
||||
throw new IllegalStateException("Model with handle: " + modelHandle
|
||||
+ " has invalid state for unloading: " + modelState.activityState);
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.unloadModel(modelHandle);
|
||||
mLoadedModels.remove(modelHandle);
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validateRecognitionConfig(config);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
if (modelState.activityState != ModelState.Activity.LOADED) {
|
||||
throw new IllegalStateException("Model with handle: " + modelHandle
|
||||
+ " has invalid state for starting recognition: "
|
||||
+ modelState.activityState);
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.startRecognition(modelHandle, config);
|
||||
modelState.activityState = ModelState.Activity.ACTIVE;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecognition(int modelHandle) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
// stopRecognition is idempotent - no need to check model state.
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.stopRecognition(modelHandle);
|
||||
modelState.activityState = ModelState.Activity.LOADED;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceRecognitionEvent(int modelHandle) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
// forceRecognitionEvent is idempotent - no need to check model state.
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.forceRecognitionEvent(modelHandle);
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModelParameter(int modelHandle, int modelParam, int value) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validateModelParameter(modelParam);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
modelState.checkSupported(modelParam, value);
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
mDelegate.setModelParameter(modelHandle, modelParam, value);
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelParameter(int modelHandle, int modelParam) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validateModelParameter(modelParam);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
modelState.checkSupported(modelParam);
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
return mDelegate.getModelParameter(modelHandle, modelParam);
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation.
|
||||
ValidationUtil.validateModelParameter(modelParam);
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has been detached.");
|
||||
}
|
||||
ModelState modelState = mLoadedModels.get(modelHandle);
|
||||
if (modelState == null) {
|
||||
throw new IllegalStateException("Invalid handle: " + modelHandle);
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
|
||||
modelParam);
|
||||
modelState.updateParameterSupport(modelParam, result);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
// Permission check.
|
||||
checkPermissions();
|
||||
// Input validation (always valid).
|
||||
|
||||
synchronized (this) {
|
||||
// State validation.
|
||||
if (mDelegate == null) {
|
||||
throw new IllegalStateException("Module has already been detached.");
|
||||
}
|
||||
if (!mLoadedModels.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot detach while models are loaded.");
|
||||
}
|
||||
|
||||
// From here on, every exception isn't client's fault.
|
||||
try {
|
||||
detachInternal();
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void detachInternal() {
|
||||
try {
|
||||
mDelegate.detach();
|
||||
mDelegate = null;
|
||||
mCallback.asBinder().unlinkToDeath(this, 0);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Callbacks
|
||||
|
||||
@Override
|
||||
public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
|
||||
synchronized (this) {
|
||||
if (event.status != RecognitionStatus.FORCED) {
|
||||
mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
|
||||
}
|
||||
try {
|
||||
mCallback.onRecognition(modelHandle, event);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) {
|
||||
synchronized (this) {
|
||||
if (event.common.status != RecognitionStatus.FORCED) {
|
||||
mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
|
||||
}
|
||||
try {
|
||||
mCallback.onPhraseRecognition(modelHandle, event);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
|
||||
synchronized (this) {
|
||||
try {
|
||||
mCallback.onRecognitionAvailabilityChange(available);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
// This is called whenever our client process dies.
|
||||
synchronized (this) {
|
||||
try {
|
||||
// Gracefully stop all active recognitions and unload the models.
|
||||
for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
|
||||
if (entry.getValue().activityState == ModelState.Activity.ACTIVE) {
|
||||
mDelegate.stopRecognition(entry.getKey());
|
||||
}
|
||||
mDelegate.unloadModel(entry.getKey());
|
||||
}
|
||||
// Detach.
|
||||
detachInternal();
|
||||
} catch (Exception e) {
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.soundtrigger_middleware;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback;
|
||||
import android.hardware.soundtrigger.V2_2.ISoundTriggerHw;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
|
||||
import android.media.soundtrigger_middleware.ISoundTriggerModule;
|
||||
import android.media.soundtrigger_middleware.ModelParameterRange;
|
||||
import android.media.soundtrigger_middleware.PhraseSoundModel;
|
||||
import android.media.soundtrigger_middleware.RecognitionConfig;
|
||||
import android.media.soundtrigger_middleware.SoundModel;
|
||||
import android.media.soundtrigger_middleware.SoundModelType;
|
||||
import android.media.soundtrigger_middleware.SoundTriggerModuleProperties;
|
||||
import android.media.soundtrigger_middleware.Status;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This is an implementation of a single module of the ISoundTriggerMiddlewareService interface,
|
||||
* exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate
|
||||
* clients.
|
||||
* <p>
|
||||
* Typical usage is to query the module capabilities using {@link #getProperties()} and then to use
|
||||
* the module through an {@link ISoundTriggerModule} instance, obtained via {@link
|
||||
* #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared
|
||||
* between sessions (i.e. cannot use a handle obtained from one session through another).
|
||||
* <p>
|
||||
* <b>Important conventions:</b>
|
||||
* <ul>
|
||||
* <li>Correct usage is assumed. This implementation does not attempt to gracefully handle
|
||||
* invalid usage, and such usage will result in undefined behavior. If this service is to be
|
||||
* offered to an untrusted client, it must be wrapped with input and state validation.
|
||||
* <li>The underlying driver is assumed to be correct. This implementation does not attempt to
|
||||
* gracefully handle driver malfunction and such behavior will result in undefined behavior. If this
|
||||
* service is to used with an untrusted driver, the driver must be wrapped with validation / error
|
||||
* recovery code.
|
||||
* <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not
|
||||
* considered recoverable faults and should not occur in a properly functioning system.
|
||||
* <li>There is no binder instance associated with this implementation. Do not call asBinder().
|
||||
* <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal,
|
||||
* recoverable faults. The error code would one of the
|
||||
* {@link android.media.soundtrigger_middleware.Status} constants. Any other exception
|
||||
* thrown should be regarded as a bug in the implementation or one of its dependencies
|
||||
* (assuming correct usage).
|
||||
* <li>The implementation is designed for testibility by featuring dependency injection (the
|
||||
* underlying HAL driver instances are passed to the ctor) and by minimizing dependencies
|
||||
* on Android runtime.
|
||||
* <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry-
|
||||
* points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance
|
||||
* for their entire scope. Any other method can be assumed to be running with the lock already
|
||||
* obtained, so no further locking should be done. While this is not necessarily the most efficient
|
||||
* synchronization strategy, it is very easy to reason about and this code is likely not on any
|
||||
* performance-critical
|
||||
* path.
|
||||
* </ul>
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
class SoundTriggerModule {
|
||||
static private final String TAG = "SoundTriggerModule";
|
||||
@NonNull private final ISoundTriggerHw2 mHalService;
|
||||
@NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider;
|
||||
private final Set<Session> mActiveSessions = new HashSet<>();
|
||||
private int mNumLoadedModels = 0;
|
||||
private SoundTriggerModuleProperties mProperties = null;
|
||||
private boolean mRecognitionAvailable;
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
*
|
||||
* @param halService The underlying HAL driver.
|
||||
*/
|
||||
SoundTriggerModule(@NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw halService,
|
||||
@NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) {
|
||||
assert halService != null;
|
||||
mHalService = new SoundTriggerHw2Compat(halService);
|
||||
mAudioSessionProvider = audioSessionProvider;
|
||||
mProperties = ConversionUtil.hidl2aidlProperties(mHalService.getProperties());
|
||||
|
||||
// We conservatively assume that external capture is active until explicitly told otherwise.
|
||||
mRecognitionAvailable = mProperties.concurrentCapture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a client session with this module.
|
||||
*
|
||||
* This module may be shared by multiple clients, each will get its own session. While resources
|
||||
* are shared between the clients, each session has its own state and data should not be shared
|
||||
* across sessions.
|
||||
*
|
||||
* @param callback The client callback, which will be used for all messages. This is a oneway
|
||||
* callback, so will never block, throw an unchecked exception or return a
|
||||
* value.
|
||||
* @return The interface through which this module can be controlled.
|
||||
*/
|
||||
synchronized @NonNull
|
||||
Session attach(@NonNull ISoundTriggerCallback callback) {
|
||||
Log.d(TAG, "attach()");
|
||||
Session session = new Session(callback);
|
||||
mActiveSessions.add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the module's properties.
|
||||
*
|
||||
* @return The properties structure.
|
||||
*/
|
||||
synchronized @NonNull
|
||||
SoundTriggerModuleProperties getProperties() {
|
||||
return mProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the module that external capture has started / finished, using the same input device
|
||||
* used for recognition.
|
||||
* If the underlying driver does not support recognition while capturing, capture will be
|
||||
* aborted, and the recognition callback will receive and abort event. In addition, all active
|
||||
* clients will be notified of the change in state.
|
||||
*
|
||||
* @param active true iff external capture is active.
|
||||
*/
|
||||
synchronized void setExternalCaptureState(boolean active) {
|
||||
Log.d(TAG, String.format("setExternalCaptureState(active=%b)", active));
|
||||
if (mProperties.concurrentCapture) {
|
||||
// If we support concurrent capture, we don't care about any of this.
|
||||
return;
|
||||
}
|
||||
mRecognitionAvailable = !active;
|
||||
if (!mRecognitionAvailable) {
|
||||
// Our module does not support recognition while a capture is active -
|
||||
// need to abort all active recognitions.
|
||||
for (Session session : mActiveSessions) {
|
||||
session.abortActiveRecognitions();
|
||||
}
|
||||
}
|
||||
for (Session session : mActiveSessions) {
|
||||
session.notifyRecognitionAvailability();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove session from the list of active sessions.
|
||||
*
|
||||
* @param session The session to remove.
|
||||
*/
|
||||
private void removeSession(@NonNull Session session) {
|
||||
mActiveSessions.remove(session);
|
||||
}
|
||||
|
||||
/** State of a single sound model. */
|
||||
private enum ModelState {
|
||||
/** Initial state, until load() is called. */
|
||||
INIT,
|
||||
/** Model is loaded, but recognition is not active. */
|
||||
LOADED,
|
||||
/** Model is loaded and recognition is active. */
|
||||
ACTIVE
|
||||
}
|
||||
|
||||
/**
|
||||
* A single client session with this module.
|
||||
*
|
||||
* This is the main interface used to interact with this module.
|
||||
*/
|
||||
private class Session implements ISoundTriggerModule {
|
||||
private ISoundTriggerCallback mCallback;
|
||||
private Map<Integer, Model> mLoadedModels = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
*
|
||||
* @param callback The client callback interface.
|
||||
*/
|
||||
private Session(@NonNull ISoundTriggerCallback callback) {
|
||||
mCallback = callback;
|
||||
notifyRecognitionAvailability();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
Log.d(TAG, "detach()");
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
removeSession(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadModel(@NonNull SoundModel model) {
|
||||
Log.d(TAG, String.format("loadModel(model=%s)", model));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
if (mNumLoadedModels == mProperties.maxSoundModels) {
|
||||
throw new RecoverableException(Status.RESOURCE_CONTENTION,
|
||||
"Maximum number of models loaded.");
|
||||
}
|
||||
Model loadedModel = new Model();
|
||||
int result = loadedModel.load(model);
|
||||
++mNumLoadedModels;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int loadPhraseModel(@NonNull PhraseSoundModel model) {
|
||||
Log.d(TAG, String.format("loadPhraseModel(model=%s)", model));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
if (mNumLoadedModels == mProperties.maxSoundModels) {
|
||||
throw new RecoverableException(Status.RESOURCE_CONTENTION,
|
||||
"Maximum number of models loaded.");
|
||||
}
|
||||
Model loadedModel = new Model();
|
||||
int result = loadedModel.load(model);
|
||||
++mNumLoadedModels;
|
||||
Log.d(TAG, String.format("loadPhraseModel()->%d", result));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unloadModel(int modelHandle) {
|
||||
Log.d(TAG, String.format("unloadModel(handle=%d)", modelHandle));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
mLoadedModels.get(modelHandle).unload();
|
||||
--mNumLoadedModels;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
|
||||
Log.d(TAG,
|
||||
String.format("startRecognition(handle=%d, config=%s)", modelHandle, config));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
mLoadedModels.get(modelHandle).startRecognition(config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecognition(int modelHandle) {
|
||||
Log.d(TAG, String.format("stopRecognition(handle=%d)", modelHandle));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
mLoadedModels.get(modelHandle).stopRecognition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceRecognitionEvent(int modelHandle) {
|
||||
Log.d(TAG, String.format("forceRecognitionEvent(handle=%d)", modelHandle));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
mLoadedModels.get(modelHandle).forceRecognitionEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModelParameter(int modelHandle, int modelParam, int value)
|
||||
throws RemoteException {
|
||||
Log.d(TAG,
|
||||
String.format("setModelParameter(handle=%d, param=%d, value=%d)", modelHandle,
|
||||
modelParam, value));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
mLoadedModels.get(modelHandle).setParameter(modelParam, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
|
||||
Log.d(TAG, String.format("getModelParameter(handle=%d, param=%d)", modelHandle,
|
||||
modelParam));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
return mLoadedModels.get(modelHandle).getParameter(modelParam);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
|
||||
Log.d(TAG, String.format("queryModelParameterSupport(handle=%d, param=%d)", modelHandle,
|
||||
modelParam));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort all currently active recognitions.
|
||||
*/
|
||||
private void abortActiveRecognitions() {
|
||||
for (Model model : mLoadedModels.values()) {
|
||||
model.abortActiveRecognition();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyRecognitionAvailability() {
|
||||
try {
|
||||
mCallback.onRecognitionAvailabilityChange(mRecognitionAvailable);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
IBinder asBinder() {
|
||||
throw new UnsupportedOperationException(
|
||||
"This implementation is not intended to be used directly with Binder.");
|
||||
}
|
||||
|
||||
/**
|
||||
* A single sound model in the system.
|
||||
*
|
||||
* All model-based operations are delegated to this class and implemented here.
|
||||
*/
|
||||
private class Model implements ISoundTriggerHw2.Callback {
|
||||
public int mHandle;
|
||||
private ModelState mState = ModelState.INIT;
|
||||
private int mModelType = SoundModelType.UNKNOWN;
|
||||
private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession;
|
||||
|
||||
private @NonNull
|
||||
ModelState getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
private void setState(@NonNull ModelState state) {
|
||||
mState = state;
|
||||
SoundTriggerModule.this.notifyAll();
|
||||
}
|
||||
|
||||
private void waitStateChange() throws InterruptedException {
|
||||
SoundTriggerModule.this.wait();
|
||||
}
|
||||
|
||||
private int load(@NonNull SoundModel model) {
|
||||
mModelType = model.type;
|
||||
ISoundTriggerHw.SoundModel hidlModel = ConversionUtil.aidl2hidlSoundModel(model);
|
||||
|
||||
mSession = mAudioSessionProvider.acquireSession();
|
||||
try {
|
||||
mHandle = mHalService.loadSoundModel(hidlModel, this, 0);
|
||||
} catch (Exception e) {
|
||||
mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
|
||||
throw e;
|
||||
}
|
||||
|
||||
setState(ModelState.LOADED);
|
||||
mLoadedModels.put(mHandle, this);
|
||||
return mHandle;
|
||||
}
|
||||
|
||||
private int load(@NonNull PhraseSoundModel model) {
|
||||
mModelType = model.common.type;
|
||||
ISoundTriggerHw.PhraseSoundModel hidlModel =
|
||||
ConversionUtil.aidl2hidlPhraseSoundModel(model);
|
||||
|
||||
mSession = mAudioSessionProvider.acquireSession();
|
||||
try {
|
||||
mHandle = mHalService.loadPhraseSoundModel(hidlModel, this, 0);
|
||||
} catch (Exception e) {
|
||||
mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
|
||||
throw e;
|
||||
}
|
||||
|
||||
setState(ModelState.LOADED);
|
||||
mLoadedModels.put(mHandle, this);
|
||||
return mHandle;
|
||||
}
|
||||
|
||||
private void unload() {
|
||||
mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
|
||||
mHalService.unloadSoundModel(mHandle);
|
||||
mLoadedModels.remove(mHandle);
|
||||
}
|
||||
|
||||
private void startRecognition(@NonNull RecognitionConfig config) {
|
||||
if (!mRecognitionAvailable) {
|
||||
// Recognition is unavailable - send an abort event immediately.
|
||||
notifyAbort();
|
||||
return;
|
||||
}
|
||||
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig hidlConfig =
|
||||
ConversionUtil.aidl2hidlRecognitionConfig(config);
|
||||
hidlConfig.header.captureDevice = mSession.mDeviceHandle;
|
||||
hidlConfig.header.captureHandle = mSession.mIoHandle;
|
||||
mHalService.startRecognition(mHandle, hidlConfig, this, 0);
|
||||
setState(ModelState.ACTIVE);
|
||||
}
|
||||
|
||||
private void stopRecognition() {
|
||||
if (getState() == ModelState.LOADED) {
|
||||
// This call is idempotent in order to avoid races.
|
||||
return;
|
||||
}
|
||||
mHalService.stopRecognition(mHandle);
|
||||
setState(ModelState.LOADED);
|
||||
}
|
||||
|
||||
/** Request a forced recognition event. Will do nothing if recognition is inactive. */
|
||||
private void forceRecognitionEvent() {
|
||||
if (getState() != ModelState.ACTIVE) {
|
||||
// This call is idempotent in order to avoid races.
|
||||
return;
|
||||
}
|
||||
mHalService.getModelState(mHandle);
|
||||
}
|
||||
|
||||
|
||||
private void setParameter(int modelParam, int value) {
|
||||
mHalService.setModelParameter(mHandle,
|
||||
ConversionUtil.aidl2hidlModelParameter(modelParam), value);
|
||||
}
|
||||
|
||||
private int getParameter(int modelParam) {
|
||||
return mHalService.getModelParameter(mHandle,
|
||||
ConversionUtil.aidl2hidlModelParameter(modelParam));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ModelParameterRange queryModelParameterSupport(int modelParam) {
|
||||
return ConversionUtil.hidl2aidlModelParameterRange(
|
||||
mHalService.queryParameter(mHandle,
|
||||
ConversionUtil.aidl2hidlModelParameter(modelParam)));
|
||||
}
|
||||
|
||||
/** Abort the recognition, if active. */
|
||||
private void abortActiveRecognition() {
|
||||
// If we're inactive, do nothing.
|
||||
if (getState() != ModelState.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
// Stop recognition.
|
||||
stopRecognition();
|
||||
|
||||
// Notify the client that recognition has been aborted.
|
||||
notifyAbort();
|
||||
}
|
||||
|
||||
/** Notify the client that recognition has been aborted. */
|
||||
private void notifyAbort() {
|
||||
try {
|
||||
switch (mModelType) {
|
||||
case SoundModelType.GENERIC: {
|
||||
android.media.soundtrigger_middleware.RecognitionEvent event =
|
||||
new android.media.soundtrigger_middleware.RecognitionEvent();
|
||||
event.status =
|
||||
android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
|
||||
mCallback.onRecognition(mHandle, event);
|
||||
}
|
||||
break;
|
||||
|
||||
case SoundModelType.KEYPHRASE: {
|
||||
android.media.soundtrigger_middleware.PhraseRecognitionEvent event =
|
||||
new android.media.soundtrigger_middleware.PhraseRecognitionEvent();
|
||||
event.common =
|
||||
new android.media.soundtrigger_middleware.RecognitionEvent();
|
||||
event.common.status =
|
||||
android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
|
||||
mCallback.onPhraseRecognition(mHandle, event);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Unknown model type: " + mModelType);
|
||||
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recognitionCallback(
|
||||
@NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent,
|
||||
int cookie) {
|
||||
Log.d(TAG, String.format("recognitionCallback_2_1(event=%s, cookie=%d)",
|
||||
recognitionEvent, cookie));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
android.media.soundtrigger_middleware.RecognitionEvent aidlEvent =
|
||||
ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent);
|
||||
aidlEvent.captureSession = mSession.mSessionHandle;
|
||||
try {
|
||||
mCallback.onRecognition(mHandle, aidlEvent);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
if (aidlEvent.status
|
||||
!= android.media.soundtrigger_middleware.RecognitionStatus.FORCED) {
|
||||
setState(ModelState.LOADED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void phraseRecognitionCallback(
|
||||
@NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent,
|
||||
int cookie) {
|
||||
Log.d(TAG, String.format("phraseRecognitionCallback_2_1(event=%s, cookie=%d)",
|
||||
phraseRecognitionEvent, cookie));
|
||||
synchronized (SoundTriggerModule.this) {
|
||||
android.media.soundtrigger_middleware.PhraseRecognitionEvent aidlEvent =
|
||||
ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent);
|
||||
aidlEvent.common.captureSession = mSession.mSessionHandle;
|
||||
try {
|
||||
mCallback.onPhraseRecognition(mHandle, aidlEvent);
|
||||
} catch (RemoteException e) {
|
||||
// Dead client will be handled by binderDied() - no need to handle here.
|
||||
// In any case, client callbacks are considered best effort.
|
||||
Log.e(TAG, "Client callback execption.", e);
|
||||
}
|
||||
if (aidlEvent.common.status
|
||||
!= android.media.soundtrigger_middleware.RecognitionStatus.FORCED) {
|
||||
setState(ModelState.LOADED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "FrameworksServicesTests",
|
||||
"options": [
|
||||
{
|
||||
"include-filter": "com.android.server.soundtrigger_middleware"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "core_jni_helpers.h"
|
||||
#include <media/AudioSystem.h>
|
||||
|
||||
namespace android {
|
||||
|
||||
namespace {
|
||||
|
||||
#define PACKAGE "com/android/server/soundtrigger_middleware"
|
||||
#define CLASSNAME PACKAGE "/AudioSessionProviderImpl"
|
||||
#define SESSION_CLASSNAME PACKAGE "/SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession"
|
||||
|
||||
jobject acquireAudioSession(
|
||||
JNIEnv* env,
|
||||
jobject clazz) {
|
||||
|
||||
audio_session_t session;
|
||||
audio_io_handle_t ioHandle;
|
||||
audio_devices_t device;
|
||||
|
||||
status_t status = AudioSystem::acquireSoundTriggerSession(&session,
|
||||
&ioHandle,
|
||||
&device);
|
||||
if (status != 0) {
|
||||
std::ostringstream message;
|
||||
message
|
||||
<< "AudioSystem::acquireSoundTriggerSession returned an error code: "
|
||||
<< status;
|
||||
env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"),
|
||||
message.str().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jclass cls = FindClassOrDie(env, SESSION_CLASSNAME);
|
||||
jmethodID ctor = GetMethodIDOrDie(env, cls, "<init>", "(III)V");
|
||||
return env->NewObject(cls,
|
||||
ctor,
|
||||
static_cast<int>(session),
|
||||
static_cast<int>(ioHandle),
|
||||
static_cast<int>(device));
|
||||
}
|
||||
|
||||
void releaseAudioSession(JNIEnv* env, jobject clazz, jint handle) {
|
||||
status_t status =
|
||||
AudioSystem::releaseSoundTriggerSession(static_cast<audio_session_t>(handle));
|
||||
|
||||
if (status != 0) {
|
||||
std::ostringstream message;
|
||||
message
|
||||
<< "AudioSystem::releaseAudioSystemSession returned an error code: "
|
||||
<< status;
|
||||
env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"),
|
||||
message.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const JNINativeMethod g_methods[] = {
|
||||
{"acquireSession", "()L" SESSION_CLASSNAME ";",
|
||||
reinterpret_cast<void*>(acquireAudioSession)},
|
||||
{"releaseSession", "(I)V",
|
||||
reinterpret_cast<void*>(releaseAudioSession)},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(
|
||||
JNIEnv* env) {
|
||||
return RegisterMethodsOrDie(env,
|
||||
CLASSNAME,
|
||||
g_methods,
|
||||
NELEM(g_methods));
|
||||
}
|
||||
|
||||
} // namespace android
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user