1 /*
2  * Copyright (C) 2020 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.permission.Identity;
22 import android.media.permission.IdentityContext;
23 import android.media.soundtrigger.ModelParameterRange;
24 import android.media.soundtrigger.PhraseSoundModel;
25 import android.media.soundtrigger.Properties;
26 import android.media.soundtrigger.RecognitionConfig;
27 import android.media.soundtrigger.RecognitionStatus;
28 import android.media.soundtrigger.SoundModel;
29 import android.media.soundtrigger.Status;
30 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
31 import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
32 import android.media.soundtrigger_middleware.ISoundTriggerModule;
33 import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
34 import android.media.soundtrigger_middleware.RecognitionEventSys;
35 import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
36 import android.os.IBinder;
37 import android.os.RemoteException;
38 import android.os.ServiceSpecificException;
39 import android.util.Slog;
40 import android.util.SparseArray;
41 
42 import com.android.internal.util.Preconditions;
43 
44 import java.io.PrintWriter;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.Set;
50 
51 /**
52  * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces correct usage by
53  * the client, as well as makes sure that exceptions representing a server malfunction get sent to
54  * the client in a consistent manner, which cannot be confused with a client fault.
55  * <p>
56  * This is intended to extract the non-business logic out of the underlying implementation and thus
57  * make it easier to maintain each one of those separate aspects. A design trade-off is being made
58  * here, in that this class would need to essentially eavesdrop on all the client-server
59  * communication and retain all state known to the client, while the client doesn't necessarily care
60  * about all of it, and while the server has its own representation of this information. However,
61  * in this case, this is a small amount of data, and the benefits in code elegance seem worth it.
62  * There is also some additional cost in employing a simplistic locking mechanism here, but
63  * following the same line of reasoning, the benefits in code simplicity outweigh it.
64  * <p>
65  * Every public method in this class, overriding an interface method, must follow the following
66  * pattern:
67  * <code><pre>
68  * @Override public T method(S arg) {
69  *     // Input validation.
70  *     ValidationUtil.validateS(arg);
71  *     synchronized (this) {
72  *         // State validation.
73  *         if (...state is not valid for this call...) {
74  *             throw new IllegalStateException("State is invalid because...");
75  *         }
76  *         // From here on, every exception isn't client's fault.
77  *         try {
78  *             T result = mDelegate.method(arg);
79  *             // Update state.;
80  *             ...
81  *             return result;
82  *         } catch (Exception e) {
83  *             throw handleException(e);
84  *         }
85  *     }
86  * }
87  * </pre></code>
88  * Following this patterns ensures a consistent and rigorous handling of all aspects associated
89  * with client-server separation. Notable exceptions are stopRecognition() and unloadModel(), which
90  * follow slightly more complicated rules for synchronization (see README.md for details).
91  * <p>
92  * <b>Exception handling approach:</b><br>
93  * We make sure all client faults (argument and state validation) happen first, and
94  * would throw {@link IllegalArgumentException}/{@link NullPointerException} or {@link
95  * IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
96  * will get sent back to the client.<br>
97  * Once this is done, any subsequent fault is considered either a recoverable (expected) or
98  * unexpected server fault. Those will be delivered to the client as a
99  * {@link ServiceSpecificException}. {@link RecoverableException}s thrown by the implementation are
100  * considered recoverable and will include a specific error code to indicate the problem. Any other
101  * exceptions will use the INTERNAL_ERROR code. They may also cause the module to become invalid
102  * asynchronously, and the client would be notified via the moduleDied() callback.
103  *
104  * {@hide}
105  */
106 public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddlewareInternal, Dumpable {
107     private static final String TAG = "SoundTriggerMiddlewareValidation";
108 
109     private enum ModuleStatus {
110         ALIVE,
111         DETACHED,
112         DEAD
113     }
114 
115     private class ModuleState {
116         public @NonNull Properties properties;
117         public Set<Session> sessions = new HashSet<>();
118 
ModuleState(@onNull Properties properties)119         private ModuleState(@NonNull Properties properties) {
120             this.properties = properties;
121         }
122     }
123 
124     private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
125     private Map<Integer, ModuleState> mModules;
126 
SoundTriggerMiddlewareValidation(@onNull ISoundTriggerMiddlewareInternal delegate)127     public SoundTriggerMiddlewareValidation(@NonNull ISoundTriggerMiddlewareInternal delegate) {
128         mDelegate = delegate;
129     }
130 
131     /**
132      * Generic exception handling for exceptions thrown by the underlying implementation.
133      *
134      * Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed
135      * by Binder to the caller) and <i>any other</i> exception as a {@link ServiceSpecificException}
136      * with a {@link Status#INTERNAL_ERROR} code.
137      * <p>
138      * Typical usage:
139      * <code><pre>
140      * try {
141      *     ... Do server operations ...
142      * } catch (Exception e) {
143      *     throw handleException(e);
144      * }
145      * </pre></code>
146      */
handleException(@onNull Exception e)147     static @NonNull RuntimeException handleException(@NonNull Exception e) {
148         if (e instanceof RecoverableException) {
149             throw new ServiceSpecificException(((RecoverableException) e).errorCode,
150                     e.getMessage());
151         }
152 
153         Slog.wtf(TAG, "Unexpected exception", e);
154         throw new ServiceSpecificException(Status.INTERNAL_ERROR, e.getMessage());
155     }
156 
157     @Override
listModules()158     public @NonNull SoundTriggerModuleDescriptor[] listModules() {
159         // Input validation (always valid).
160 
161         synchronized (this) {
162             // State validation (always valid).
163 
164             // From here on, every exception isn't client's fault.
165             try {
166                 SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
167                 if (mModules == null) {
168                     mModules = new HashMap<>(result.length);
169                     for (SoundTriggerModuleDescriptor desc : result) {
170                         mModules.put(desc.handle, new ModuleState(desc.properties));
171                     }
172                 } else {
173                     if (result.length != mModules.size()) {
174                         throw new RuntimeException(
175                                 "listModules must always return the same result.");
176                     }
177                     for (SoundTriggerModuleDescriptor desc : result) {
178                         if (!mModules.containsKey(desc.handle)) {
179                             throw new RuntimeException(
180                                     "listModules must always return the same result.");
181                         }
182                         mModules.get(desc.handle).properties = desc.properties;
183                     }
184                 }
185                 return result;
186             } catch (Exception e) {
187                 throw handleException(e);
188             }
189         }
190     }
191 
192     @Override
attach(int handle, @NonNull ISoundTriggerCallback callback, boolean isTrusted)193     public @NonNull ISoundTriggerModule attach(int handle,
194             @NonNull ISoundTriggerCallback callback, boolean isTrusted) {
195         // Input validation.
196         Objects.requireNonNull(callback);
197         Objects.requireNonNull(callback.asBinder());
198 
199         synchronized (this) {
200             // State validation.
201             if (mModules == null) {
202                 throw new IllegalStateException(
203                         "Client must call listModules() prior to attaching.");
204             }
205             if (!mModules.containsKey(handle)) {
206                 throw new IllegalArgumentException("Invalid handle: " + handle);
207             }
208 
209             // From here on, every exception isn't client's fault.
210             try {
211                 Session session = new Session(handle, callback);
212                 session.attach(mDelegate.attach(handle, session.getCallbackWrapper(), isTrusted));
213                 return session;
214             } catch (Exception e) {
215                 throw handleException(e);
216             }
217         }
218     }
219 
220     // Override toString() in order to have the delegate's ID in it.
221     @Override
toString()222     public String toString() {
223         return mDelegate.toString();
224     }
225 
226     @Override
dump(PrintWriter pw)227     public void dump(PrintWriter pw) {
228         synchronized (this) {
229             if (mModules != null) {
230                 for (int handle : mModules.keySet()) {
231                     final ModuleState module = mModules.get(handle);
232                     pw.println("=========================================");
233                     pw.printf("Module %d\n%s\n", handle,
234                             ObjectPrinter.print(module.properties, 16));
235                     pw.println("=========================================");
236                     for (Session session : module.sessions) {
237                         session.dump(pw);
238                     }
239                 }
240             } else {
241                 pw.println("Modules have not yet been enumerated.");
242             }
243         }
244         pw.println();
245 
246         if (mDelegate instanceof Dumpable) {
247             ((Dumpable) mDelegate).dump(pw);
248         }
249     }
250 
251     /** State of a sound model. */
252     static class ModelState {
ModelState(SoundModel model)253         ModelState(SoundModel model) {
254             this.description = ObjectPrinter.print(model, 16);
255         }
256 
ModelState(PhraseSoundModel model)257         ModelState(PhraseSoundModel model) {
258             this.description = ObjectPrinter.print(model, 16);
259         }
260 
261         /** Activity state of a sound model. */
262         enum Activity {
263             /** Model is loaded, recognition is inactive. */
264             LOADED,
265             /** Model is loaded, recognition is active. */
266             ACTIVE,
267             /**
268              * Model has been preemptively unloaded by the HAL.
269              */
270             PREEMPTED,
271         }
272 
273         /** Activity state. */
274         Activity activityState = Activity.LOADED;
275 
276         /** Recognition config, used to start the model. */
277         RecognitionConfig config;
278 
279         /** Human-readable description of the model. */
280         final String description;
281 
282         /**
283          * A map of known parameter support. A missing key means we don't know yet whether the
284          * parameter is supported. A null value means it is known to not be supported. A non-null
285          * value indicates the valid value range.
286          */
287         private final Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
288 
289         /**
290          * Check that the given parameter is known to be supported for this model.
291          *
292          * @param modelParam The parameter key.
293          */
checkSupported(int modelParam)294         void checkSupported(int modelParam) {
295             if (!parameterSupport.containsKey(modelParam)) {
296                 throw new IllegalStateException("Parameter has not been checked for support.");
297             }
298             ModelParameterRange range = parameterSupport.get(modelParam);
299             if (range == null) {
300                 throw new IllegalArgumentException("Paramater is not supported.");
301             }
302         }
303 
304         /**
305          * Check that the given parameter is known to be supported for this model and that the given
306          * value is a valid value for it.
307          *
308          * @param modelParam The parameter key.
309          * @param value      The value.
310          */
checkSupported(int modelParam, int value)311         void checkSupported(int modelParam, int value) {
312             if (!parameterSupport.containsKey(modelParam)) {
313                 throw new IllegalStateException("Parameter has not been checked for support.");
314             }
315             ModelParameterRange range = parameterSupport.get(modelParam);
316             if (range == null) {
317                 throw new IllegalArgumentException("Paramater is not supported.");
318             }
319             Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive,
320                     "value");
321         }
322     }
323 
324     /**
325      * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
326      * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
327      */
328     private class Session extends ISoundTriggerModule.Stub {
329         private ISoundTriggerModule mDelegate;
330         private final @NonNull Map<Integer, ModelState> mLoadedModels = new HashMap<>();
331         private final int mHandle;
332         private ModuleStatus mState = ModuleStatus.ALIVE;
333         private final CallbackWrapper mCallbackWrapper;
334         private final Identity mOriginatorIdentity;
335 
Session(int handle, @NonNull ISoundTriggerCallback callback)336         Session(int handle, @NonNull ISoundTriggerCallback callback) {
337             mCallbackWrapper = new CallbackWrapper(callback);
338             mHandle = handle;
339             mOriginatorIdentity = IdentityContext.get();
340         }
341 
getCallbackWrapper()342         ISoundTriggerCallback getCallbackWrapper() {
343             return mCallbackWrapper;
344         }
345 
attach(@onNull ISoundTriggerModule delegate)346         void attach(@NonNull ISoundTriggerModule delegate) {
347             mDelegate = delegate;
348             mModules.get(mHandle).sessions.add(this);
349         }
350 
351         @Override
loadModel(@onNull SoundModel model)352         public int loadModel(@NonNull SoundModel model) {
353             // Input validation.
354             ValidationUtil.validateGenericModel(model);
355 
356             synchronized (SoundTriggerMiddlewareValidation.this) {
357                 // State validation.
358                 if (mState == ModuleStatus.DETACHED) {
359                     throw new IllegalStateException("Module has been detached.");
360                 }
361 
362                 // From here on, every exception isn't client's fault.
363                 try {
364                     int handle = mDelegate.loadModel(model);
365                     mLoadedModels.put(handle, new ModelState(model));
366                     return handle;
367                 } catch (Exception e) {
368                     throw handleException(e);
369                 }
370             }
371         }
372 
373         @Override
loadPhraseModel(@onNull PhraseSoundModel model)374         public int loadPhraseModel(@NonNull PhraseSoundModel model) {
375             // Input validation.
376             ValidationUtil.validatePhraseModel(model);
377 
378             synchronized (SoundTriggerMiddlewareValidation.this) {
379                 // State validation.
380                 if (mState == ModuleStatus.DETACHED) {
381                     throw new IllegalStateException("Module has been detached.");
382                 }
383 
384                 // From here on, every exception isn't client's fault.
385                 try {
386                     int handle = mDelegate.loadPhraseModel(model);
387                     mLoadedModels.put(handle, new ModelState(model));
388                     return handle;
389                 } catch (Exception e) {
390                     throw handleException(e);
391                 }
392             }
393         }
394 
395         @Override
unloadModel(int modelHandle)396         public void unloadModel(int modelHandle) {
397             // Input validation (always valid).
398             synchronized (SoundTriggerMiddlewareValidation.this) {
399                 // State validation.
400                 if (mState == ModuleStatus.DETACHED) {
401                     throw new IllegalStateException("Module has been detached.");
402                 }
403                 ModelState modelState = mLoadedModels.get(
404                         modelHandle);
405                 if (modelState == null) {
406                     throw new IllegalStateException("Invalid handle: " + modelHandle);
407                 }
408                 // To avoid race conditions, we treat LOADED and PREEMPTED exactly the same.
409                 if (modelState.activityState != ModelState.Activity.LOADED
410                         && modelState.activityState != ModelState.Activity.PREEMPTED) {
411                     throw new IllegalStateException("Model with handle: " + modelHandle
412                             + " has invalid state for unloading");
413                 }
414             }
415 
416             // From here on, every exception isn't client's fault.
417             try {
418                 // Calling the delegate must be done outside the lock.
419                 mDelegate.unloadModel(modelHandle);
420             } catch (Exception e) {
421                 throw handleException(e);
422             }
423 
424             synchronized (SoundTriggerMiddlewareValidation.this) {
425                 mLoadedModels.remove(modelHandle);
426             }
427         }
428 
429         @Override
startRecognition(int modelHandle, @NonNull RecognitionConfig config)430         public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
431             // Input validation.
432             ValidationUtil.validateRecognitionConfig(config);
433 
434             synchronized (SoundTriggerMiddlewareValidation.this) {
435                 // State validation.
436                 if (mState == ModuleStatus.DETACHED) {
437                     throw new IllegalStateException("Module has been detached.");
438                 }
439                 ModelState modelState = mLoadedModels.get(
440                         modelHandle);
441                 if (modelState == null) {
442                     throw new IllegalStateException("Invalid handle: " + modelHandle);
443                 }
444                 ModelState.Activity activityState = modelState.activityState;
445                 // To avoid race conditions, we treat LOADED and PREEMPTED exactly the same.
446                 if (activityState != ModelState.Activity.LOADED
447                         && activityState != ModelState.Activity.PREEMPTED) {
448                     throw new IllegalStateException("Model with handle: " + modelHandle
449                             + " has invalid state for starting recognition");
450                 }
451 
452                 // From here on, every exception isn't client's fault.
453                 try {
454                     var result = mDelegate.startRecognition(modelHandle, config);
455                     modelState.config = config;
456                     modelState.activityState = ModelState.Activity.ACTIVE;
457                     return result;
458                 } catch (Exception e) {
459                     throw handleException(e);
460                 }
461             }
462         }
463 
464         @Override
stopRecognition(int modelHandle)465         public void stopRecognition(int modelHandle) {
466             // Input validation (always valid).
467 
468             synchronized (SoundTriggerMiddlewareValidation.this) {
469                 // State validation.
470                 if (mState == ModuleStatus.DETACHED) {
471                     throw new IllegalStateException("Module has been detached.");
472                 }
473                 ModelState modelState = mLoadedModels.get(
474                         modelHandle);
475                 if (modelState == null) {
476                     throw new IllegalStateException("Invalid handle: " + modelHandle);
477                 }
478                 // stopRecognition is idempotent - no need to check model state.
479             }
480 
481             // Calling the delegate's stop must be done without the lock.
482             try {
483                 mDelegate.stopRecognition(modelHandle);
484             } catch (Exception e) {
485                 throw handleException(e);
486             }
487 
488             synchronized (SoundTriggerMiddlewareValidation.this) {
489                 ModelState modelState = mLoadedModels.get(modelHandle);
490                 if (modelState == null) {
491                     // The model was unloaded while we let go of the lock.
492                     return;
493                 }
494 
495                 // After the call, the state is LOADED, unless it has been first preempted.
496                 if (modelState.activityState != ModelState.Activity.PREEMPTED) {
497                     modelState.activityState = ModelState.Activity.LOADED;
498                 }
499             }
500         }
501 
502         @Override
forceRecognitionEvent(int modelHandle)503         public void forceRecognitionEvent(int modelHandle) {
504             // Input validation (always valid).
505 
506             synchronized (SoundTriggerMiddlewareValidation.this) {
507                 // State validation.
508                 if (mState == ModuleStatus.DETACHED) {
509                     throw new IllegalStateException("Module has been detached.");
510                 }
511                 ModelState modelState = mLoadedModels.get(
512                         modelHandle);
513                 if (modelState == null) {
514                     throw new IllegalStateException("Invalid handle: " + modelHandle);
515                 }
516                 // forceRecognitionEvent is idempotent - no need to check model state.
517 
518                 // From here on, every exception isn't client's fault.
519                 try {
520                     // If the activity state is LOADED or INTERCEPTED, we skip delegating the
521                     // command, but still consider the call valid.
522                     if (modelState.activityState == ModelState.Activity.ACTIVE) {
523                         mDelegate.forceRecognitionEvent(modelHandle);
524                     }
525                 } catch (Exception e) {
526                     throw handleException(e);
527                 }
528             }
529         }
530 
531         @Override
setModelParameter(int modelHandle, int modelParam, int value)532         public void setModelParameter(int modelHandle, int modelParam, int value) {
533             // Input validation.
534             ValidationUtil.validateModelParameter(modelParam);
535 
536             synchronized (SoundTriggerMiddlewareValidation.this) {
537                 // State validation.
538                 if (mState == ModuleStatus.DETACHED) {
539                     throw new IllegalStateException("Module has been detached.");
540                 }
541                 ModelState modelState = mLoadedModels.get(
542                         modelHandle);
543                 if (modelState == null) {
544                     throw new IllegalStateException("Invalid handle: " + modelHandle);
545                 }
546                 modelState.checkSupported(modelParam, value);
547 
548                 // From here on, every exception isn't client's fault.
549                 try {
550                     mDelegate.setModelParameter(modelHandle, modelParam, value);
551                 } catch (Exception e) {
552                     throw handleException(e);
553                 }
554             }
555         }
556 
557         @Override
getModelParameter(int modelHandle, int modelParam)558         public int getModelParameter(int modelHandle, int modelParam) {
559             // Input validation.
560             ValidationUtil.validateModelParameter(modelParam);
561 
562             synchronized (SoundTriggerMiddlewareValidation.this) {
563                 // State validation.
564                 if (mState == ModuleStatus.DETACHED) {
565                     throw new IllegalStateException("Module has been detached.");
566                 }
567                 ModelState modelState = mLoadedModels.get(
568                         modelHandle);
569                 if (modelState == null) {
570                     throw new IllegalStateException("Invalid handle: " + modelHandle);
571                 }
572                 modelState.checkSupported(modelParam);
573 
574                 // From here on, every exception isn't client's fault.
575                 try {
576                     return mDelegate.getModelParameter(modelHandle, modelParam);
577                 } catch (Exception e) {
578                     throw handleException(e);
579                 }
580             }
581         }
582 
583         @Override
584         @Nullable
queryModelParameterSupport(int modelHandle, int modelParam)585         public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
586             // Input validation.
587             ValidationUtil.validateModelParameter(modelParam);
588 
589             synchronized (SoundTriggerMiddlewareValidation.this) {
590                 // State validation.
591                 if (mState == ModuleStatus.DETACHED) {
592                     throw new IllegalStateException("Module has been detached.");
593                 }
594                 ModelState modelState = mLoadedModels.get(
595                         modelHandle);
596                 if (modelState == null) {
597                     throw new IllegalStateException("Invalid handle: " + modelHandle);
598                 }
599 
600                 // From here on, every exception isn't client's fault.
601                 try {
602                     ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
603                             modelParam);
604                     modelState.parameterSupport.put(modelParam, result);
605                     return result;
606                 } catch (Exception e) {
607                     throw handleException(e);
608                 }
609             }
610         }
611 
612         @Override
detach()613         public void detach() {
614             // Input validation (always valid).
615 
616             synchronized (SoundTriggerMiddlewareValidation.this) {
617                 // State validation.
618                 if (mState == ModuleStatus.DETACHED) {
619                     throw new IllegalStateException("Module has already been detached.");
620                 }
621                 if (mState == ModuleStatus.ALIVE && !mLoadedModels.isEmpty()) {
622                     throw new IllegalStateException("Cannot detach while models are loaded.");
623                 }
624 
625                 // From here on, every exception isn't client's fault.
626                 try {
627                     detachInternal();
628                 } catch (Exception e) {
629                     throw handleException(e);
630                 }
631             }
632         }
633 
634         // Override toString() in order to have the delegate's ID in it.
635         @Override
toString()636         public String toString() {
637             return Objects.toString(mDelegate);
638         }
639 
detachInternal()640         private void detachInternal() {
641             try {
642                 mDelegate.detach();
643                 mState = ModuleStatus.DETACHED;
644                 mCallbackWrapper.detached();
645                 mModules.get(mHandle).sessions.remove(this);
646             } catch (RemoteException e) {
647                 throw e.rethrowAsRuntimeException();
648             }
649         }
650 
dump(PrintWriter pw)651         void dump(PrintWriter pw) {
652             if (mState == ModuleStatus.ALIVE) {
653                 pw.println("-------------------------------");
654                 pw.printf("Session %s, client: %s\n", toString(),
655                         ObjectPrinter.print(mOriginatorIdentity, 16));
656                 pw.println("Loaded models (handle, active, description):");
657                 pw.println();
658                 pw.println("-------------------------------");
659                 for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
660                     pw.print(entry.getKey());
661                     pw.print('\t');
662                     pw.print(entry.getValue().activityState.name());
663                     pw.print('\t');
664                     pw.print(entry.getValue().description);
665                     pw.println();
666                 }
667                 pw.println();
668             } else {
669                 pw.printf("Session %s is dead", toString());
670                 pw.println();
671             }
672         }
673 
674         class CallbackWrapper implements ISoundTriggerCallback, IBinder.DeathRecipient {
675             private final ISoundTriggerCallback mCallback;
676 
CallbackWrapper(ISoundTriggerCallback callback)677             CallbackWrapper(ISoundTriggerCallback callback) {
678                 mCallback = callback;
679                 try {
680                     mCallback.asBinder().linkToDeath(this, 0);
681                 } catch (RemoteException e) {
682                     throw e.rethrowAsRuntimeException();
683                 }
684             }
685 
detached()686             void detached() {
687                 mCallback.asBinder().unlinkToDeath(this, 0);
688             }
689 
690             @Override
onRecognition(int modelHandle, @NonNull RecognitionEventSys event, int captureSession)691             public void onRecognition(int modelHandle, @NonNull RecognitionEventSys event,
692                     int captureSession) {
693                 synchronized (SoundTriggerMiddlewareValidation.this) {
694                     ModelState modelState = mLoadedModels.get(modelHandle);
695                     if (!event.recognitionEvent.recognitionStillActive) {
696                         modelState.activityState = ModelState.Activity.LOADED;
697                     }
698                 }
699 
700                 // Calling the delegate callback must be done outside the lock.
701                 try {
702                     mCallback.onRecognition(modelHandle, event, captureSession);
703                 } catch (Exception e) {
704                     Slog.w(TAG, "Client callback exception.", e);
705                }
706             }
707 
708             @Override
onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEventSys event, int captureSession)709             public void onPhraseRecognition(int modelHandle,
710                     @NonNull PhraseRecognitionEventSys event, int captureSession) {
711                 synchronized (SoundTriggerMiddlewareValidation.this) {
712                     ModelState modelState = mLoadedModels.get(modelHandle);
713                     if (!event.phraseRecognitionEvent.common.recognitionStillActive) {
714                         modelState.activityState = ModelState.Activity.LOADED;
715                     }
716                 }
717 
718                 // Calling the delegate callback must be done outside the lock.
719                 try {
720                     mCallback.onPhraseRecognition(modelHandle, event, captureSession);
721                 } catch (Exception e) {
722                     Slog.w(TAG, "Client callback exception.", e);
723                }
724             }
725 
726             @Override
onModelUnloaded(int modelHandle)727             public void onModelUnloaded(int modelHandle) {
728                 synchronized (SoundTriggerMiddlewareValidation.this) {
729                     ModelState modelState = mLoadedModels.get(modelHandle);
730                     modelState.activityState = ModelState.Activity.PREEMPTED;
731                 }
732 
733                 // Calling the delegate callback must be done outside the lock.
734                 try {
735                     mCallback.onModelUnloaded(modelHandle);
736                 } catch (Exception e) {
737                     Slog.w(TAG, "Client callback exception.", e);
738                 }
739             }
740 
741             @Override
onResourcesAvailable()742             public void onResourcesAvailable() {
743                 // Not locking to avoid deadlocks (not affecting any state).
744                 try {
745                     mCallback.onResourcesAvailable();
746                 } catch (RemoteException e) {
747                     // Dead client will be handled by binderDied() - no need to handle here.
748                     // In any case, client callbacks are considered best effort.
749                     Slog.e(TAG, "Client callback exception.", e);
750                 }
751             }
752 
753             @Override
onModuleDied()754             public void onModuleDied() {
755                 synchronized (SoundTriggerMiddlewareValidation.this) {
756                     mState = ModuleStatus.DEAD;
757                 }
758                 // Trigger the callback outside of the lock to avoid deadlocks.
759                 try {
760                     mCallback.onModuleDied();
761                 } catch (RemoteException e) {
762                     // Dead client will be handled by binderDied() - no need to handle here.
763                     // In any case, client callbacks are considered best effort.
764                     Slog.e(TAG, "Client callback exception.", e);
765                 }
766             }
767 
768             @Override
binderDied()769             public void binderDied() {
770                 // This is called whenever our client process dies.
771                 SparseArray<ModelState.Activity> cachedMap =
772                         new SparseArray<ModelState.Activity>();
773                 synchronized (SoundTriggerMiddlewareValidation.this) {
774                         // Copy the relevant state under the lock, so we can call back without
775                         // holding a lock. This exposes us to a potential race, but the client is
776                         // dead so we don't expect one.
777                         // TODO(240613068) A more resilient fix for this.
778                         for (Map.Entry<Integer, ModelState> entry :
779                                 mLoadedModels.entrySet()) {
780                             cachedMap.put(entry.getKey(), entry.getValue().activityState);
781                         }
782                 }
783                 try {
784                     // Gracefully stop all active recognitions and unload the models.
785                     for (int i = 0; i < cachedMap.size(); i++) {
786                         if (cachedMap.valueAt(i) == ModelState.Activity.ACTIVE) {
787                             mDelegate.stopRecognition(cachedMap.keyAt(i));
788                         }
789                         mDelegate.unloadModel(cachedMap.keyAt(i));
790                     }
791                 } catch (Exception e) {
792                     throw handleException(e);
793                 }
794                 synchronized (SoundTriggerMiddlewareValidation.this) {
795                    // Check if state updated unexpectedly to log race conditions.
796                     for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
797                         if (cachedMap.get(entry.getKey()) != entry.getValue().activityState) {
798                             Slog.e(TAG, "Unexpected state update in binderDied. Race occurred!");
799                         }
800                     }
801                     if (mLoadedModels.size() != cachedMap.size()) {
802                         Slog.e(TAG, "Unexpected state update in binderDied. Race occurred!");
803                     }
804                     try {
805                         // Detach
806                         detachInternal();
807                     } catch (Exception e) {
808                         throw handleException(e);
809                     }
810                 }
811             }
812 
813             @Override
asBinder()814             public IBinder asBinder() {
815                 return mCallback.asBinder();
816             }
817 
818             // Override toString() in order to have the delegate's ID in it.
819             @Override
toString()820             public String toString() {
821                 return Objects.toString(mDelegate);
822             }
823         }
824     }
825 }
826