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.testcore;
18 
19 import static android.media.AudioFormat.CHANNEL_IN_FRONT;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.fail;
25 
26 import android.app.compat.CompatChanges;
27 import android.content.Context;
28 import android.hardware.soundtrigger.SoundTrigger;
29 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
30 import android.media.AudioFormat;
31 import android.os.ParcelFileDescriptor;
32 import android.os.PersistableBundle;
33 import android.os.Process;
34 import android.os.SharedMemory;
35 import android.provider.DeviceConfig;
36 import android.service.voice.AlwaysOnHotwordDetector;
37 import android.service.voice.HotwordAudioStream;
38 import android.service.voice.HotwordDetectedResult;
39 import android.service.voice.HotwordRejectedResult;
40 import android.support.test.uiautomator.By;
41 import android.support.test.uiautomator.UiDevice;
42 import android.support.test.uiautomator.Until;
43 import android.system.ErrnoException;
44 import android.util.Log;
45 
46 import androidx.test.platform.app.InstrumentationRegistry;
47 
48 import com.android.compatibility.common.util.SystemUtil;
49 
50 import com.google.common.collect.ImmutableList;
51 
52 import java.io.ByteArrayOutputStream;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.OutputStream;
56 import java.nio.ByteBuffer;
57 import java.util.Arrays;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.concurrent.ExecutionException;
61 import java.util.concurrent.Future;
62 import java.util.concurrent.TimeUnit;
63 import java.util.concurrent.TimeoutException;
64 
65 /**
66  * Helper for common functionalities.
67  */
68 public final class Helper {
69 
70     public static final String TAG = "VoiceInteractionCtsHelper";
71 
72     // The timeout to wait for async result
73     public static final long WAIT_TIMEOUT_IN_MS = 10_000;
74     public static final long WAIT_LONG_TIMEOUT_IN_MS = 15_000;
75     public static final long WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS = 3_000;
76 
77     // The test package
78     public static final String CTS_SERVICE_PACKAGE = "android.voiceinteraction.cts";
79 
80     // The id that is used to gate compat change
81     public static final long MULTIPLE_ACTIVE_HOTWORD_DETECTORS = 193232191L;
82     public static final Long PERMISSION_INDICATORS_NOT_PRESENT = 162547999L;
83 
84     // The mic indicator information
85     public static final Long CLEAR_CHIP_MS = 10000L;
86     private static final String PRIVACY_CHIP_PKG = "com.android.systemui";
87     private static final String PRIVACY_CHIP_ID = "privacy_chip";
88     private static final String INDICATORS_FLAG = "camera_mic_icons_enabled";
89     private static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
90     private static final String KEY_RESTART_PERIOD_IN_SECONDS = "restart_period_in_seconds";
91 
92     private static final String KEY_FAKE_DATA = "fakeData";
93     private static final String VALUE_FAKE_DATA = "fakeData";
94     private static final byte[] FAKE_BYTE_ARRAY_DATA = new byte[]{1, 2, 3};
95 
96     public static final int DEFAULT_PHRASE_ID = 5;
97     public static byte[] FAKE_HOTWORD_AUDIO_DATA =
98             new byte[]{'h', 'o', 't', 'w', 'o', 'r', 'd', '!'};
99 
100     // The permission is used to test keyphrase triggered.
101     // This is not exposed as an API so we define it here.
102     // TODO(b/273567812)
103     public static final String MANAGE_VOICE_KEYPHRASES =
104             "android.permission.MANAGE_VOICE_KEYPHRASES";
105 
106     // The locale is used to test keyphrase triggered
107     public static final Locale KEYPHRASE_LOCALE = Locale.forLanguageTag("en-US");
108     // The text is used to test keyphrase triggered
109     public static final String KEYPHRASE_TEXT = "Hello Android";
110 
111     // The key or extra used for HotwordDetectionService
112     public static final String KEY_TEST_SCENARIO = "testScenario";
113     public static final int EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_CRASH = 1;
114 
115     // The expected HotwordDetectedResult for testing
116     public static final HotwordDetectedResult DETECTED_RESULT =
117             new HotwordDetectedResult.Builder()
118                     .setAudioChannel(CHANNEL_IN_FRONT)
119                     .setConfidenceLevel(HotwordDetectedResult.CONFIDENCE_LEVEL_HIGH)
120                     .setHotwordDetectionPersonalized(true)
121                     .setHotwordDurationMillis(1000)
122                     .setHotwordOffsetMillis(500)
123                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
124                     .setPersonalizedScore(10)
125                     .setScore(15)
126                     .setBackgroundAudioPower(50)
127                     .build();
128     public static final HotwordDetectedResult DETECTED_RESULT_AFTER_STOP_DETECTION =
129             new HotwordDetectedResult.Builder()
130                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
131                     .setScore(57)
132                     .build();
133     public static final HotwordDetectedResult DETECTED_RESULT_FOR_MIC_FAILURE =
134             new HotwordDetectedResult.Builder()
135                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
136                     .setScore(58)
137                     .build();
138     public static final HotwordRejectedResult REJECTED_RESULT =
139             new HotwordRejectedResult.Builder()
140                     .setConfidenceLevel(HotwordRejectedResult.CONFIDENCE_LEVEL_MEDIUM)
141                     .build();
142 
143     /**
144      * Returns the SharedMemory data that is used for testing.
145      */
createFakeSharedMemoryData()146     public static SharedMemory createFakeSharedMemoryData() {
147         try {
148             SharedMemory sharedMemory = SharedMemory.create("SharedMemory", 3);
149             ByteBuffer byteBuffer = sharedMemory.mapReadWrite();
150             byteBuffer.put(FAKE_BYTE_ARRAY_DATA);
151             return sharedMemory;
152         } catch (ErrnoException e) {
153             Log.w(TAG, "createFakeSharedMemoryData ErrnoException : " + e);
154             throw new RuntimeException(e.getMessage());
155         }
156     }
157 
158     /**
159      * Returns the PersistableBundle data that is used for testing.
160      */
createFakePersistableBundleData()161     public static PersistableBundle createFakePersistableBundleData() {
162         // TODO : Add more data for testing
163         PersistableBundle persistableBundle = new PersistableBundle();
164         persistableBundle.putString(KEY_FAKE_DATA, VALUE_FAKE_DATA);
165         return persistableBundle;
166     }
167 
168     /**
169      * Returns the AudioFormat data that is used for testing.
170      */
createFakeAudioFormat()171     public static AudioFormat createFakeAudioFormat() {
172         return new AudioFormat.Builder()
173                 .setSampleRate(32000)
174                 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
175                 .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build();
176     }
177 
178     /**
179      * Returns a list of KeyphraseRecognitionExtra that is used for testing.
180      */
createFakeKeyphraseRecognitionExtraList()181     public static List<KeyphraseRecognitionExtra> createFakeKeyphraseRecognitionExtraList() {
182         return ImmutableList.of(new KeyphraseRecognitionExtra(DEFAULT_PHRASE_ID,
183                 SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER, 100));
184     }
185 
186     /**
187      * Returns the ParcelFileDescriptor data that is used for testing.
188      */
createFakeAudioStream()189     public static ParcelFileDescriptor createFakeAudioStream() {
190         ParcelFileDescriptor[] tempParcelFileDescriptors = null;
191         try {
192             tempParcelFileDescriptors = ParcelFileDescriptor.createPipe();
193             try (OutputStream fos =
194                          new ParcelFileDescriptor.AutoCloseOutputStream(
195                                  tempParcelFileDescriptors[1])) {
196                 fos.write(FAKE_HOTWORD_AUDIO_DATA, 0, 8);
197             } catch (IOException e) {
198                 Log.w(TAG, "Failed to pipe audio data : ", e);
199                 throw new IllegalStateException();
200             }
201             return tempParcelFileDescriptors[0];
202         } catch (IOException e) {
203             Log.w(TAG, "Failed to create a pipe : " + e);
204         }
205         throw new IllegalStateException();
206     }
207 
208     /**
209      * Returns the list of KeyphraseRecognitionExtra that is used for testing.
210      */
createKeyphraseRecognitionExtraList()211     public static List<KeyphraseRecognitionExtra> createKeyphraseRecognitionExtraList() {
212         return Arrays.asList(new SoundTrigger.KeyphraseRecognitionExtra(DEFAULT_PHRASE_ID,
213                 SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER, /* coarseConfidenceLevel= */ 10));
214     }
215 
216     /**
217      * Returns the array of {@link SoundTrigger.Keyphrase} that is used for testing.
218      */
createKeyphraseArray(Context context)219     public static SoundTrigger.Keyphrase[] createKeyphraseArray(Context context) {
220         return new SoundTrigger.Keyphrase[]{new SoundTrigger.Keyphrase(DEFAULT_PHRASE_ID,
221                 SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER,
222                 KEYPHRASE_LOCALE,
223                 KEYPHRASE_TEXT,
224                 new int[]{context.getUserId()}
225         )};
226     }
227 
228     /**
229      * Checks if the privacy indicators are enabled on this device. Sets the state to the parameter,
230      * And returns the original enable state (to allow this state to be reset after the test)
231      */
getIndicatorEnabledState()232     public static String getIndicatorEnabledState() {
233         return SystemUtil.runWithShellPermissionIdentity(() -> {
234             String currentlyEnabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PRIVACY,
235                     INDICATORS_FLAG);
236             Log.v(TAG, "getIndicatorEnabledStateIfNeeded()=" + currentlyEnabled);
237             return currentlyEnabled;
238         });
239     }
240 
241     /**
242      * Checks if the privacy indicators are enabled on this device. Sets the state to the parameter,
243      * and returns the original enable state (to allow this state to be reset after the test)
244      */
setIndicatorEnabledState(String shouldEnable)245     public static void setIndicatorEnabledState(String shouldEnable) {
246         SystemUtil.runWithShellPermissionIdentity(() -> {
247             Log.v(TAG, "setIndicatorEnabledState()=" + shouldEnable);
248             DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG, shouldEnable,
249                     false);
250         });
251     }
252 
253     /**
254      * Returns the period of restarting the hotword detection service.
255      */
getHotwordDetectionServiceRestartPeriod()256     public static String getHotwordDetectionServiceRestartPeriod() {
257         return SystemUtil.runWithShellPermissionIdentity(() -> {
258             String currentPeriod = DeviceConfig.getProperty(NAMESPACE_VOICE_INTERACTION,
259                     KEY_RESTART_PERIOD_IN_SECONDS);
260             Log.v(TAG, "getHotwordDetectionServiceRestartPeriod()=" + currentPeriod);
261             return currentPeriod;
262         });
263     }
264 
265     /**
266      * Sets the period of restarting the hotword detection service.
267      */
268     public static void setHotwordDetectionServiceRestartPeriod(String period) {
269         SystemUtil.runWithShellPermissionIdentity(() -> {
270             Log.v(TAG, "setHotwordDetectionServiceRestartPeriod()=" + period);
271             DeviceConfig.setProperty(NAMESPACE_VOICE_INTERACTION, KEY_RESTART_PERIOD_IN_SECONDS,
272                     period, false);
273         });
274     }
275 
276     /**
277      * Verify the microphone indicator present status.
278      */
279     public static void verifyMicrophoneChipHandheld(boolean shouldBePresent) throws Exception {
280         // If the change Id is not present, then isChangeEnabled will return true. To bypass this,
281         // the change is set to "false" if present.
282         if (SystemUtil.callWithShellPermissionIdentity(() -> CompatChanges.isChangeEnabled(
283                 PERMISSION_INDICATORS_NOT_PRESENT, Process.SYSTEM_UID))) {
284             return;
285         }
286         // Ensure the privacy chip is present (or not)
287         UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
288         final boolean chipFound = device.wait(Until.hasObject(
289                 By.res(PRIVACY_CHIP_PKG, PRIVACY_CHIP_ID)), CLEAR_CHIP_MS);
290         assertEquals("chip display state", shouldBePresent, chipFound);
291     }
292 
293     /**
294      * Verify HotwordDetectedResult.
295      */
296     public static void verifyDetectedResult(AlwaysOnHotwordDetector.EventPayload detectedResult,
297             HotwordDetectedResult expectedDetectedResult) {
298         // TODO: Implement HotwordDetectedResult#equals to override the Bundle equality check; then
299         // simply check that the HotwordDetectedResults are equal.
300         HotwordDetectedResult hotwordDetectedResult = detectedResult.getHotwordDetectedResult();
301         verifyHotwordDetectedResult(expectedDetectedResult, hotwordDetectedResult);
302 
303         ParcelFileDescriptor audioStream = detectedResult.getAudioStream();
304         assertThat(audioStream).isNull();
305     }
306 
307     private static void verifyHotwordDetectedResult(HotwordDetectedResult expectedDetectedResult,
308             HotwordDetectedResult hotwordDetectedResult) {
309         assertThat(hotwordDetectedResult).isNotNull();
310         assertThat(hotwordDetectedResult.getAudioChannel())
311                 .isEqualTo(expectedDetectedResult.getAudioChannel());
312         assertThat(hotwordDetectedResult.getConfidenceLevel())
313                 .isEqualTo(expectedDetectedResult.getConfidenceLevel());
314         assertThat(hotwordDetectedResult.isHotwordDetectionPersonalized())
315                 .isEqualTo(expectedDetectedResult.isHotwordDetectionPersonalized());
316         assertThat(hotwordDetectedResult.getHotwordDurationMillis())
317                 .isEqualTo(expectedDetectedResult.getHotwordDurationMillis());
318         assertThat(hotwordDetectedResult.getHotwordOffsetMillis())
319                 .isEqualTo(expectedDetectedResult.getHotwordOffsetMillis());
320         assertThat(hotwordDetectedResult.getHotwordPhraseId())
321                 .isEqualTo(expectedDetectedResult.getHotwordPhraseId());
322         assertThat(hotwordDetectedResult.getPersonalizedScore())
323                 .isEqualTo(expectedDetectedResult.getPersonalizedScore());
324         assertThat(hotwordDetectedResult.getScore()).isEqualTo(expectedDetectedResult.getScore());
325         assertThat(hotwordDetectedResult.getBackgroundAudioPower())
326                 .isEqualTo(expectedDetectedResult.getBackgroundAudioPower());
327     }
328 
329     /**
330      * Verify Audio Egress HotwordDetectedResult.
331      */
332     public static void verifyAudioEgressDetectedResult(
333             AlwaysOnHotwordDetector.EventPayload detectedResult,
334             HotwordDetectedResult expectedDetectedResult) throws Exception {
335         // TODO: Implement HotwordDetectedResult#equals to override the Bundle equality check; then
336         // simply check that the HotwordDetectedResults are equal.
337         HotwordDetectedResult hotwordDetectedResult = detectedResult.getHotwordDetectedResult();
338         verifyHotwordDetectedResult(expectedDetectedResult, hotwordDetectedResult);
339 
340         // Verify the HotwordAudioStream result
341         verifyHotwordAudioStream(hotwordDetectedResult.getAudioStreams().get(0),
342                 expectedDetectedResult.getAudioStreams().get(0));
343 
344         ParcelFileDescriptor audioStream = detectedResult.getAudioStream();
345         assertThat(audioStream).isNull();
346     }
347 
348     private static void verifyHotwordAudioStream(HotwordAudioStream detectedAudioStream,
349             HotwordAudioStream expectedAudioStream) throws Exception {
350         assertThat(detectedAudioStream.getAudioFormat()).isNotNull();
351         assertThat(detectedAudioStream.getAudioStreamParcelFileDescriptor()).isNotNull();
352         assertThat(detectedAudioStream.getAudioFormat()).isEqualTo(
353                 expectedAudioStream.getAudioFormat());
354         assertThat(detectedAudioStream.getInitialAudio()).isNotNull();
355         assertThat(detectedAudioStream.getInitialAudio()).isEqualTo(
356                 expectedAudioStream.getInitialAudio());
357         assertAudioStream(detectedAudioStream.getAudioStreamParcelFileDescriptor(),
358                 FAKE_HOTWORD_AUDIO_DATA);
359         assertThat(detectedAudioStream.getTimestamp().framePosition).isEqualTo(
360                 expectedAudioStream.getTimestamp().framePosition);
361         assertThat(detectedAudioStream.getTimestamp().nanoTime).isEqualTo(
362                 expectedAudioStream.getTimestamp().nanoTime);
363         assertThat(detectedAudioStream.getMetadata().size()).isEqualTo(
364                 expectedAudioStream.getMetadata().size());
365         assertThat(detectedAudioStream.getMetadata().getString(KEY_FAKE_DATA)).isEqualTo(
366                 VALUE_FAKE_DATA);
367     }
368 
369     private static void assertAudioStream(ParcelFileDescriptor audioStream, byte[] expected)
370             throws IOException {
371         try (InputStream audioSource = new ParcelFileDescriptor.AutoCloseInputStream(audioStream)) {
372             ByteArrayOutputStream result = new ByteArrayOutputStream();
373             byte[] buffer = new byte[1024];
374             int count;
375             while ((count = audioSource.read(buffer)) != -1) {
376                 result.write(buffer, 0, count);
377             }
378             assertThat(result.toByteArray()).isEqualTo(expected);
379         }
380 
381         try (OutputStream audioSource = new ParcelFileDescriptor.AutoCloseOutputStream(
382                 audioStream)) {
383             audioSource.write(1);
384             fail("The parcelFileDescriptor should be ready only!");
385         } catch (IOException exception) {
386             // expected
387         }
388     }
389 
390     /**
391      * Returns {@code true} if the device supports multiple detectors, otherwise
392      * returns {@code false}.
393      */
394     public static boolean isEnableMultipleDetectors() {
395         final boolean enableMultipleHotwordDetectors = CompatChanges.isChangeEnabled(
396                 MULTIPLE_ACTIVE_HOTWORD_DETECTORS);
397         Log.d(TAG, "enableMultipleHotwordDetectors = " + enableMultipleHotwordDetectors);
398         return enableMultipleHotwordDetectors;
399     }
400 
401     /**
402      * TODO: remove this helper when FutureSubject is available from
403      * {@link com.google.common.truth.Truth}
404      */
405     public static <V> V waitForFutureDoneAndAssertSuccessful(Future<V> future) {
406         try {
407             return future.get(WAIT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
408         } catch (InterruptedException | ExecutionException | TimeoutException e) {
409             throw new AssertionError("future failed to complete", e);
410         }
411     }
412 
413     /**
414      * TODO: remove this helper when FutureSubject is available from
415      * {@link com.google.common.truth.Truth}
416      */
417     public static void waitForVoidFutureAndAssertSuccessful(Future<Void> future) {
418         assertThat(waitForFutureDoneAndAssertSuccessful(future)).isNull();
419     }
420 }
421