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