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:
Ytai Ben-Tsvi
2019-11-25 12:43:28 -08:00
parent 0b0441d16c
commit 93c117c86f
26 changed files with 4408 additions and 0 deletions

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
{
"presubmit": [
{
"name": "FrameworksServicesTests",
"options": [
{
"include-filter": "com.android.server.soundtrigger_middleware"
}
]
}
]
}

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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