1 /*
2  * Copyright (C) 2021 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.annotation.Nullable;
21 import android.media.soundtrigger.ModelParameterRange;
22 import android.media.soundtrigger.PhraseSoundModel;
23 import android.media.soundtrigger.Properties;
24 import android.media.soundtrigger.RecognitionConfig;
25 import android.media.soundtrigger.SoundModel;
26 import android.media.soundtrigger.SoundModelType;
27 import android.media.soundtrigger.Status;
28 import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
29 import android.media.soundtrigger_middleware.RecognitionEventSys;
30 import android.os.IBinder;
31 
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.LinkedList;
35 import java.util.Map;
36 import java.util.Queue;
37 import java.util.Set;
38 import java.util.concurrent.ConcurrentHashMap;
39 
40 /**
41  * This is a decorator around ISoundTriggerHal, which implements enforcement of concurrent capture
42  * constraints, for HAL implementations older than V2.4 (later versions support this feature at the
43  * HAL level).
44  * <p>
45  * Decorating an instance with this class would result in all active recognitions being aborted as
46  * soon as capture state becomes active. This class ensures consistent handling of abortions coming
47  * from that HAL and abortions coming from concurrent capture, in that only one abort event will be
48  * delivered, irrespective of the relative timing of the two events.
49  * <p>
50  * There are some delicate thread-safety issues handled here:
51  * <ul>
52  * <li>When a model is stopped via stopRecognition(), we guarantee that by the time the call
53  * returns, there will be no more recognition events (including abort) delivered for this model.
54  * This implies synchronous stopping and blocking until all pending events have been delivered.
55  * <li>When a model is stopped via onCaptureStateChange(true), the stopping of the recognition at
56  * the HAL level must be synchronous, but the call must not block on the delivery of the
57  * callbacks, due to the risk of a deadlock: the onCaptureStateChange() calls are typically
58  * invoked with the audio policy mutex held, so must not call method which may attempt to lock
59  * higher-level mutexes. See README.md in this directory for further details.
60  * </ul>
61  * The way this behavior is achieved is by having an additional thread with an event queue, which
62  * joins together model events coming from the delegate module with abort events originating from
63  * this layer (as result of external capture).
64  */
65 public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal,
66         ICaptureStateNotifier.Listener {
67     @NonNull private final ISoundTriggerHal mDelegate;
68     private GlobalCallback mGlobalCallback;
69     /**
70      * This lock must be held to synchronize forward calls (start/stop/onCaptureStateChange) that
71      * update the mActiveModels set and mCaptureState.
72      * It must not be locked in HAL callbacks to avoid deadlocks.
73      */
74     @NonNull private final Object mStartStopLock = new Object();
75 
76     /**
77      * Information about a model that is currently loaded. This is needed in order to be able to
78      * send abort events to its designated callback.
79      */
80     private static class LoadedModel {
81         public final int type;
82         @NonNull public final ModelCallback callback;
83 
LoadedModel(int type, @NonNull ModelCallback callback)84         LoadedModel(int type, @NonNull ModelCallback callback) {
85             this.type = type;
86             this.callback = callback;
87         }
88     }
89 
90     /**
91      * This map holds the model type for every model that is loaded.
92      */
93     @NonNull private final Map<Integer, LoadedModel> mLoadedModels = new ConcurrentHashMap<>();
94 
95     /**
96      * A set of all models that are currently active.
97      * We use this in order to know which models to stop in case of external capture.
98      * Used as a lock to synchronize operations that effect activity.
99      */
100     @NonNull private final Set<Integer> mActiveModels = new HashSet<>();
101 
102     /**
103      * Notifier for changes in capture state.
104      */
105     @NonNull private final ICaptureStateNotifier mNotifier;
106 
107     /**
108      * Whether capture is active.
109      */
110     private boolean mCaptureState;
111 
112     /**
113      * Since we're wrapping the death recipient, we need to keep a translation map for unlinking.
114      * Key is the client recipient, value is the wrapper.
115      */
116     @NonNull private final Map<IBinder.DeathRecipient, IBinder.DeathRecipient>
117             mDeathRecipientMap = new ConcurrentHashMap<>();
118 
119     @NonNull private final CallbackThread mCallbackThread = new CallbackThread();
120 
SoundTriggerHalConcurrentCaptureHandler( @onNull ISoundTriggerHal delegate, @NonNull ICaptureStateNotifier notifier)121     public SoundTriggerHalConcurrentCaptureHandler(
122             @NonNull ISoundTriggerHal delegate,
123             @NonNull ICaptureStateNotifier notifier) {
124         mDelegate = delegate;
125         mNotifier = notifier;
126         mCaptureState = mNotifier.registerListener(this);
127     }
128 
129     @Override
startRecognition(int modelHandle, int deviceHandle, int ioHandle, RecognitionConfig config)130     public void startRecognition(int modelHandle, int deviceHandle, int ioHandle,
131             RecognitionConfig config) {
132         synchronized (mStartStopLock) {
133             synchronized (mActiveModels) {
134                 if (mCaptureState) {
135                     throw new RecoverableException(Status.RESOURCE_CONTENTION);
136                 }
137                 mDelegate.startRecognition(modelHandle, deviceHandle, ioHandle, config);
138                 mActiveModels.add(modelHandle);
139             }
140         }
141     }
142 
143     @Override
stopRecognition(int modelHandle)144     public void stopRecognition(int modelHandle) {
145         synchronized (mStartStopLock) {
146             boolean wasActive;
147             synchronized (mActiveModels) {
148                 wasActive = mActiveModels.remove(modelHandle);
149             }
150             if (wasActive) {
151                 // Must be done outside of the lock, since it may trigger synchronous callbacks.
152                 mDelegate.stopRecognition(modelHandle);
153             }
154         }
155         // Block until all previous events are delivered. Since this is potentially blocking on
156         // upward calls, it must be done outside the lock.
157         mCallbackThread.flush();
158     }
159 
160     @Override
onCaptureStateChange(boolean active)161     public void onCaptureStateChange(boolean active) {
162         synchronized (mStartStopLock) {
163             if (active) {
164                 abortAllActiveModels();
165             } else {
166                 if (mGlobalCallback != null) {
167                     mGlobalCallback.onResourcesAvailable();
168                 }
169             }
170             mCaptureState = active;
171         }
172     }
173 
abortAllActiveModels()174     private void abortAllActiveModels() {
175         while (true) {
176             int toStop;
177             synchronized (mActiveModels) {
178                 Iterator<Integer> iterator = mActiveModels.iterator();
179                 if (!iterator.hasNext()) {
180                     return;
181                 }
182                 toStop = iterator.next();
183                 mActiveModels.remove(toStop);
184             }
185             // Invoke stop outside of the lock.
186             mDelegate.stopRecognition(toStop);
187 
188             LoadedModel model = mLoadedModels.get(toStop);
189             // Queue an abort event (no need to flush).
190             mCallbackThread.push(() -> notifyAbort(toStop, model));
191         }
192     }
193 
194     @Override
loadSoundModel(SoundModel soundModel, ModelCallback callback)195     public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
196         int handle = mDelegate.loadSoundModel(soundModel, new CallbackWrapper(callback));
197         mLoadedModels.put(handle, new LoadedModel(SoundModelType.GENERIC, callback));
198         return handle;
199     }
200 
201     @Override
loadPhraseSoundModel(PhraseSoundModel soundModel, ModelCallback callback)202     public int loadPhraseSoundModel(PhraseSoundModel soundModel,
203             ModelCallback callback) {
204         int handle = mDelegate.loadPhraseSoundModel(soundModel, new CallbackWrapper(callback));
205         mLoadedModels.put(handle, new LoadedModel(SoundModelType.KEYPHRASE, callback));
206         return handle;
207     }
208 
209     @Override
unloadSoundModel(int modelHandle)210     public void unloadSoundModel(int modelHandle) {
211         mLoadedModels.remove(modelHandle);
212         mDelegate.unloadSoundModel(modelHandle);
213     }
214 
215     @Override
registerCallback(GlobalCallback callback)216     public void registerCallback(GlobalCallback callback) {
217         mGlobalCallback = () -> mCallbackThread.push(callback::onResourcesAvailable);
218         mDelegate.registerCallback(mGlobalCallback);
219     }
220 
221     @Override
linkToDeath(IBinder.DeathRecipient recipient)222     public void linkToDeath(IBinder.DeathRecipient recipient) {
223         IBinder.DeathRecipient wrapper = () -> mCallbackThread.push(recipient::binderDied);
224         mDelegate.linkToDeath(wrapper);
225         mDeathRecipientMap.put(recipient, wrapper);
226     }
227 
228     @Override
unlinkToDeath(IBinder.DeathRecipient recipient)229     public void unlinkToDeath(IBinder.DeathRecipient recipient) {
230         mDelegate.unlinkToDeath(mDeathRecipientMap.remove(recipient));
231     }
232 
233     private class CallbackWrapper implements ISoundTriggerHal.ModelCallback {
234         @NonNull private final ISoundTriggerHal.ModelCallback mDelegateCallback;
235 
CallbackWrapper(@onNull ModelCallback delegateCallback)236         private CallbackWrapper(@NonNull ModelCallback delegateCallback) {
237             mDelegateCallback = delegateCallback;
238         }
239 
240         @Override
recognitionCallback(int modelHandle, RecognitionEventSys event)241         public void recognitionCallback(int modelHandle, RecognitionEventSys event) {
242             synchronized (mActiveModels) {
243                 if (!mActiveModels.contains(modelHandle)) {
244                     // Discard the event.
245                     return;
246                 }
247                 if (!event.recognitionEvent.recognitionStillActive) {
248                     mActiveModels.remove(modelHandle);
249                 }
250                 // A recognition event must be the last one for its model, unless it indicates that
251                 // recognition is still active.
252                 mCallbackThread.push(
253                         () -> mDelegateCallback.recognitionCallback(modelHandle, event));
254             }
255         }
256 
257         @Override
phraseRecognitionCallback(int modelHandle, PhraseRecognitionEventSys event)258         public void phraseRecognitionCallback(int modelHandle, PhraseRecognitionEventSys event) {
259             synchronized (mActiveModels) {
260                 if (!mActiveModels.contains(modelHandle)) {
261                     // Discard the event.
262                     return;
263                 }
264                 if (!event.phraseRecognitionEvent.common.recognitionStillActive) {
265                     mActiveModels.remove(modelHandle);
266                 }
267                 // A recognition event must be the last one for its model, unless it indicates that
268                 // recognition is still active.
269                 mCallbackThread.push(
270                         () -> mDelegateCallback.phraseRecognitionCallback(modelHandle, event));
271             }
272         }
273 
274         @Override
modelUnloaded(int modelHandle)275         public void modelUnloaded(int modelHandle) {
276             mCallbackThread.push(() -> mDelegateCallback.modelUnloaded(modelHandle));
277         }
278     }
279 
280     @Override
flushCallbacks()281     public void flushCallbacks() {
282         mDelegate.flushCallbacks();
283         mCallbackThread.flush();
284     }
285 
286     @Override
clientAttached(IBinder binder)287     public void clientAttached(IBinder binder) {
288         mDelegate.clientAttached(binder);
289     }
290 
291     @Override
clientDetached(IBinder binder)292     public void clientDetached(IBinder binder) {
293         mDelegate.clientDetached(binder);
294     }
295 
296     /**
297      * This is a thread for asynchronous delivery of callback events, having the following features:
298      * <ul>
299      * <li>Events are processed on a separate thread than the thread that pushed them, in the order
300      * they were pushed.
301      * <li>Events can be flushed via {@link #flush()}. This will block until all events pushed prior
302      * to this call have been fully processed.
303      * TODO(b/246584464) Remove and replace with Handler (and other concurrency fixes).
304      * </ul>
305      */
306     private static class CallbackThread implements Runnable {
307         private final Queue<Runnable> mList = new LinkedList<>();
308         private int mPushCount = 0;
309         private int mProcessedCount = 0;
310         private boolean mQuitting = false;
311         private final Thread mThread;
312         /**
313          * Ctor. Starts the thread.
314          */
CallbackThread()315         CallbackThread() {
316             mThread = new Thread(this , "STHAL Concurrent Capture Handler Callback");
317             mThread.start();
318         }
319         /**
320          * Consume items in the queue until quit is called.
321          */
run()322         public void run() {
323             try {
324                 while (true) {
325                     Runnable toRun = pop();
326                     if (toRun == null) {
327                       // There are no longer any runnables to run,
328                       // and quit() has been called.
329                       return;
330                     }
331                     toRun.run();
332                     synchronized (mList) {
333                         mProcessedCount++;
334                         mList.notifyAll();
335                     }
336                 }
337             } catch (InterruptedException e) {
338                 // If interrupted, exit.
339                 // Note, this is dangerous wrt to flush.
340             }
341         }
342         /**
343          * Push a new runnable to the queue, with no deduping.
344          * If quit has been called, the runnable will not be pushed.
345          *
346          * @param runnable The runnable to push.
347          * @return If the runnable was successfully pushed.
348          */
push(Runnable runnable)349         boolean push(Runnable runnable) {
350             synchronized (mList) {
351                 if (mQuitting) return false;
352                 mList.add(runnable);
353                 mPushCount++;
354                 mList.notifyAll();
355             }
356             return true;
357         }
358 
359         /**
360          * Block until every entry pushed prior to this call has been processed.
361          */
flush()362         void flush() {
363             try {
364                 synchronized (mList) {
365                     int pushCount = mPushCount;
366                     while (mProcessedCount != pushCount) {
367                         mList.wait();
368                     }
369                 }
370             } catch (InterruptedException ignored) {
371             }
372         }
373 
374         /**
375          * Quit processing after the queue is cleared.
376          * All subsequent calls to push will fail.
377          * Note, this does not flush.
378          */
quit()379         void quit() {
380             synchronized(mList) {
381                 mQuitting = true;
382                 mList.notifyAll();
383             }
384         }
385 
386         // Returns the next runnable when available.
387         // Returns null iff the list is empty and quit has been called.
pop()388         private @Nullable Runnable pop() throws InterruptedException {
389             synchronized (mList) {
390                 while (mList.isEmpty() && !mQuitting) {
391                     mList.wait();
392                 }
393                 if (mList.isEmpty() && mQuitting) return null;
394                 return mList.remove();
395             }
396         }
397 
398     }
399 
400     /** Notify the client that recognition has been aborted. */
notifyAbort(int modelHandle, LoadedModel model)401     private static void notifyAbort(int modelHandle, LoadedModel model) {
402         switch (model.type) {
403             case SoundModelType.GENERIC:
404                 model.callback.recognitionCallback(modelHandle, AidlUtil.newAbortEvent());
405                 break;
406 
407             case SoundModelType.KEYPHRASE:
408                 model.callback.phraseRecognitionCallback(modelHandle,
409                         AidlUtil.newAbortPhraseEvent());
410                 break;
411         }
412     }
413 
414     @Override
detach()415     public void detach() {
416         mDelegate.detach();
417         mNotifier.unregisterListener(this);
418         mCallbackThread.quit();
419     }
420 
421     ////////////////////////////////////////////////////////////////////////////////////////////////
422     // All methods below do trivial delegation - no interesting logic.
423     @Override
reboot()424     public void reboot() {
425         mDelegate.reboot();
426     }
427 
428     @Override
getProperties()429     public Properties getProperties() {
430         return mDelegate.getProperties();
431     }
432 
433     @Override
forceRecognitionEvent(int modelHandle)434     public void forceRecognitionEvent(int modelHandle) {
435         mDelegate.forceRecognitionEvent(modelHandle);
436     }
437 
438     @Override
getModelParameter(int modelHandle, int param)439     public int getModelParameter(int modelHandle, int param) {
440         return mDelegate.getModelParameter(modelHandle, param);
441     }
442 
443     @Override
setModelParameter(int modelHandle, int param, int value)444     public void setModelParameter(int modelHandle, int param, int value) {
445         mDelegate.setModelParameter(modelHandle, param, value);
446     }
447 
448     @Override
queryParameter(int modelHandle, int param)449     public ModelParameterRange queryParameter(int modelHandle, int param) {
450         return mDelegate.queryParameter(modelHandle, param);
451     }
452 
453     @Override
interfaceDescriptor()454     public String interfaceDescriptor() {
455         return mDelegate.interfaceDescriptor();
456     }
457 }
458