1 /*
2  * Copyright (C) 2019 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.Status;
27 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
28 import android.media.soundtrigger_middleware.ISoundTriggerModule;
29 import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
30 import android.media.soundtrigger_middleware.RecognitionEventSys;
31 import android.os.Binder;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.util.Slog;
35 
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.Set;
43 
44 /**
45  * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface,
46  * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate
47  * clients.
48  * <p>
49  * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use
50  * the module through an {@link ISoundTriggerModule} instance, obtained via {@link
51  * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared
52  * between sessions (i.e. cannot use a handle obtained from one session through another).
53  * <p>
54  * <b>Important conventions:</b>
55  * <ul>
56  * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle
57  * invalid usage, and such usage will result in undefined behavior. If this service is to be
58  * offered to an untrusted client, it must be wrapped with input and state validation.
59  * <li>The underlying driver is assumed to be correct. This implementation does not attempt to
60  * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this
61  * service is to used with an untrusted driver, the driver must be wrapped with validation / error
62  * recovery code.
63  * <li>Recovery from driver death is supported.</li>
64  * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not
65  * considered recoverable faults and should not occur in a properly functioning system.
66  * <li>There is no binder instance associated with this implementation. Do not call asBinder().
67  * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal,
68  * recoverable faults. The error code would one of the
69  * {@link android.media.soundtrigger.Status} constants. Any other exception thrown should be
70  * regarded as a bug in the implementation or one of its dependencies (assuming correct usage).
71  * <li>The implementation is designed for testability by featuring dependency injection (the
72  * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies
73  * on Android runtime.
74  * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry-
75  * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance
76  * for their entire scope. Any other method can be assumed to be running with the lock already
77  * obtained, so no further locking should be done. While this is not necessarily the most efficient
78  * synchronization strategy, it is very easy to reason about and this code is likely not on any
79  * performance-critical
80  * path.
81  * </ul>
82  *
83  * @hide
84  */
85 class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.GlobalCallback {
86     static private final String TAG = "SoundTriggerModule";
87     @NonNull private final HalFactory mHalFactory;
88     @NonNull private ISoundTriggerHal mHalService;
89     @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider;
90     private final Set<Session> mActiveSessions = new HashSet<>();
91     private Properties mProperties;
92 
93     /**
94      * Ctor.
95      *
96      * @param halFactory - A factory for the underlying HAL driver.
97      * @param audioSessionProvider - Creates a session token + device id/port pair used to
98      * associate recognition events with the audio stream used to access data.
99      */
SoundTriggerModule(@onNull HalFactory halFactory, @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider)100     SoundTriggerModule(@NonNull HalFactory halFactory,
101             @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) {
102         mHalFactory = Objects.requireNonNull(halFactory);
103         mAudioSessionProvider = Objects.requireNonNull(audioSessionProvider);
104 
105         attachToHal();
106     }
107 
108     /**
109      * Establish a client session with this module.
110      *
111      * This module may be shared by multiple clients, each will get its own session. While resources
112      * are shared between the clients, each session has its own state and data should not be shared
113      * across sessions.
114      *
115      * @param callback The client callback, which will be used for all messages. This is a oneway
116      *                 callback, so will never block, throw an unchecked exception or return a
117      *                 value.
118      * @return The interface through which this module can be controlled.
119      */
120     synchronized @NonNull
attach(@onNull ISoundTriggerCallback callback)121     ISoundTriggerModule attach(@NonNull ISoundTriggerCallback callback) {
122         Session session = new Session(callback);
123         mActiveSessions.add(session);
124         return session;
125     }
126 
127     /**
128      * Query the module's properties.
129      *
130      * @return The properties structure.
131      */
132     synchronized @NonNull
getProperties()133     Properties getProperties() {
134         return mProperties;
135     }
136 
137     @Override
binderDied()138     public void binderDied() {
139         Slog.w(TAG, "Underlying HAL driver died.");
140         List<ISoundTriggerCallback> callbacks;
141         synchronized (this) {
142             callbacks = new ArrayList<>(mActiveSessions.size());
143             for (Session session : mActiveSessions) {
144                 callbacks.add(session.moduleDied());
145             }
146             mActiveSessions.clear();
147             reset();
148         }
149         // Trigger the callbacks outside of the lock to avoid deadlocks.
150         for (ISoundTriggerCallback callback : callbacks) {
151             try {
152                 callback.onModuleDied();
153             } catch (RemoteException e) {
154                 throw e.rethrowAsRuntimeException();
155             }
156         }
157     }
158 
159     /**
160      * Resets the transient state of this object.
161      */
reset()162     private void reset() {
163         mHalService.detach();
164         attachToHal();
165     }
166 
167     /**
168      * Attached to the HAL service via factory.
169      */
attachToHal()170     private void attachToHal() {
171         mHalService = null;
172         while (mHalService == null) {
173             try {
174                 mHalService = new SoundTriggerHalEnforcer(
175                         new SoundTriggerHalWatchdog(
176                             new SoundTriggerDuplicateModelHandler(mHalFactory.create())));
177             } catch (RuntimeException e) {
178                 if (!(e.getCause() instanceof RemoteException)) {
179                     throw e;
180                 }
181             }
182         }
183         mHalService.linkToDeath(this);
184         mHalService.registerCallback(this);
185         mProperties = mHalService.getProperties();
186     }
187 
188     /**
189      * Remove session from the list of active sessions.
190      *
191      * @param session The session to remove.
192      */
removeSession(@onNull Session session)193     private void removeSession(@NonNull Session session) {
194         mActiveSessions.remove(session);
195     }
196 
197     @Override
onResourcesAvailable()198     public void onResourcesAvailable() {
199         List<ISoundTriggerCallback> callbacks;
200         synchronized (this) {
201             callbacks = new ArrayList<>(mActiveSessions.size());
202             for (Session session : mActiveSessions) {
203                 callbacks.add(session.mCallback);
204             }
205         }
206         // Trigger the callbacks outside of the lock to avoid deadlocks.
207         for (ISoundTriggerCallback callback : callbacks) {
208             try {
209                 callback.onResourcesAvailable();
210             } catch (RemoteException e) {
211                 throw e.rethrowAsRuntimeException();
212             }
213         }
214     }
215 
216     /** State of a single sound model. */
217     private enum ModelState {
218         /** Initial state, until load() is called. */
219         INIT,
220         /** Model is loaded, but recognition is not active. */
221         LOADED,
222         /** Model is loaded and recognition is active. */
223         ACTIVE
224     }
225 
226     /**
227      * A single client session with this module.
228      *
229      * This is the main interface used to interact with this module.
230      */
231     private class Session implements ISoundTriggerModule {
232         private ISoundTriggerCallback mCallback;
233         private final IBinder mToken = new Binder();
234         private final Map<Integer, Model> mLoadedModels = new HashMap<>();
235 
236         /**
237          * Ctor.
238          *
239          * @param callback The client callback interface.
240          */
Session(@onNull ISoundTriggerCallback callback)241         private Session(@NonNull ISoundTriggerCallback callback) {
242             mCallback = callback;
243             mHalService.clientAttached(mToken);
244         }
245 
246         @Override
detach()247         public void detach() {
248             synchronized (SoundTriggerModule.this) {
249                 if (mCallback == null) {
250                     return;
251                 }
252                 removeSession(this);
253                 mCallback = null;
254                 mHalService.clientDetached(mToken);
255             }
256         }
257 
258         @Override
loadModel(@onNull SoundModel model)259         public int loadModel(@NonNull SoundModel model) {
260             synchronized (SoundTriggerModule.this) {
261                 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession =
262                         mAudioSessionProvider.acquireSession();
263                 try {
264                     checkValid();
265                     Model loadedModel = new Model();
266                     return loadedModel.load(model, audioSession);
267                 } catch (Exception e) {
268                     // We must do this outside the lock, to avoid possible deadlocks with the remote
269                     // process that provides the audio sessions, which may also be calling into us.
270                     try {
271                         mAudioSessionProvider.releaseSession(audioSession.mSessionHandle);
272                     } catch (Exception ee) {
273                         Slog.e(TAG, "Failed to release session.", ee);
274                     }
275                     throw e;
276                 }
277             }
278         }
279 
280         @Override
loadPhraseModel(@onNull PhraseSoundModel model)281         public int loadPhraseModel(@NonNull PhraseSoundModel model) {
282             synchronized (SoundTriggerModule.this) {
283                 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession =
284                         mAudioSessionProvider.acquireSession();
285                 try {
286                     checkValid();
287                     Model loadedModel = new Model();
288                     int result = loadedModel.load(model, audioSession);
289                     Slog.d(TAG, String.format("loadPhraseModel()->%d", result));
290                     return result;
291                 } catch (Exception e) {
292                     // We must do this outside the lock, to avoid possible deadlocks with the remote
293                     // process that provides the audio sessions, which may also be calling into us.
294                     try {
295                         mAudioSessionProvider.releaseSession(audioSession.mSessionHandle);
296                     } catch (Exception ee) {
297                         Slog.e(TAG, "Failed to release session.", ee);
298                     }
299                     throw e;
300                 }
301             }
302         }
303 
304         @Override
unloadModel(int modelHandle)305         public void unloadModel(int modelHandle) {
306             synchronized (SoundTriggerModule.this) {
307                 int sessionId;
308                 checkValid();
309                 sessionId = mLoadedModels.get(modelHandle).unload();
310                 mAudioSessionProvider.releaseSession(sessionId);
311             }
312         }
313 
314         @Override
startRecognition(int modelHandle, @NonNull RecognitionConfig config)315         public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
316             synchronized (SoundTriggerModule.this) {
317                 checkValid();
318                 return mLoadedModels.get(modelHandle).startRecognition(config);
319             }
320         }
321 
322         @Override
stopRecognition(int modelHandle)323         public void stopRecognition(int modelHandle) {
324             Model model;
325             synchronized (SoundTriggerModule.this) {
326                 checkValid();
327                 model = mLoadedModels.get(modelHandle);
328             }
329             model.stopRecognition();
330         }
331 
332         @Override
forceRecognitionEvent(int modelHandle)333         public void forceRecognitionEvent(int modelHandle) {
334             synchronized (SoundTriggerModule.this) {
335                 checkValid();
336                 mLoadedModels.get(modelHandle).forceRecognitionEvent();
337             }
338         }
339 
340         @Override
setModelParameter(int modelHandle, int modelParam, int value)341         public void setModelParameter(int modelHandle, int modelParam, int value) {
342             synchronized (SoundTriggerModule.this) {
343                 checkValid();
344                 mLoadedModels.get(modelHandle).setParameter(modelParam, value);
345             }
346         }
347 
348         @Override
getModelParameter(int modelHandle, int modelParam)349         public int getModelParameter(int modelHandle, int modelParam) {
350             synchronized (SoundTriggerModule.this) {
351                 checkValid();
352                 return mLoadedModels.get(modelHandle).getParameter(modelParam);
353             }
354         }
355 
356         @Override
357         @Nullable
queryModelParameterSupport(int modelHandle, int modelParam)358         public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
359             synchronized (SoundTriggerModule.this) {
360                 checkValid();
361                 return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam);
362             }
363         }
364 
365         /**
366          * The underlying module HAL is dead.
367          * @return The client callback that needs to be invoked to notify the client.
368          */
moduleDied()369         private ISoundTriggerCallback moduleDied() {
370             ISoundTriggerCallback callback = mCallback;
371             mCallback = null;
372             return callback;
373         }
374 
checkValid()375         private void checkValid() {
376             if (mCallback == null) {
377                 throw new RecoverableException(Status.DEAD_OBJECT);
378             }
379         }
380 
381         @Override
382         public @NonNull
asBinder()383         IBinder asBinder() {
384             throw new UnsupportedOperationException(
385                     "This implementation is not intended to be used directly with Binder.");
386         }
387 
388         /**
389          * A single sound model in the system.
390          *
391          * All model-based operations are delegated to this class and implemented here.
392          */
393         private class Model implements ISoundTriggerHal.ModelCallback {
394             public int mHandle;
395             private ModelState mState = ModelState.INIT;
396             private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession;
397             private IBinder mRecognitionToken = null;
398             private boolean mIsStopping = false;
399 
400             private @NonNull
getState()401             ModelState getState() {
402                 return mState;
403             }
404 
setState(@onNull ModelState state)405             private void setState(@NonNull ModelState state) {
406                 mState = state;
407                 SoundTriggerModule.this.notifyAll();
408             }
409 
load(@onNull SoundModel model, SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession)410             private int load(@NonNull SoundModel model,
411                     SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) {
412                 mSession = audioSession;
413                 mHandle = mHalService.loadSoundModel(model, this);
414                 setState(ModelState.LOADED);
415                 mLoadedModels.put(mHandle, this);
416                 return mHandle;
417             }
418 
load(@onNull PhraseSoundModel model, SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession)419             private int load(@NonNull PhraseSoundModel model,
420                     SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) {
421                 mSession = audioSession;
422                 mHandle = mHalService.loadPhraseSoundModel(model, this);
423 
424                 setState(ModelState.LOADED);
425                 mLoadedModels.put(mHandle, this);
426                 return mHandle;
427             }
428 
429             /**
430              * Unloads the model.
431              * @return The audio session handle.
432              */
unload()433             private int unload() {
434                 mHalService.unloadSoundModel(mHandle);
435                 mLoadedModels.remove(mHandle);
436                 return mSession.mSessionHandle;
437             }
438 
startRecognition(@onNull RecognitionConfig config)439             private IBinder startRecognition(@NonNull RecognitionConfig config) {
440                 if (mIsStopping == true) {
441                     throw new RecoverableException(Status.INTERNAL_ERROR, "Race occurred");
442                 }
443                 mHalService.startRecognition(mHandle, mSession.mDeviceHandle,
444                         mSession.mIoHandle, config);
445                 mRecognitionToken = new Binder();
446                 setState(ModelState.ACTIVE);
447                 return mRecognitionToken;
448             }
449 
stopRecognition()450             private void stopRecognition() {
451                 synchronized (SoundTriggerModule.this) {
452                     if (getState() == ModelState.LOADED) {
453                         // This call is idempotent in order to avoid races.
454                         return;
455                     }
456                     mRecognitionToken = null;
457                     mIsStopping = true;
458                 }
459                 mHalService.stopRecognition(mHandle);
460                 synchronized (SoundTriggerModule.this) {
461                     mIsStopping = false;
462                     setState(ModelState.LOADED);
463                 }
464             }
465 
466             /** Request a forced recognition event. Will do nothing if recognition is inactive. */
forceRecognitionEvent()467             private void forceRecognitionEvent() {
468                 if (getState() != ModelState.ACTIVE) {
469                     // This call is idempotent in order to avoid races.
470                     return;
471                 }
472                 mHalService.forceRecognitionEvent(mHandle);
473             }
474 
475 
setParameter(int modelParam, int value)476             private void setParameter(int modelParam, int value) {
477                 mHalService.setModelParameter(mHandle,
478                         ConversionUtil.aidl2hidlModelParameter(modelParam), value);
479             }
480 
getParameter(int modelParam)481             private int getParameter(int modelParam) {
482                 return mHalService.getModelParameter(mHandle,
483                         ConversionUtil.aidl2hidlModelParameter(modelParam));
484             }
485 
486             @Nullable
queryModelParameterSupport(int modelParam)487             private ModelParameterRange queryModelParameterSupport(int modelParam) {
488                 return mHalService.queryParameter(mHandle, modelParam);
489             }
490 
491             @Override
recognitionCallback(int modelHandle, @NonNull RecognitionEventSys event)492             public void recognitionCallback(int modelHandle,
493                     @NonNull RecognitionEventSys event) {
494                 ISoundTriggerCallback callback;
495                 synchronized (SoundTriggerModule.this) {
496                     if (mRecognitionToken == null) {
497                         return;
498                     }
499                     event.token = mRecognitionToken;
500                     if (!event.recognitionEvent.recognitionStillActive) {
501                         setState(ModelState.LOADED);
502                         mRecognitionToken = null;
503                     }
504                     callback = mCallback;
505                 }
506                 // The callback must be invoked outside of the lock.
507                 try {
508                     if (callback != null) {
509                         callback.onRecognition(mHandle, event, mSession.mSessionHandle);
510                     }
511                 } catch (RemoteException e) {
512                     // We're not expecting any exceptions here.
513                     throw e.rethrowAsRuntimeException();
514                 }
515             }
516 
517             @Override
phraseRecognitionCallback(int modelHandle, @NonNull PhraseRecognitionEventSys event)518             public void phraseRecognitionCallback(int modelHandle,
519                     @NonNull PhraseRecognitionEventSys event) {
520                 ISoundTriggerCallback callback;
521                 synchronized (SoundTriggerModule.this) {
522                     if (mRecognitionToken == null) {
523                         return;
524                     }
525                     event.token = mRecognitionToken;
526                     if (!event.phraseRecognitionEvent.common.recognitionStillActive) {
527                         setState(ModelState.LOADED);
528                         mRecognitionToken = null;
529                     }
530                     callback = mCallback;
531                 }
532                 // The callback must be invoked outside of the lock.
533                 try {
534                     if (callback != null) {
535                         mCallback.onPhraseRecognition(mHandle, event, mSession.mSessionHandle);
536                     }
537                 } catch (RemoteException e) {
538                     // We're not expecting any exceptions here.
539                     throw e.rethrowAsRuntimeException();
540                 }
541             }
542 
543             @Override
modelUnloaded(int modelHandle)544             public void modelUnloaded(int modelHandle) {
545                 ISoundTriggerCallback callback;
546                 synchronized (SoundTriggerModule.this) {
547                     callback = mCallback;
548                 }
549 
550                 // The callback must be invoked outside of the lock.
551                 try {
552                     if (callback != null) {
553                         callback.onModelUnloaded(modelHandle);
554                     }
555                 } catch (RemoteException e) {
556                     // We're not expecting any exceptions here.
557                     throw e.rethrowAsRuntimeException();
558                 }
559             }
560         }
561     }
562 }
563