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