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;
18 
19 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
20 import static android.Manifest.permission.MANAGE_SOUND_TRIGGER;
21 import static android.Manifest.permission.RECORD_AUDIO;
22 import static android.content.pm.PackageManager.FEATURE_MICROPHONE;
23 
24 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
25 
26 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 
30 import static org.junit.Assert.assertThrows;
31 import static org.junit.Assume.assumeTrue;
32 
33 import android.app.AppOpsManager;
34 import android.content.Context;
35 import android.media.soundtrigger.SoundTriggerDetector;
36 import android.media.soundtrigger.SoundTriggerDetector.Callback;
37 import android.media.soundtrigger.SoundTriggerDetector.EventPayload;
38 import android.media.soundtrigger.SoundTriggerInstrumentation;
39 import android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionSession;
40 import android.media.soundtrigger.SoundTriggerManager;
41 import android.media.soundtrigger.SoundTriggerManager.Model;
42 import android.os.Handler;
43 import android.os.HandlerThread;
44 import android.os.SystemClock;
45 import android.soundtrigger.cts.instrumentation.SoundTriggerInstrumentationObserver;
46 import android.soundtrigger.cts.instrumentation.SoundTriggerInstrumentationObserver.ModelSessionObserver;
47 import android.util.Log;
48 
49 import androidx.test.ext.junit.runners.AndroidJUnit4;
50 import androidx.test.filters.FlakyTest;
51 
52 import com.android.compatibility.common.util.RequiredFeatureRule;
53 
54 import com.google.common.util.concurrent.Futures;
55 import com.google.common.util.concurrent.ListenableFuture;
56 import com.google.common.util.concurrent.SettableFuture;
57 
58 import org.junit.After;
59 import org.junit.Before;
60 import org.junit.Rule;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 
64 import java.util.Objects;
65 import java.util.UUID;
66 import java.util.concurrent.ExecutionException;
67 import java.util.concurrent.Future;
68 import java.util.concurrent.TimeUnit;
69 import java.util.concurrent.TimeoutException;
70 
71 
72 @RunWith(AndroidJUnit4.class)
73 public class SoundTriggerManagerTest {
74     private static final String TAG = "SoundTriggerManagerTest";
75     private static final Context sContext = getInstrumentation().getTargetContext();
76     private static final int TIMEOUT = 5000;
77     private static final UUID MODEL_UUID = new UUID(5, 7);
78     private static final Model sModel = Model.create(MODEL_UUID, new UUID(7, 5), new byte[0], 1);
79 
80     private SoundTriggerManager mRealManager = null;
81     private SoundTriggerManager mManager = null;
82     private SoundTriggerInstrumentation mInstrumentation = null;
83     private SoundTriggerDetector mDetector = null;
84 
85     private final SoundTriggerInstrumentationObserver mInstrumentationObserver =
86             new SoundTriggerInstrumentationObserver();
87 
88     private final Object mDetectedLock = new Object();
89     private SettableFuture<Void> mDetectedFuture;
90     private boolean mDroppedCallback = false;
91 
92     private Handler mHandler = null;
93     private boolean mIsModelLoaded = false;
94 
95     @Rule
96     public RequiredFeatureRule REQUIRES_MIC_RULE = new RequiredFeatureRule(FEATURE_MICROPHONE);
97 
98     @Before
setup()99     public void setup() {
100         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
101         mRealManager = sContext.getSystemService(SoundTriggerManager.class);
102         mInstrumentationObserver.attachInstrumentation();
103         mManager = mRealManager.createManagerForTestModule();
104         getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
105     }
106 
107     @After
tearDown()108     public void tearDown() {
109         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
110         try {
111             if (mIsModelLoaded) {
112                 mManager.deleteModel(MODEL_UUID);
113             }
114         } catch (Exception e) {
115         }
116         if (mHandler != null) {
117             mHandler.getLooper().quit();
118         }
119 
120         // Clean up any unexpected HAL state
121         // Wait for stray callbacks and to disambiguate the logs
122         SystemClock.sleep(50);
123         try {
124             mInstrumentationObserver.close();
125         } catch (Exception e) {
126             throw new RuntimeException(e);
127         }
128         // Wait for the mock HAL to reboot
129         SystemClock.sleep(100);
130         getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
131     }
132 
waitForFutureDoneAndAssertSuccessful(Future<V> future)133     public static <V> V waitForFutureDoneAndAssertSuccessful(Future<V> future) {
134         try {
135             return future.get(TIMEOUT, TimeUnit.MILLISECONDS);
136         } catch (InterruptedException | ExecutionException | TimeoutException e) {
137             throw new AssertionError("future failed to complete", e);
138         }
139     }
140 
loadModelForRecognition()141     private void loadModelForRecognition() {
142         mManager.updateModel(sModel);
143         assertThat(mManager.loadSoundModel(sModel.getSoundModel())).isEqualTo(0);
144         mIsModelLoaded = true;
145     }
146 
listenForDetection()147     private ListenableFuture<Void> listenForDetection() {
148         synchronized (mDetectedLock) {
149             if (mDetectedFuture != null) {
150                 assertThat(mDetectedFuture.isDone()).isTrue();
151                 assertThat(mDroppedCallback).isFalse();
152             }
153             mDetectedFuture = SettableFuture.create();
154             Log.d(TAG, "Begin listen for detection" + mDetectedFuture);
155             return mDetectedFuture;
156         }
157     }
158 
setUpDetector()159     private void setUpDetector() {
160         var thread = new HandlerThread("SoundTriggerDetectorHandler");
161         thread.start();
162         mHandler = new Handler(thread.getLooper());
163         mDetector =
164                 mManager.createSoundTriggerDetector(
165                         MODEL_UUID,
166                         new Callback() {
167                             @Override
168                             public void onAvailabilityChanged(int status) {}
169 
170                             @Override
171                             public void onDetected(EventPayload eventPayload) {
172                                 synchronized (mDetectedLock) {
173                                     mDroppedCallback = !mDetectedFuture.set(null);
174                                     if (mDroppedCallback) {
175                                         Log.e(TAG, "Dropped detection" + mDetectedFuture);
176                                     } else {
177                                         Log.d(TAG, "Detection" + mDetectedFuture);
178                                     }
179                                 }
180                             }
181 
182                             @Override
183                             public void onError() {}
184 
185                             @Override
186                             public void onRecognitionPaused() {}
187 
188                             @Override
189                             public void onRecognitionResumed() {}
190                         },
191                         mHandler);
192     }
193 
getSoundTriggerPermissions()194     private void getSoundTriggerPermissions() {
195         getInstrumentation()
196                 .getUiAutomation()
197                 .adoptShellPermissionIdentity(
198                         RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD, MANAGE_SOUND_TRIGGER);
199     }
200 
201     @Test
testStartRecognitionFails_whenMissingRecordPermission()202     public void testStartRecognitionFails_whenMissingRecordPermission() {
203         getSoundTriggerPermissions();
204         loadModelForRecognition();
205         getInstrumentation()
206                 .getUiAutomation()
207                 .adoptShellPermissionIdentity(CAPTURE_AUDIO_HOTWORD, MANAGE_SOUND_TRIGGER);
208         setUpDetector();
209         assertThat(mDetector.startRecognition(0)).isFalse();
210     }
211 
212     @Test
testStartRecognitionFails_whenMissingHotwordPermission()213     public void testStartRecognitionFails_whenMissingHotwordPermission() {
214         getSoundTriggerPermissions();
215         loadModelForRecognition();
216         getInstrumentation()
217                 .getUiAutomation()
218                 .adoptShellPermissionIdentity(RECORD_AUDIO, MANAGE_SOUND_TRIGGER);
219         setUpDetector();
220         assertThat(mDetector.startRecognition(0)).isFalse();
221     }
222 
223     @Test
testStartRecognitionSucceeds_whenHoldingPermissions()224     public void testStartRecognitionSucceeds_whenHoldingPermissions() throws Exception {
225         getSoundTriggerPermissions();
226         var detectedFuture = listenForDetection();
227         loadModelForRecognition();
228         setUpDetector();
229         assertThat(mDetector.startRecognition(0)).isTrue();
230 
231         RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
232                 mInstrumentationObserver.getOnRecognitionStartedFuture());
233         assertThat(recognitionSession).isNotNull();
234 
235         recognitionSession.triggerRecognitionEvent(new byte[] {0x11}, null);
236         waitForFutureDoneAndAssertSuccessful(detectedFuture);
237     }
238 
239     @Test
testRecognitionEvent_notesAppOps()240     public void testRecognitionEvent_notesAppOps() throws Exception {
241         getSoundTriggerPermissions();
242 
243         loadModelForRecognition();
244         setUpDetector();
245         assertThat(mDetector.startRecognition(0)).isTrue();
246         RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
247                 mInstrumentationObserver.getOnRecognitionStartedFuture());
248 
249         assertThat(recognitionSession).isNotNull();
250 
251         final SettableFuture<Void> ambientOpFuture = SettableFuture.create();
252         var detectedFuture = listenForDetection();
253 
254         AppOpsManager appOpsManager =
255                 sContext.getSystemService(AppOpsManager.class);
256         final String[] OPS_TO_WATCH =
257                 new String[] {
258                     AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO
259                 };
260 
261         runWithShellPermissionIdentity(() ->
262                 appOpsManager.startWatchingNoted(
263                         OPS_TO_WATCH,
264                         (op, uid, pkgName, attributionTag, flags, result) -> {
265                             if (Objects.equals(
266                                     op, AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO)) {
267                                 ambientOpFuture.set(null);
268                             }
269                         }));
270 
271         // Grab permissions again since we transitioned out of shell identity
272         getSoundTriggerPermissions();
273 
274         recognitionSession.triggerRecognitionEvent(new byte[] {0x11}, null);
275         waitForFutureDoneAndAssertSuccessful(ambientOpFuture);
276         waitForFutureDoneAndAssertSuccessful(detectedFuture);
277     }
278 
279     @Test
testAttachInvalidSession_whenNoDspAvailable()280     public void testAttachInvalidSession_whenNoDspAvailable() {
281         getSoundTriggerPermissions();
282         if (mManager.listModuleProperties().size() == 1) {
283             assertThrows(IllegalStateException.class,
284                     () -> mRealManager.loadSoundModel(sModel.getSoundModel()));
285         }
286     }
287 
288     @Test
testNullModuleProperties_whenNoDspAvailable()289     public void testNullModuleProperties_whenNoDspAvailable() {
290         getSoundTriggerPermissions();
291         if (mManager.listModuleProperties().size() == 1) {
292             assertThat(mRealManager.getModuleProperties()).isNull();
293         }
294     }
295 
296     @Test
testAttachThrows_whenMissingRecordPermission()297     public void testAttachThrows_whenMissingRecordPermission() {
298         getInstrumentation()
299                 .getUiAutomation()
300                 .adoptShellPermissionIdentity(CAPTURE_AUDIO_HOTWORD, MANAGE_SOUND_TRIGGER);
301         assertThrows(
302                 SecurityException.class,
303                 () -> mRealManager.createManagerForTestModule());
304     }
305 
306     @Test
testAttachThrows_whenMissingCaptureHotwordPermission()307     public void testAttachThrows_whenMissingCaptureHotwordPermission() {
308         getInstrumentation()
309                 .getUiAutomation()
310                 .adoptShellPermissionIdentity(RECORD_AUDIO, MANAGE_SOUND_TRIGGER);
311         assertThrows(
312                 SecurityException.class,
313                 () -> mRealManager.createManagerForTestModule());
314     }
315 
316     // This test is inherently flaky, since the raciness it tests isn't totally solved.
317     @FlakyTest
318     @Test
testStartTriggerStopRecognitionRace_doesNotFail()319     public void testStartTriggerStopRecognitionRace_doesNotFail() throws Exception {
320         // Disable this test for now since it is flaky
321         assumeTrue(false);
322         final int ITERATIONS = 20;
323         getSoundTriggerPermissions();
324         final ListenableFuture<ModelSessionObserver> modelSessionFuture =
325                 mInstrumentationObserver.getGlobalCallbackObserver().getOnModelLoadedFuture();
326 
327         loadModelForRecognition();
328         setUpDetector();
329         assertThat(mDetector.startRecognition(0)).isTrue();
330         final ModelSessionObserver modelSessionObserver = waitForFutureDoneAndAssertSuccessful(
331                 modelSessionFuture);
332 
333         for (int i = 0; i < ITERATIONS; i++) {
334             var detectedFuture = listenForDetection();
335             RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
336                     modelSessionObserver.getOnRecognitionStartedFuture());
337             assertThat(recognitionSession).isNotNull();
338             modelSessionObserver.resetOnRecognitionStartedFuture();
339             // Attempt to interleave a stopRecognition + startRecognition and an upward
340             // recognition event
341             recognitionSession.triggerRecognitionEvent(new byte[] {0x11}, null);
342             mDetector.stopRecognition();
343             // Due to limitations in the STHAL API, we are still vulnerable to raciness, and
344             // we could receive the recognition event for this startRecognition
345             assertThat(mDetector.startRecognition(0)).isTrue();
346             // Get the new recognition session
347             recognitionSession = waitForFutureDoneAndAssertSuccessful(
348                     modelSessionObserver.getOnRecognitionStartedFuture());
349             assertThat(recognitionSession).isNotNull();
350             // Wait a bit to receive a recognition event which we *may* have gotten
351             SystemClock.sleep(50);
352             if (detectedFuture.isDone()) {
353                 detectedFuture = listenForDetection();
354             }
355             // Check that the validation layer doesn't think that the new recognition session is
356             // stopped
357             recognitionSession.triggerRecognitionEvent(new byte[] {0x11}, null);
358             waitForFutureDoneAndAssertSuccessful(detectedFuture);
359             modelSessionObserver.resetOnRecognitionStartedFuture();
360             assertThat(mDetector.startRecognition(0)).isTrue();
361         }
362     }
363 }
364