1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.soundtrigger_middleware;
18 
19 import android.annotation.NonNull;
20 import android.media.soundtrigger.ModelParameterRange;
21 import android.media.soundtrigger.PhraseSoundModel;
22 import android.media.soundtrigger.Properties;
23 import android.media.soundtrigger.RecognitionConfig;
24 import android.media.soundtrigger.SoundModel;
25 import android.media.soundtrigger.Status;
26 import android.os.IBinder;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 
31 /**
32  * This wrapper prevents a models with the same UUID from being loaded concurrently. This is used to
33  * protect STHAL implementations, which don't support concurrent loads of the same model. We reject
34  * the duplicate load with {@link Status#RESOURCE_CONTENTION}.
35  */
36 public class SoundTriggerDuplicateModelHandler implements ISoundTriggerHal {
37     private final @NonNull ISoundTriggerHal mDelegate;
38 
39     private GlobalCallback mGlobalCallback;
40     // There are rarely more than two models loaded.
41     private final List<ModelData> mModelList = new ArrayList<>();
42 
43     private static final class ModelData {
ModelData(int modelId, String uuid)44         ModelData(int modelId, String uuid) {
45             mModelId = modelId;
46             mUuid = uuid;
47         }
48 
getModelId()49         int getModelId() {
50             return mModelId;
51         }
52 
getUuid()53         String getUuid() {
54             return mUuid;
55         }
56 
getWasContended()57         boolean getWasContended() {
58             return mWasContended;
59         }
60 
setWasContended()61         void setWasContended() {
62             mWasContended = true;
63         }
64 
65         private int mModelId;
66         private String mUuid;
67         private boolean mWasContended = false;
68     }
69 
SoundTriggerDuplicateModelHandler(@onNull ISoundTriggerHal delegate)70     public SoundTriggerDuplicateModelHandler(@NonNull ISoundTriggerHal delegate) {
71         mDelegate = delegate;
72     }
73 
74     @Override
reboot()75     public void reboot() {
76         mDelegate.reboot();
77     }
78 
79     @Override
detach()80     public void detach() {
81         mDelegate.detach();
82     }
83 
84     @Override
getProperties()85     public Properties getProperties() {
86         return mDelegate.getProperties();
87     }
88 
89     @Override
registerCallback(GlobalCallback callback)90     public void registerCallback(GlobalCallback callback) {
91         mGlobalCallback = callback;
92         mDelegate.registerCallback(mGlobalCallback);
93     }
94 
95     @Override
loadSoundModel(SoundModel soundModel, ModelCallback callback)96     public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
97         synchronized (this) {
98             checkDuplicateModelUuid(soundModel.uuid);
99             var result = mDelegate.loadSoundModel(soundModel, callback);
100             mModelList.add(new ModelData(result, soundModel.uuid));
101             return result;
102         }
103     }
104 
105     @Override
loadPhraseSoundModel(PhraseSoundModel soundModel, ModelCallback callback)106     public int loadPhraseSoundModel(PhraseSoundModel soundModel, ModelCallback callback) {
107         synchronized (this) {
108             checkDuplicateModelUuid(soundModel.common.uuid);
109             var result = mDelegate.loadPhraseSoundModel(soundModel, callback);
110             mModelList.add(new ModelData(result, soundModel.common.uuid));
111             return result;
112         }
113     }
114 
115     @Override
unloadSoundModel(int modelHandle)116     public void unloadSoundModel(int modelHandle) {
117         mDelegate.unloadSoundModel(modelHandle);
118         for (int i = 0; i < mModelList.size(); i++) {
119             if (mModelList.get(i).getModelId() == modelHandle) {
120                 var modelData = mModelList.remove(i);
121                 if (modelData.getWasContended()) {
122                     mGlobalCallback.onResourcesAvailable();
123                 }
124                 // Model ID is unique
125                 return;
126             }
127         }
128     }
129 
130     // Uninteresting delegation calls to follow.
131     @Override
stopRecognition(int modelHandle)132     public void stopRecognition(int modelHandle) {
133         mDelegate.stopRecognition(modelHandle);
134     }
135 
136     @Override
startRecognition( int modelHandle, int deviceHandle, int ioHandle, RecognitionConfig config)137     public void startRecognition(
138             int modelHandle, int deviceHandle, int ioHandle, RecognitionConfig config) {
139         mDelegate.startRecognition(modelHandle, deviceHandle, ioHandle, config);
140     }
141 
142     @Override
forceRecognitionEvent(int modelHandle)143     public void forceRecognitionEvent(int modelHandle) {
144         mDelegate.forceRecognitionEvent(modelHandle);
145     }
146 
147     @Override
getModelParameter(int modelHandle, int param)148     public int getModelParameter(int modelHandle, int param) {
149         return mDelegate.getModelParameter(modelHandle, param);
150     }
151 
152     @Override
setModelParameter(int modelHandle, int param, int value)153     public void setModelParameter(int modelHandle, int param, int value) {
154         mDelegate.setModelParameter(modelHandle, param, value);
155     }
156 
157     @Override
queryParameter(int modelHandle, int param)158     public ModelParameterRange queryParameter(int modelHandle, int param) {
159         return mDelegate.queryParameter(modelHandle, param);
160     }
161 
162     @Override
linkToDeath(IBinder.DeathRecipient recipient)163     public void linkToDeath(IBinder.DeathRecipient recipient) {
164         mDelegate.linkToDeath(recipient);
165     }
166 
167     @Override
unlinkToDeath(IBinder.DeathRecipient recipient)168     public void unlinkToDeath(IBinder.DeathRecipient recipient) {
169         mDelegate.unlinkToDeath(recipient);
170     }
171 
172     @Override
interfaceDescriptor()173     public String interfaceDescriptor() {
174         return mDelegate.interfaceDescriptor();
175     }
176 
177     @Override
flushCallbacks()178     public void flushCallbacks() {
179         mDelegate.flushCallbacks();
180     }
181 
182     @Override
clientAttached(IBinder binder)183     public void clientAttached(IBinder binder) {
184         mDelegate.clientAttached(binder);
185     }
186 
187     @Override
clientDetached(IBinder binder)188     public void clientDetached(IBinder binder) {
189         mDelegate.clientDetached(binder);
190     }
191 
192     /**
193      * Helper for handling duplicate model. If there is a load attempt for a model with a UUID which
194      * is already loaded: 1) Reject with {@link Status.RESOURCE_CONTENTION} 2) Mark the already
195      * loaded model as contended, as we need to dispatch a resource available callback following the
196      * original model being unloaded.
197      */
checkDuplicateModelUuid(String uuid)198     private void checkDuplicateModelUuid(String uuid) {
199         var model = mModelList.stream().filter(x -> x.getUuid().equals(uuid)).findFirst();
200         if (model.isPresent()) {
201             model.get().setWasContended();
202             throw new RecoverableException(Status.RESOURCE_CONTENTION);
203         }
204     }
205 }
206