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