1 /* 2 * Copyright (C) 2023 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 android.soundtrigger.cts.instrumentation; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 22 import android.media.soundtrigger.SoundTriggerInstrumentation; 23 import android.media.soundtrigger.SoundTriggerInstrumentation.GlobalCallback; 24 import android.media.soundtrigger.SoundTriggerInstrumentation.ModelCallback; 25 import android.media.soundtrigger.SoundTriggerInstrumentation.ModelSession; 26 import android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionCallback; 27 import android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionSession; 28 import android.media.soundtrigger.SoundTriggerManager; 29 import android.util.Log; 30 31 import androidx.annotation.GuardedBy; 32 import androidx.annotation.NonNull; 33 34 import com.google.common.util.concurrent.Futures; 35 import com.google.common.util.concurrent.ListenableFuture; 36 import com.google.common.util.concurrent.SettableFuture; 37 38 import java.util.concurrent.Executor; 39 import java.util.concurrent.Executors; 40 41 /** 42 * Supporting class to observe and attach to the SoundTrigger HAL via 43 * {@link SoundTriggerInstrumentation} 44 */ 45 public class SoundTriggerInstrumentationObserver implements AutoCloseable { 46 private static final String TAG = SoundTriggerInstrumentationObserver.class.getSimpleName(); 47 48 /** 49 * Observer class exposing {@link ListenableFuture}'s on the {@link GlobalCallback} interface 50 * 51 * <p>This value gets replaced via {@link #attachInstrumentation} 52 */ 53 private GlobalCallbackObserver mGlobalCallbackObserver; 54 55 /** 56 * Attaches to the SoundTrigger HAL instrumentation and registers listeners to HAL events 57 * 58 * <p>This call requires {@link android.Manifest.permission.MANAGE_SOUND_TRIGGER}. 59 * and then the permissions are revoked prior to returning. 60 */ attachInstrumentation()61 public void attachInstrumentation() { 62 Log.d(TAG, "attachInstrumentation"); 63 mGlobalCallbackObserver = new GlobalCallbackObserver(); 64 mGlobalCallbackObserver.attach(); 65 } 66 67 /** 68 * Observer class exposing {@link ListenableFuture}'s on the {@link ModelCallback} and 69 * {@link RecognitionCallback} 70 * interfaces. 71 */ 72 public static class ModelSessionObserver implements AutoCloseable { 73 private final Object mModelSessionLock = new Object(); 74 @GuardedBy("mModelSessionLock") 75 @NonNull 76 private SettableFuture<RecognitionSession> mOnRecognitionStarted = SettableFuture.create(); 77 @GuardedBy("mModelSessionLock") 78 @NonNull 79 private SettableFuture<Void> mOnRecognitionStopped = SettableFuture.create(); 80 @GuardedBy("mModelSessionLock") 81 @NonNull 82 private SettableFuture<Void> mOnModelUnloaded = SettableFuture.create(); 83 private final ModelSession mModelSession; 84 private final Executor mModelSessionExecutor = Executors.newSingleThreadExecutor(); 85 86 private final RecognitionCallback mRecognitionCallback = () -> { 87 Log.d(TAG, "RecognitionCallback.onRecognitionStopped"); 88 synchronized (mModelSessionLock) { 89 mOnRecognitionStopped.set(null); 90 } 91 }; 92 93 private final ModelCallback mModelCallback = new ModelCallback() { 94 @Override 95 public void onRecognitionStarted(@NonNull RecognitionSession recognitionSession) { 96 Log.d(TAG, "ModelCallback.onRecognitionStarted"); 97 recognitionSession.setRecognitionCallback(mModelSessionExecutor, 98 mRecognitionCallback); 99 synchronized (mModelSessionLock) { 100 mOnRecognitionStarted.set(recognitionSession); 101 } 102 } 103 104 @Override 105 public void onModelUnloaded() { 106 Log.d(TAG, "ModelCallback.onModelUnloaded"); 107 synchronized (mModelSessionLock) { 108 mOnModelUnloaded.set(null); 109 } 110 } 111 }; 112 ModelSessionObserver(ModelSession modelSession)113 private ModelSessionObserver(ModelSession modelSession) { 114 mModelSession = modelSession; 115 modelSession.setModelCallback(mModelSessionExecutor, mModelCallback); 116 } 117 getModelSession()118 public ModelSession getModelSession() { 119 return mModelSession; 120 } 121 122 /** 123 * Returns a future to be completed on the next {@link ModelCallback#onRecognitionStarted} 124 */ getOnRecognitionStartedFuture()125 public ListenableFuture<RecognitionSession> getOnRecognitionStartedFuture() { 126 synchronized (mModelSessionLock) { 127 return mOnRecognitionStarted; 128 } 129 } 130 131 /** 132 * Returns a future to be completed on the next 133 * {@link RecognitionCallback#onRecognitionStopped} 134 */ getOnRecognitionStoppedFuture()135 public ListenableFuture<Void> getOnRecognitionStoppedFuture() { 136 synchronized (mModelSessionLock) { 137 return mOnRecognitionStopped; 138 } 139 } 140 141 /** 142 * Creates future to be completed on the next 143 * {@link ModelCallback#onModelUnloaded} 144 */ getOnModelUnloadedFuture()145 public ListenableFuture<Void> getOnModelUnloadedFuture() { 146 synchronized (mModelSessionLock) { 147 return mOnModelUnloaded; 148 } 149 } 150 151 /** 152 * Reset future listening for {@link ModelCallback#onRecognitionStarted} 153 * 154 * <p>The future must be completed prior to calling this method. 155 */ resetOnRecognitionStartedFuture()156 public void resetOnRecognitionStartedFuture() { 157 synchronized (mModelSessionLock) { 158 assertThat(mOnRecognitionStarted.isDone()).isTrue(); 159 mOnRecognitionStarted = SettableFuture.create(); 160 } 161 } 162 163 /** 164 * Reset future listening for {@link RecognitionCallback#onRecognitionStopped} 165 * 166 * <p>The future must be completed prior to calling this method. 167 */ resetOnRecognitionStoppedFuture()168 public void resetOnRecognitionStoppedFuture() { 169 synchronized (mModelSessionLock) { 170 assertThat(mOnRecognitionStopped.isDone()).isTrue(); 171 mOnRecognitionStopped = SettableFuture.create(); 172 } 173 } 174 175 /** 176 * Reset future listening for {@link ModelCallback#onModelUnloaded} 177 * 178 * <p>The future must be completed prior to calling this method. 179 */ resetOnModelUnloadedFuture()180 public void resetOnModelUnloadedFuture() { 181 synchronized (mModelSessionLock) { 182 assertThat(mOnModelUnloaded.isDone()).isTrue(); 183 mOnModelUnloaded = SettableFuture.create(); 184 } 185 } 186 187 @Override close()188 public void close() throws Exception { 189 mModelSession.clearModelCallback(); 190 synchronized (mModelSessionLock) { 191 mOnRecognitionStarted.cancel(true /* mayInterruptIfRunning */); 192 mOnRecognitionStopped.cancel(true /* mayInterruptIfRunning */); 193 mOnModelUnloaded.cancel(true /* mayInterruptIfRunning */); 194 } 195 } 196 } 197 198 /** 199 * Observer class exposing {@link ListenableFuture}'s on the {@link GlobalCallback} interface 200 */ 201 public static class GlobalCallbackObserver implements AutoCloseable { 202 private final Executor mGlobalCallbackExecutor = Executors.newSingleThreadExecutor(); 203 private final Object mGlobalObserverLock = new Object(); 204 @GuardedBy("mGlobalObserverLock") 205 @NonNull 206 private SettableFuture<ModelSessionObserver> mOnModelLoaded = SettableFuture.create(); 207 @GuardedBy("mGlobalObserverLock") 208 @NonNull 209 private SettableFuture<Void> mOnClientAttached = SettableFuture.create(); 210 @GuardedBy("mGlobalObserverLock") 211 @NonNull 212 private SettableFuture<Void> mOnClientDetached = SettableFuture.create(); 213 // do not expose as an API, only held for cleanup 214 @GuardedBy("mGlobalObserverLock") 215 private ModelSessionObserver mModelSessionObserver; 216 private SoundTriggerInstrumentation mInstrumentation; 217 218 private final GlobalCallback mGlobalCallback = new GlobalCallback() { 219 @Override 220 public void onModelLoaded(@NonNull ModelSession modelSession) { 221 Log.d(TAG, "GlobalCallback.onModelLoaded"); 222 synchronized (mGlobalObserverLock) { 223 mModelSessionObserver = new ModelSessionObserver(modelSession); 224 mOnModelLoaded.set(mModelSessionObserver); 225 } 226 } 227 228 @Override 229 public void onClientAttached() { 230 Log.d(TAG, "GlobalCallback.onClientAttached"); 231 synchronized (mGlobalObserverLock) { 232 mOnClientAttached.set(null); 233 } 234 } 235 236 @Override 237 public void onClientDetached() { 238 Log.d(TAG, "GlobalCallback.onClientDetached"); 239 synchronized (mGlobalObserverLock) { 240 mOnClientDetached.set(null); 241 } 242 } 243 }; 244 245 /** 246 * Attaches to the SoundTrigger HAL instrumentation and registers listeners to HAL events 247 */ attach()248 private void attach() { 249 Log.d(TAG, "attach SoundTriggerInstrumentation"); 250 mInstrumentation = SoundTriggerManager.attachInstrumentation( 251 mGlobalCallbackExecutor, mGlobalCallback); 252 } 253 254 /** 255 * Instrumentation reference to the SoundTrigger HAL 256 * 257 * <p>Must call {@link #attach} first 258 * <p>This value gets replaced via {@link #attach} 259 */ getInstrumentation()260 public SoundTriggerInstrumentation getInstrumentation() { 261 assertThat(mInstrumentation).isNotNull(); 262 return mInstrumentation; 263 } 264 265 /** 266 * Returns a future to be completed on the next {@link GlobalCallback#onModelLoaded} 267 */ getOnModelLoadedFuture()268 public ListenableFuture<ModelSessionObserver> getOnModelLoadedFuture() { 269 synchronized (mGlobalObserverLock) { 270 return mOnModelLoaded; 271 } 272 } 273 274 /** 275 * Returns a future to be completed on the next {@link GlobalCallback#onClientAttached()} 276 */ getOnClientAttachedFuture()277 public ListenableFuture<Void> getOnClientAttachedFuture() { 278 synchronized (mGlobalObserverLock) { 279 return mOnClientAttached; 280 } 281 } 282 283 /** 284 * Returns a future to be completed on the next {@link GlobalCallback#onClientDetached()} 285 */ getOnClientDetachedFuture()286 public ListenableFuture<Void> getOnClientDetachedFuture() { 287 synchronized (mGlobalObserverLock) { 288 return mOnClientDetached; 289 } 290 } 291 292 /** 293 * Reset future listening for {@link GlobalCallback#onModelLoaded} 294 * 295 * <p>The future must be completed prior to calling this method. 296 */ resetOnModelLoadedFuture()297 public void resetOnModelLoadedFuture() { 298 synchronized (mGlobalObserverLock) { 299 assertThat(mOnModelLoaded.isDone()).isTrue(); 300 mOnModelLoaded = SettableFuture.create(); 301 } 302 } 303 304 /** 305 * Reset future listening for {@link GlobalCallback#onModelLoaded} 306 * 307 * <p>The future must be completed prior to calling this method. 308 */ resetOnClientAttachedFuture()309 public void resetOnClientAttachedFuture() { 310 synchronized (mGlobalObserverLock) { 311 assertThat(mOnClientAttached.isDone()).isTrue(); 312 mOnClientAttached = SettableFuture.create(); 313 } 314 } 315 316 /** 317 * Reset future listening for {@link GlobalCallback#onModelLoaded} 318 * 319 * <p>The future must be completed prior to calling this method. 320 */ resetOnClientDetachedFuture()321 public void resetOnClientDetachedFuture() { 322 synchronized (mGlobalObserverLock) { 323 assertThat(mOnClientDetached.isDone()).isTrue(); 324 mOnClientDetached = SettableFuture.create(); 325 } 326 } 327 328 @Override close()329 public void close() throws Exception { 330 synchronized (mGlobalObserverLock) { 331 if (mModelSessionObserver != null) { 332 mModelSessionObserver.close(); 333 mModelSessionObserver = null; 334 } 335 mOnModelLoaded.cancel(true /* mayInterruptIfRunning */); 336 mOnClientAttached.cancel(true /* mayInterruptIfRunning */); 337 mOnClientDetached.cancel(true /* mayInterruptIfRunning */); 338 } 339 if (mInstrumentation != null) { 340 try { 341 mInstrumentation.triggerRestart(); 342 } catch (IllegalStateException e) { 343 Log.i(TAG, "Closing before instrumentation registration", e); 344 } 345 mInstrumentation = null; 346 } 347 } 348 } 349 350 /** 351 * Get the observer to {@link SoundTriggerInstrumentation} callbacks 352 * 353 * <p>Must call {@link #attachInstrumentation} first 354 */ getGlobalCallbackObserver()355 public GlobalCallbackObserver getGlobalCallbackObserver() { 356 assertThat(mGlobalCallbackObserver).isNotNull(); 357 return mGlobalCallbackObserver; 358 } 359 360 /** 361 * Helper method for common listener of recognition started 362 */ getOnRecognitionStartedFuture()363 public ListenableFuture<RecognitionSession> getOnRecognitionStartedFuture() { 364 return Futures.transformAsync(getGlobalCallbackObserver().getOnModelLoadedFuture(), 365 ModelSessionObserver::getOnRecognitionStartedFuture, Runnable::run); 366 } 367 368 @Override close()369 public void close() throws Exception { 370 if (mGlobalCallbackObserver != null) { 371 mGlobalCallbackObserver.close(); 372 mGlobalCallbackObserver = null; 373 } 374 } 375 } 376