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