summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Atneya Nair <atneya@google.com> 2023-04-03 11:29:29 -0700
committer Atneya Nair <atneya@google.com> 2023-04-11 12:12:53 -0700
commit552c327b60752f28e5ee5fcca89af65ffb8a6c5e (patch)
tree7eaf18aeb29f38d718ef094188af2a750c644b84
parentca7c149247e029b159b089c5abe5f583bb640227 (diff)
Protect STHAL from duplicate UUID model loads
The STHAL aborts when duplicate UUID model loads are received. To protect against this, we add a handler above both v2, v3 hals, which rejects duplicate UUID model loads with RESOURCE_CONTENTION. Fixes: 275058053 Test: atest SoundTriggerDuplicateModelHandlerTest Test: manual verification of assistant/now playing Change-Id: I53b249856fba49b3b33d5668660f4beb8611a20c
-rw-r--r--services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandlerTest.java335
-rw-r--r--services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/TestUtil.java2
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandler.java205
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java3
4 files changed, 543 insertions, 2 deletions
diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandlerTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandlerTest.java
new file mode 100644
index 000000000000..bc94dacea324
--- /dev/null
+++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandlerTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2022 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 static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.media.soundtrigger.ModelParameterRange;
+import android.media.soundtrigger.Phrase;
+import android.media.soundtrigger.PhraseSoundModel;
+import android.media.soundtrigger.Properties;
+import android.media.soundtrigger.RecognitionConfig;
+import android.media.soundtrigger.RecognitionMode;
+import android.media.soundtrigger.SoundModel;
+import android.media.soundtrigger.SoundModelType;
+import android.media.soundtrigger.Status;
+import android.os.IBinder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.InOrder;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public final class SoundTriggerDuplicateModelHandlerTest {
+ // Component under test
+ private SoundTriggerDuplicateModelHandler mComponent;
+
+ private static final String DUPLICATE_UUID = "abcddead-beef-0123-3210-0123456789ab";
+ private static final String DIFFERENT_UUID = "0000dead-beef-0123-3210-0123456789ab";
+
+ @Mock private ISoundTriggerHal mUnderlying;
+ @Mock private ISoundTriggerHal.GlobalCallback mGlobalCallback;
+ @Mock private ISoundTriggerHal.ModelCallback mModelCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mComponent = new SoundTriggerDuplicateModelHandler(mUnderlying);
+ doNothing().when(mUnderlying).registerCallback(any());
+ mComponent.registerCallback(mGlobalCallback);
+ verify(mUnderlying).registerCallback(eq(mGlobalCallback));
+ }
+
+ @Test
+ public void loadSoundModel_throwsResourceContention_whenDuplicateUuid() {
+ final var soundModel = createSoundModelOne();
+ final var soundModelSameUuid = createSoundModelTwo();
+ // First sound model load should complete successfully
+ mComponent.loadSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadSoundModel(eq(soundModel), eq(mModelCallback));
+ assertEquals(
+ assertThrows(
+ RecoverableException.class,
+ () -> mComponent.loadSoundModel(soundModelSameUuid, mModelCallback))
+ .errorCode,
+ Status.RESOURCE_CONTENTION);
+ // Model has not been unloaded, so we don't get a callback
+ verify(mGlobalCallback, never()).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void loadSoundModel_doesNotThrowResourceContention_whenDifferentUuid() {
+ final var soundModel = createSoundModelOne();
+ // Make all other fields the same
+ final var soundModelDifferentUuid = createSoundModelOne();
+ soundModelDifferentUuid.uuid = DIFFERENT_UUID;
+ InOrder inOrder = Mockito.inOrder(mUnderlying);
+ // First sound model load should complete successfully
+ mComponent.loadSoundModel(soundModel, mModelCallback);
+ inOrder.verify(mUnderlying).loadSoundModel(eq(soundModel), eq(mModelCallback));
+ mComponent.loadSoundModel(soundModelDifferentUuid, mModelCallback);
+ inOrder.verify(mUnderlying).loadSoundModel(eq(soundModelDifferentUuid), eq(mModelCallback));
+ // No contention, so we don't get a callback
+ verify(mGlobalCallback, never()).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void loadSoundModel_doesNotThrow_afterDuplicateUuidHasBeenUnloaded() {
+ final var soundModel = createSoundModelOne();
+ // First sound model load should complete successfully
+ int handle = mComponent.loadSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadSoundModel(eq(soundModel), eq(mModelCallback));
+ // Unload model should complete successfully
+ mComponent.unloadSoundModel(handle);
+ verify(mUnderlying).unloadSoundModel(eq(handle));
+ // Since the model with the same UUID was unloaded, the subsequent load model
+ // should succeed.
+ mComponent.loadSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying, times(2)).loadSoundModel(eq(soundModel), eq(mModelCallback));
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void unloadSoundModel_triggersResourceCallback_afterDuplicateUuidRejected() {
+ final var soundModel = createSoundModelOne();
+ final var soundModelSameUuid = createSoundModelTwo();
+ // First sound model load should complete successfully
+ int handle = mComponent.loadSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadSoundModel(eq(soundModel), eq(mModelCallback));
+ assertEquals(
+ assertThrows(
+ RecoverableException.class,
+ () -> mComponent.loadSoundModel(soundModelSameUuid, mModelCallback))
+ .errorCode,
+ Status.RESOURCE_CONTENTION);
+ mComponent.unloadSoundModel(handle);
+ verify(mUnderlying).unloadSoundModel(eq(handle));
+ verify(mGlobalCallback).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ // Next tests are same as above, but for phrase sound model.
+ @Test
+ public void loadPhraseSoundModel_throwsResourceContention_whenDuplicateUuid() {
+ final var soundModel = createPhraseSoundModelOne();
+ final var soundModelSameUuid = createPhraseSoundModelTwo();
+ // First sound model load should complete successfully
+ mComponent.loadPhraseSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadPhraseSoundModel(eq(soundModel), eq(mModelCallback));
+ assertEquals(
+ assertThrows(
+ RecoverableException.class,
+ () ->
+ mComponent.loadPhraseSoundModel(
+ soundModelSameUuid, mModelCallback))
+ .errorCode,
+ Status.RESOURCE_CONTENTION);
+ // Model has not been unloaded, so we don't get a callback
+ verify(mGlobalCallback, never()).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void loadPhraseSoundModel_doesNotThrowResourceContention_whenDifferentUuid() {
+ final var soundModel = createPhraseSoundModelOne();
+ // Make all other fields the same
+ final var soundModelDifferentUuid = createPhraseSoundModelOne();
+ soundModelDifferentUuid.common.uuid = DIFFERENT_UUID;
+ InOrder inOrder = Mockito.inOrder(mUnderlying);
+ // First sound model load should complete successfully
+ mComponent.loadPhraseSoundModel(soundModel, mModelCallback);
+ inOrder.verify(mUnderlying).loadPhraseSoundModel(eq(soundModel), eq(mModelCallback));
+ mComponent.loadPhraseSoundModel(soundModelDifferentUuid, mModelCallback);
+ inOrder.verify(mUnderlying).loadPhraseSoundModel(eq(soundModelDifferentUuid),
+ eq(mModelCallback));
+ // No contention, so we don't get a callback
+ verify(mGlobalCallback, never()).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void loadPhraseSoundModel_doesNotThrow_afterDuplicateUuidHasBeenUnloaded() {
+ final var soundModel = createPhraseSoundModelOne();
+ // First sound model load should complete successfully
+ int handle = mComponent.loadPhraseSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadPhraseSoundModel(eq(soundModel), eq(mModelCallback));
+ // Unload model should complete successfully
+ mComponent.unloadSoundModel(handle);
+ verify(mUnderlying).unloadSoundModel(eq(handle));
+ // Since the model with the same UUID was unloaded, the subsequent load model
+ // should succeed.
+ mComponent.loadPhraseSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying, times(2)).loadPhraseSoundModel(eq(soundModel), eq(mModelCallback));
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void unloadSoundModel_triggersResourceCallback_afterDuplicateUuidRejectedPhrase() {
+ final var soundModel = createPhraseSoundModelOne();
+ final var soundModelSameUuid = createPhraseSoundModelTwo();
+ // First sound model load should complete successfully
+ int handle = mComponent.loadPhraseSoundModel(soundModel, mModelCallback);
+ verify(mUnderlying).loadPhraseSoundModel(eq(soundModel), eq(mModelCallback));
+ assertEquals(
+ assertThrows(
+ RecoverableException.class,
+ () ->
+ mComponent.loadPhraseSoundModel(
+ soundModelSameUuid, mModelCallback))
+ .errorCode,
+ Status.RESOURCE_CONTENTION);
+ mComponent.unloadSoundModel(handle);
+ verify(mUnderlying).unloadSoundModel(eq(handle));
+ verify(mGlobalCallback).onResourcesAvailable();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ @Test
+ public void testDelegation() {
+ // Test that the rest of the interface delegates its calls to the underlying object
+ // appropriately.
+ // This test method does not test load/unloadSoundModel
+ var properties = new Properties();
+ InOrder inOrder = Mockito.inOrder(mUnderlying);
+ doReturn(properties).when(mUnderlying).getProperties();
+ assertEquals(mComponent.getProperties(), properties);
+ inOrder.verify(mUnderlying).getProperties();
+ var mockGlobalCallback = mock(ISoundTriggerHal.GlobalCallback.class);
+ mComponent.registerCallback(mockGlobalCallback);
+ inOrder.verify(mUnderlying).registerCallback(eq(mockGlobalCallback));
+ int modelId = 5;
+ int deviceHandle = 2;
+ int ioHandle = 3;
+ var config = mock(RecognitionConfig.class);
+ mComponent.startRecognition(modelId, deviceHandle, ioHandle, config);
+ inOrder.verify(mUnderlying)
+ .startRecognition(eq(modelId), eq(deviceHandle), eq(ioHandle), eq(config));
+
+ mComponent.stopRecognition(modelId);
+ inOrder.verify(mUnderlying).stopRecognition(eq(modelId));
+ mComponent.forceRecognitionEvent(modelId);
+ inOrder.verify(mUnderlying).forceRecognitionEvent(eq(modelId));
+ int param = 10;
+ int value = 50;
+ var modelParamRange = new ModelParameterRange();
+ doReturn(modelParamRange).when(mUnderlying).queryParameter(anyInt(), anyInt());
+ assertEquals(mComponent.queryParameter(param, value), modelParamRange);
+ inOrder.verify(mUnderlying).queryParameter(param, value);
+ doReturn(value).when(mUnderlying).getModelParameter(anyInt(), anyInt());
+ assertEquals(mComponent.getModelParameter(modelId, param), value);
+ inOrder.verify(mUnderlying).getModelParameter(eq(modelId), eq(param));
+ mComponent.setModelParameter(modelId, param, value);
+ inOrder.verify(mUnderlying).setModelParameter(eq(modelId), eq(param), eq(value));
+ var recipient = mock(IBinder.DeathRecipient.class);
+ mComponent.linkToDeath(recipient);
+ inOrder.verify(mUnderlying).linkToDeath(eq(recipient));
+ mComponent.unlinkToDeath(recipient);
+ inOrder.verify(mUnderlying).unlinkToDeath(eq(recipient));
+ mComponent.flushCallbacks();
+ inOrder.verify(mUnderlying).flushCallbacks();
+ var token = mock(IBinder.class);
+ mComponent.clientAttached(token);
+ inOrder.verify(mUnderlying).clientAttached(eq(token));
+ mComponent.clientDetached(token);
+ inOrder.verify(mUnderlying).clientDetached(eq(token));
+ mComponent.reboot();
+ inOrder.verify(mUnderlying).reboot();
+ mComponent.detach();
+ inOrder.verify(mUnderlying).detach();
+ verifyNoMoreInteractions(mUnderlying);
+ verifyNoMoreInteractions(mGlobalCallback);
+ }
+
+ private static SoundModel createSoundModelOne() {
+ SoundModel model = new SoundModel();
+ model.type = SoundModelType.GENERIC;
+ model.uuid = DUPLICATE_UUID;
+ model.vendorUuid = "87654321-5432-6543-7654-456789fedcba";
+ byte[] data = new byte[] {91, 92, 93, 94, 95};
+ model.data = TestUtil.byteArrayToParcelFileDescriptor(data);
+ model.dataSize = data.length;
+ return model;
+ }
+
+ // Different except for the same UUID
+ private static SoundModel createSoundModelTwo() {
+ SoundModel model = new SoundModel();
+ model.type = SoundModelType.GENERIC;
+ model.uuid = DUPLICATE_UUID;
+ model.vendorUuid = "12345678-9876-5432-1012-345678901234";
+ byte[] data = new byte[] {19, 18, 17, 16};
+ model.data = TestUtil.byteArrayToParcelFileDescriptor(data);
+ model.dataSize = data.length;
+ return model;
+ }
+
+ private static PhraseSoundModel createPhraseSoundModelOne() {
+ PhraseSoundModel model = new PhraseSoundModel();
+ model.common = createSoundModelOne();
+ model.common.type = SoundModelType.KEYPHRASE;
+ model.phrases = new Phrase[1];
+ model.phrases[0] = new Phrase();
+ model.phrases[0].id = 123;
+ model.phrases[0].users = new int[] {5, 6, 7};
+ model.phrases[0].locale = "locale";
+ model.phrases[0].text = "text";
+ model.phrases[0].recognitionModes =
+ RecognitionMode.USER_AUTHENTICATION | RecognitionMode.USER_IDENTIFICATION;
+ return model;
+ }
+
+ private static PhraseSoundModel createPhraseSoundModelTwo() {
+ PhraseSoundModel model = new PhraseSoundModel();
+ model.common = createSoundModelTwo();
+ model.common.type = SoundModelType.KEYPHRASE;
+ model.phrases = new Phrase[1];
+ model.phrases[0] = new Phrase();
+ model.phrases[0].id = 321;
+ model.phrases[0].users = new int[] {4, 3, 2, 1};
+ model.phrases[0].locale = "differentLocale";
+ model.phrases[0].text = "differentText";
+ model.phrases[0].recognitionModes = 0;
+ return model;
+ }
+}
diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/TestUtil.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/TestUtil.java
index 39561f74d7ed..3b7bc952639e 100644
--- a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/TestUtil.java
+++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/TestUtil.java
@@ -466,7 +466,7 @@ class TestUtil {
assertEquals(43, event.phraseExtras[0].levels[0].levelPercent);
}
- private static ParcelFileDescriptor byteArrayToParcelFileDescriptor(byte[] data) {
+ static ParcelFileDescriptor byteArrayToParcelFileDescriptor(byte[] data) {
try (SharedMemory shmem = SharedMemory.create("", data.length)) {
ByteBuffer buffer = shmem.mapReadWrite();
buffer.put(data);
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandler.java
new file mode 100644
index 000000000000..01041932567c
--- /dev/null
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerDuplicateModelHandler.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 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.media.soundtrigger.ModelParameterRange;
+import android.media.soundtrigger.PhraseSoundModel;
+import android.media.soundtrigger.Properties;
+import android.media.soundtrigger.RecognitionConfig;
+import android.media.soundtrigger.SoundModel;
+import android.media.soundtrigger.Status;
+import android.os.IBinder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This wrapper prevents a models with the same UUID from being loaded concurrently. This is used to
+ * protect STHAL implementations, which don't support concurrent loads of the same model. We reject
+ * the duplicate load with {@link Status#RESOURCE_CONTENTION}.
+ */
+public class SoundTriggerDuplicateModelHandler implements ISoundTriggerHal {
+ private final @NonNull ISoundTriggerHal mDelegate;
+
+ private GlobalCallback mGlobalCallback;
+ // There are rarely more than two models loaded.
+ private final List<ModelData> mModelList = new ArrayList<>();
+
+ private static final class ModelData {
+ ModelData(int modelId, String uuid) {
+ mModelId = modelId;
+ mUuid = uuid;
+ }
+
+ int getModelId() {
+ return mModelId;
+ }
+
+ String getUuid() {
+ return mUuid;
+ }
+
+ boolean getWasContended() {
+ return mWasContended;
+ }
+
+ void setWasContended() {
+ mWasContended = true;
+ }
+
+ private int mModelId;
+ private String mUuid;
+ private boolean mWasContended = false;
+ }
+
+ public SoundTriggerDuplicateModelHandler(@NonNull ISoundTriggerHal delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void reboot() {
+ mDelegate.reboot();
+ }
+
+ @Override
+ public void detach() {
+ mDelegate.detach();
+ }
+
+ @Override
+ public Properties getProperties() {
+ return mDelegate.getProperties();
+ }
+
+ @Override
+ public void registerCallback(GlobalCallback callback) {
+ mGlobalCallback = callback;
+ mDelegate.registerCallback(mGlobalCallback);
+ }
+
+ @Override
+ public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
+ synchronized (this) {
+ checkDuplicateModelUuid(soundModel.uuid);
+ var result = mDelegate.loadSoundModel(soundModel, callback);
+ mModelList.add(new ModelData(result, soundModel.uuid));
+ return result;
+ }
+ }
+
+ @Override
+ public int loadPhraseSoundModel(PhraseSoundModel soundModel, ModelCallback callback) {
+ synchronized (this) {
+ checkDuplicateModelUuid(soundModel.common.uuid);
+ var result = mDelegate.loadPhraseSoundModel(soundModel, callback);
+ mModelList.add(new ModelData(result, soundModel.common.uuid));
+ return result;
+ }
+ }
+
+ @Override
+ public void unloadSoundModel(int modelHandle) {
+ mDelegate.unloadSoundModel(modelHandle);
+ for (int i = 0; i < mModelList.size(); i++) {
+ if (mModelList.get(i).getModelId() == modelHandle) {
+ var modelData = mModelList.remove(i);
+ if (modelData.getWasContended()) {
+ mGlobalCallback.onResourcesAvailable();
+ }
+ // Model ID is unique
+ return;
+ }
+ }
+ }
+
+ // Uninteresting delegation calls to follow.
+ @Override
+ public void stopRecognition(int modelHandle) {
+ mDelegate.stopRecognition(modelHandle);
+ }
+
+ @Override
+ public void startRecognition(
+ int modelHandle, int deviceHandle, int ioHandle, RecognitionConfig config) {
+ mDelegate.startRecognition(modelHandle, deviceHandle, ioHandle, config);
+ }
+
+ @Override
+ public void forceRecognitionEvent(int modelHandle) {
+ mDelegate.forceRecognitionEvent(modelHandle);
+ }
+
+ @Override
+ public int getModelParameter(int modelHandle, int param) {
+ return mDelegate.getModelParameter(modelHandle, param);
+ }
+
+ @Override
+ public void setModelParameter(int modelHandle, int param, int value) {
+ mDelegate.setModelParameter(modelHandle, param, value);
+ }
+
+ @Override
+ public ModelParameterRange queryParameter(int modelHandle, int param) {
+ return mDelegate.queryParameter(modelHandle, param);
+ }
+
+ @Override
+ public void linkToDeath(IBinder.DeathRecipient recipient) {
+ mDelegate.linkToDeath(recipient);
+ }
+
+ @Override
+ public void unlinkToDeath(IBinder.DeathRecipient recipient) {
+ mDelegate.unlinkToDeath(recipient);
+ }
+
+ @Override
+ public String interfaceDescriptor() {
+ return mDelegate.interfaceDescriptor();
+ }
+
+ @Override
+ public void flushCallbacks() {
+ mDelegate.flushCallbacks();
+ }
+
+ @Override
+ public void clientAttached(IBinder binder) {
+ mDelegate.clientAttached(binder);
+ }
+
+ @Override
+ public void clientDetached(IBinder binder) {
+ mDelegate.clientDetached(binder);
+ }
+
+ /**
+ * Helper for handling duplicate model. If there is a load attempt for a model with a UUID which
+ * is already loaded: 1) Reject with {@link Status.RESOURCE_CONTENTION} 2) Mark the already
+ * loaded model as contended, as we need to dispatch a resource available callback following the
+ * original model being unloaded.
+ */
+ private void checkDuplicateModelUuid(String uuid) {
+ var model = mModelList.stream().filter(x -> x.getUuid().equals(uuid)).findFirst();
+ if (model.isPresent()) {
+ model.get().setWasContended();
+ throw new RecoverableException(Status.RESOURCE_CONTENTION);
+ }
+ }
+}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
index d2d8f1ad7a71..6223b2e8ace4 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
@@ -170,7 +170,8 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo
*/
private void attachToHal() {
mHalService = new SoundTriggerHalEnforcer(
- new SoundTriggerHalWatchdog(mHalFactory.create()));
+ new SoundTriggerHalWatchdog(
+ new SoundTriggerDuplicateModelHandler(mHalFactory.create())));
mHalService.linkToDeath(this);
mHalService.registerCallback(this);
mProperties = mHalService.getProperties();