1 /*
2  * Copyright (C) 2022 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.voiceinteraction.cts;
18 
19 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
20 import static android.Manifest.permission.RECORD_AUDIO;
21 import static android.content.Context.ATTENTION_SERVICE;
22 import static android.service.voice.HotwordDetectedResult.PROXIMITY_FAR;
23 import static android.service.voice.HotwordDetectedResult.PROXIMITY_NEAR;
24 import static android.service.voice.HotwordDetectedResult.PROXIMITY_UNKNOWN;
25 import static android.service.voice.HotwordDetectionService.ENABLE_PROXIMITY_RESULT;
26 import static android.voiceinteraction.cts.testcore.Helper.CTS_SERVICE_PACKAGE;
27 
28 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
29 
30 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
31 
32 import static com.google.common.truth.Truth.assertThat;
33 
34 import static org.junit.Assume.assumeTrue;
35 
36 import android.app.Instrumentation;
37 import android.app.UiAutomation;
38 import android.attentionservice.cts.CtsTestAttentionService;
39 import android.content.res.Resources;
40 import android.os.ParcelFileDescriptor;
41 import android.platform.test.annotations.AppModeFull;
42 import android.provider.DeviceConfig;
43 import android.service.voice.AlwaysOnHotwordDetector;
44 import android.service.voice.HotwordDetectedResult;
45 import android.service.voice.HotwordDetectionService;
46 import android.service.voice.HotwordDetector;
47 import android.util.Log;
48 import android.voiceinteraction.cts.services.BaseVoiceInteractionService;
49 import android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService;
50 import android.voiceinteraction.cts.testcore.Helper;
51 import android.voiceinteraction.cts.testcore.VoiceInteractionServiceConnectedRule;
52 
53 import androidx.test.ext.junit.runners.AndroidJUnit4;
54 import androidx.test.filters.RequiresDevice;
55 import androidx.test.platform.app.InstrumentationRegistry;
56 
57 import com.android.compatibility.common.util.ApiTest;
58 import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
59 import com.android.compatibility.common.util.RequiredServiceRule;
60 
61 import org.junit.After;
62 import org.junit.Before;
63 import org.junit.Rule;
64 import org.junit.Test;
65 import org.junit.runner.RunWith;
66 
67 import java.util.Objects;
68 
69 /**
70  * Tests for using the Attention Service inside VoiceInteractionService using
71  * a basic HotwordDetectionService.
72  */
73 
74 @ApiTest(apis = {"android.service.voice.HotwordDetectedResult#getExtras"})
75 @RunWith(AndroidJUnit4.class)
76 @AppModeFull(reason = "No real use case for instant mode hotword detection service")
77 public final class HotwordDetectionServiceProximityTest {
78 
79     private static final String TAG = "HotwordDetectionServiceProximityTest";
80     private static final String SERVICE_ENABLED = "service_enabled";
81     // The VoiceInteractionService used by this test
82     private static final String SERVICE_COMPONENT =
83             "android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService";
84 
85     private static final double PROXIMITY_NEAR_METERS = 2.0;
86     private static final double PROXIMITY_FAR_METERS = 6.0;
87     private static final String FAKE_SERVICE_PACKAGE =
88             HotwordDetectionServiceProximityTest.class.getPackage().getName();
89 
90     @Rule
91     public final RequiredServiceRule ATTENTION_SERVICE_RULE =
92             new RequiredServiceRule(ATTENTION_SERVICE);
93 
94     @Rule
95     public final DeviceConfigStateChangerRule mEnableAttentionManagerServiceRule =
96             new DeviceConfigStateChangerRule(sInstrumentation.getTargetContext(),
97                     DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE,
98                     SERVICE_ENABLED,
99                     "true");
100 
101     @Rule
102     public VoiceInteractionServiceConnectedRule mConnectedRule =
103             new VoiceInteractionServiceConnectedRule(
104                     getInstrumentation().getTargetContext(), getTestVoiceInteractionService());
105 
106     private CtsBasicVoiceInteractionService mService;
107 
108     private static final Instrumentation sInstrumentation =
109             InstrumentationRegistry.getInstrumentation();
110 
111     private final Resources mResources = getInstrumentation().getTargetContext()
112             .getResources();
113 
114     private boolean mIsProximitySupported;
115 
116     @Before
setup()117     public void setup() {
118         try {
119             mIsProximitySupported = mResources.getBoolean(
120                     mResources.getIdentifier("config_enableProximityService", "bool", "android"));
121         } catch (Resources.NotFoundException e) {
122             // this would mean resource is not found in device. skip test
123         }
124         assumeTrue("Proximity Feature not available on this device. Skipping test.",
125                 mIsProximitySupported);
126 
127         // Set up Attention Service
128         CtsTestAttentionService.reset();
129         assertThat(setTestableAttentionService(FAKE_SERVICE_PACKAGE)).isTrue();
130         assertThat(getAttentionServiceComponent()).contains(FAKE_SERVICE_PACKAGE);
131         runShellCommand("cmd attention call checkAttention");
132 
133         // VoiceInteractionServiceConnectedRule handles the service connected,
134         // the test should be able to get service
135         mService = (CtsBasicVoiceInteractionService) BaseVoiceInteractionService.getService();
136         // Check the test can get the service
137         Objects.requireNonNull(mService);
138     }
139 
140     @After
tearDown()141     public void tearDown() {
142         runShellCommand("cmd attention clearTestableAttentionService");
143         mService = null;
144     }
145 
146     @Test
147     @RequiresDevice
testAttentionService_onDetectFromDsp()148     public void testAttentionService_onDetectFromDsp() throws Throwable {
149         // Create AlwaysOnHotwordDetector and wait the HotwordDetectionService ready
150         AlwaysOnHotwordDetector alwaysOnHotwordDetector = createAlwaysOnHotwordDetector();
151 
152         try {
153             adoptShellPermissionIdentityForHotword();
154 
155             // Trigger recognition for test
156             triggerHardwareRecognitionEventForTest(alwaysOnHotwordDetector);
157 
158             // wait onDetected() called and verify the result
159             HotwordDetectedResult hotwordDetectedResult1 = waitHotwordServiceOnDetectedResult();
160 
161             // by default, proximity should not be returned.
162             verifyProximityBundle(hotwordDetectedResult1, null);
163 
164             // proximity is unknown
165             CtsTestAttentionService.respondProximity(PROXIMITY_UNKNOWN);
166 
167             // Trigger recognition for test
168             triggerHardwareRecognitionEventForTest(alwaysOnHotwordDetector);
169 
170             // wait onDetected() called and verify the result
171             HotwordDetectedResult hotwordDetectedResult2 = waitHotwordServiceOnDetectedResult();
172 
173             // when proximity is unknown, proximity should not be returned.
174             verifyProximityBundle(hotwordDetectedResult2, null);
175 
176             // proximity is PROXIMITY_NEAR_METERS
177             CtsTestAttentionService.respondProximity(PROXIMITY_NEAR_METERS);
178 
179             // Trigger recognition for test
180             triggerHardwareRecognitionEventForTest(alwaysOnHotwordDetector);
181 
182             // wait onDetected() called and verify the result
183             HotwordDetectedResult hotwordDetectedResult3 = waitHotwordServiceOnDetectedResult();
184 
185             // when proximity is PROXIMITY_NEAR_METERS, proximity should be PROXIMITY_NEAR.
186             verifyProximityBundle(hotwordDetectedResult3, PROXIMITY_NEAR);
187 
188             // proximity is PROXIMITY_FAR_METERS
189             CtsTestAttentionService.respondProximity(PROXIMITY_FAR_METERS);
190 
191             // Trigger recognition for test
192             triggerHardwareRecognitionEventForTest(alwaysOnHotwordDetector);
193 
194             // wait onDetected() called and verify the result
195             HotwordDetectedResult hotwordDetectedResult4 = waitHotwordServiceOnDetectedResult();
196 
197             // when proximity is PROXIMITY_FAR_METERS, proximity should be PROXIMITY_FAR.
198             verifyProximityBundle(hotwordDetectedResult4, PROXIMITY_FAR);
199         } finally {
200             alwaysOnHotwordDetector.destroy();
201             // Drop identity adopted.
202             InstrumentationRegistry.getInstrumentation().getUiAutomation()
203                     .dropShellPermissionIdentity();
204         }
205     }
206 
207     @Test
208     @RequiresDevice
testAttentionService_onDetectFromMic_noUpdates()209     public void testAttentionService_onDetectFromMic_noUpdates() throws Throwable {
210         // Create SoftwareHotwordDetector
211         HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector();
212         try {
213             adoptShellPermissionIdentityForHotword();
214 
215             mService.initDetectRejectLatch();
216             softwareHotwordDetector.startRecognition();
217 
218             // wait onDetected() called and verify the result
219             HotwordDetectedResult hotwordDetectedResult = waitHotwordServiceOnDetectedResult();
220 
221             verifyProximityBundle(hotwordDetectedResult, null);
222         } finally {
223             softwareHotwordDetector.destroy();
224             // Drop identity adopted.
225             InstrumentationRegistry.getInstrumentation().getUiAutomation()
226                     .dropShellPermissionIdentity();
227         }
228     }
229 
230     @Test
231     @RequiresDevice
testAttentionService_onDetectFromMic_unknownProximity()232     public void testAttentionService_onDetectFromMic_unknownProximity() throws Throwable {
233         // Create SoftwareHotwordDetector
234         HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector();
235 
236         CtsTestAttentionService.respondProximity(PROXIMITY_UNKNOWN);
237 
238         try {
239             adoptShellPermissionIdentityForHotword();
240 
241             mService.initDetectRejectLatch();
242             softwareHotwordDetector.startRecognition();
243 
244             // wait onDetected() called and verify the result
245             HotwordDetectedResult hotwordDetectedResult = waitHotwordServiceOnDetectedResult();
246 
247             verifyProximityBundle(hotwordDetectedResult, null);
248         } finally {
249             softwareHotwordDetector.destroy();
250             // Drop identity adopted.
251             InstrumentationRegistry.getInstrumentation().getUiAutomation()
252                     .dropShellPermissionIdentity();
253         }
254     }
255 
256     @Test
257     @RequiresDevice
testAttentionService_onDetectFromMic_updatedProximity()258     public void testAttentionService_onDetectFromMic_updatedProximity() throws Throwable {
259         // Create SoftwareHotwordDetector
260         HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector();
261 
262         CtsTestAttentionService.respondProximity(PROXIMITY_NEAR_METERS);
263 
264         try {
265             adoptShellPermissionIdentityForHotword();
266 
267             mService.initDetectRejectLatch();
268             softwareHotwordDetector.startRecognition();
269 
270             // wait onDetected() called and verify the result
271             HotwordDetectedResult hotwordDetectedResult = waitHotwordServiceOnDetectedResult();
272 
273             verifyProximityBundle(hotwordDetectedResult, PROXIMITY_NEAR);
274         } finally {
275             softwareHotwordDetector.destroy();
276             // Drop identity adopted.
277             InstrumentationRegistry.getInstrumentation().getUiAutomation()
278                     .dropShellPermissionIdentity();
279         }
280     }
281 
282     @Test
283     @RequiresDevice
testAttentionService_onDetectFromExternalSource_doesNotReceiveProximity()284     public void testAttentionService_onDetectFromExternalSource_doesNotReceiveProximity()
285             throws Throwable {
286         AlwaysOnHotwordDetector alwaysOnHotwordDetector = createAlwaysOnHotwordDetector();
287         CtsTestAttentionService.respondProximity(PROXIMITY_FAR);
288 
289         try {
290             adoptShellPermissionIdentityForHotword();
291 
292             ParcelFileDescriptor audioStream = Helper.createFakeAudioStream();
293             mService.initDetectRejectLatch();
294             alwaysOnHotwordDetector.startRecognition(audioStream,
295                     Helper.createFakeAudioFormat(),
296                     Helper.createFakePersistableBundleData());
297 
298             // wait onDetected() called and verify the result
299             HotwordDetectedResult hotwordDetectedResult = waitHotwordServiceOnDetectedResult();
300 
301             verifyProximityBundle(hotwordDetectedResult, null);
302         } finally {
303             alwaysOnHotwordDetector.destroy();
304             // Drop identity adopted.
305             InstrumentationRegistry.getInstrumentation().getUiAutomation()
306                     .dropShellPermissionIdentity();
307         }
308     }
309 
getTestVoiceInteractionService()310     public String getTestVoiceInteractionService() {
311         Log.d(TAG, "getTestVoiceInteractionService()");
312         return CTS_SERVICE_PACKAGE + "/" + SERVICE_COMPONENT;
313     }
314 
getAttentionServiceComponent()315     private static String getAttentionServiceComponent() {
316         return runShellCommand("cmd attention getAttentionServiceComponent");
317     }
318 
setTestableAttentionService(String service)319     private static boolean setTestableAttentionService(String service) {
320         return runShellCommand("cmd attention setTestableAttentionService " + service)
321                 .equals("true");
322     }
323 
324     // TODO: use a base test case and move common part to base test class
triggerHardwareRecognitionEventForTest( AlwaysOnHotwordDetector alwaysOnHotwordDetector)325     private void triggerHardwareRecognitionEventForTest(
326             AlwaysOnHotwordDetector alwaysOnHotwordDetector) {
327         mService.initDetectRejectLatch();
328         alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
329                 /* status= */ 0, /* soundModelHandle= */ 100,
330                 /* halEventReceivedMillis= */ 12345, /* captureAvailable= */ true,
331                 /* captureSession= */ 101, /* captureDelayMs= */ 1000,
332                 /* capturePreambleMs= */ 1001, /* triggerInData= */ true,
333                 Helper.createFakeAudioFormat(), new byte[1024],
334                 Helper.createFakeKeyphraseRecognitionExtraList());
335     }
336 
waitHotwordServiceOnDetectedResult()337     private HotwordDetectedResult waitHotwordServiceOnDetectedResult() throws Throwable {
338         mService.waitOnDetectOrRejectCalled();
339         AlwaysOnHotwordDetector.EventPayload detectedResult =
340                 mService.getHotwordServiceOnDetectedResult();
341         return detectedResult.getHotwordDetectedResult();
342     }
343 
adoptShellPermissionIdentityForHotword()344     private void adoptShellPermissionIdentityForHotword() {
345         // Drop any identity adopted earlier.
346         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
347         uiAutomation.dropShellPermissionIdentity();
348         // need to retain the identity until the callback is triggered
349         uiAutomation.adoptShellPermissionIdentity(RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD);
350     }
351 
352     /**
353      * Create software hotword detector and wait for ready
354      */
createSoftwareHotwordDetector()355     private HotwordDetector createSoftwareHotwordDetector() throws Throwable {
356         // Create SoftwareHotwordDetector
357         mService.createSoftwareHotwordDetector();
358 
359         mService.waitSandboxedDetectionServiceInitializedCalledOrException();
360 
361         // verify callback result
362         assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
363                 HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
364         HotwordDetector softwareHotwordDetector = mService.getSoftwareHotwordDetector();
365         Objects.requireNonNull(softwareHotwordDetector);
366 
367         return softwareHotwordDetector;
368     }
369 
370     /**
371      * Create AlwaysOnHotwordDetector and wait for ready
372      */
createAlwaysOnHotwordDetector()373     private AlwaysOnHotwordDetector createAlwaysOnHotwordDetector() throws Throwable {
374         // Create AlwaysOnHotwordDetector and wait ready.
375         mService.createAlwaysOnHotwordDetector();
376 
377         mService.waitSandboxedDetectionServiceInitializedCalledOrException();
378 
379         // verify callback result
380         assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
381                 HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
382         AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
383         Objects.requireNonNull(alwaysOnHotwordDetector);
384 
385         return alwaysOnHotwordDetector;
386     }
387 
388     // simply check that the proximity values are equal.
verifyProximityBundle(HotwordDetectedResult hotwordDetectedResult, Integer expected)389     private void verifyProximityBundle(HotwordDetectedResult hotwordDetectedResult,
390             Integer expected) {
391         assertThat(hotwordDetectedResult).isNotNull();
392         if (expected == null || !ENABLE_PROXIMITY_RESULT) {
393             assertThat(hotwordDetectedResult.getProximity()).isEqualTo(PROXIMITY_UNKNOWN);
394         } else {
395             assertThat(hotwordDetectedResult.getProximity()).isEqualTo(expected);
396         }
397     }
398 }
399