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 * Returns whether the camera / mic privacy indicators are enabled. This method uses the same 230 * API call as the platform to check if the indicators are available to facilitate checking the 231 * enabled state. 232 */ getCameraMicIndicatorsEnabled()233 public static boolean getCameraMicIndicatorsEnabled() { 234 return SystemUtil.runWithShellPermissionIdentity( 235 () -> { 236 boolean enabled = 237 DeviceConfig.getBoolean( 238 DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG, true); 239 Log.v(TAG, "getCameraMicIndicatorsEnabled()=" + enabled); 240 return enabled; 241 }); 242 } 243 244 /** 245 * Returns the period of restarting the hotword detection service. 246 */ getHotwordDetectionServiceRestartPeriod()247 public static String getHotwordDetectionServiceRestartPeriod() { 248 return SystemUtil.runWithShellPermissionIdentity(() -> { 249 String currentPeriod = DeviceConfig.getProperty(NAMESPACE_VOICE_INTERACTION, 250 KEY_RESTART_PERIOD_IN_SECONDS); 251 Log.v(TAG, "getHotwordDetectionServiceRestartPeriod()=" + currentPeriod); 252 return currentPeriod; 253 }); 254 } 255 256 /** 257 * Sets the period of restarting the hotword detection service. 258 */ 259 public static void setHotwordDetectionServiceRestartPeriod(String period) { 260 SystemUtil.runWithShellPermissionIdentity(() -> { 261 Log.v(TAG, "setHotwordDetectionServiceRestartPeriod()=" + period); 262 DeviceConfig.setProperty(NAMESPACE_VOICE_INTERACTION, KEY_RESTART_PERIOD_IN_SECONDS, 263 period, false); 264 }); 265 } 266 267 /** 268 * Verify the microphone indicator present status. 269 */ 270 public static void verifyMicrophoneChipHandheld(boolean shouldBePresent) throws Exception { 271 // If the change Id is not present, then isChangeEnabled will return true. To bypass this, 272 // the change is set to "false" if present. 273 if (SystemUtil.callWithShellPermissionIdentity(() -> CompatChanges.isChangeEnabled( 274 PERMISSION_INDICATORS_NOT_PRESENT, Process.SYSTEM_UID))) { 275 return; 276 } 277 // Ensure the privacy chip is present (or not) 278 UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 279 final boolean chipFound = device.wait(Until.hasObject( 280 By.res(PRIVACY_CHIP_PKG, PRIVACY_CHIP_ID)), CLEAR_CHIP_MS); 281 assertEquals("chip display state", shouldBePresent, chipFound); 282 } 283 284 /** 285 * Verify HotwordDetectedResult. 286 */ 287 public static void verifyDetectedResult(AlwaysOnHotwordDetector.EventPayload detectedResult, 288 HotwordDetectedResult expectedDetectedResult) { 289 // TODO: Implement HotwordDetectedResult#equals to override the Bundle equality check; then 290 // simply check that the HotwordDetectedResults are equal. 291 HotwordDetectedResult hotwordDetectedResult = detectedResult.getHotwordDetectedResult(); 292 verifyHotwordDetectedResult(expectedDetectedResult, hotwordDetectedResult); 293 294 ParcelFileDescriptor audioStream = detectedResult.getAudioStream(); 295 assertThat(audioStream).isNull(); 296 } 297 298 private static void verifyHotwordDetectedResult(HotwordDetectedResult expectedDetectedResult, 299 HotwordDetectedResult hotwordDetectedResult) { 300 assertThat(hotwordDetectedResult).isNotNull(); 301 assertThat(hotwordDetectedResult.getAudioChannel()) 302 .isEqualTo(expectedDetectedResult.getAudioChannel()); 303 assertThat(hotwordDetectedResult.getConfidenceLevel()) 304 .isEqualTo(expectedDetectedResult.getConfidenceLevel()); 305 assertThat(hotwordDetectedResult.isHotwordDetectionPersonalized()) 306 .isEqualTo(expectedDetectedResult.isHotwordDetectionPersonalized()); 307 assertThat(hotwordDetectedResult.getHotwordDurationMillis()) 308 .isEqualTo(expectedDetectedResult.getHotwordDurationMillis()); 309 assertThat(hotwordDetectedResult.getHotwordOffsetMillis()) 310 .isEqualTo(expectedDetectedResult.getHotwordOffsetMillis()); 311 assertThat(hotwordDetectedResult.getHotwordPhraseId()) 312 .isEqualTo(expectedDetectedResult.getHotwordPhraseId()); 313 assertThat(hotwordDetectedResult.getPersonalizedScore()) 314 .isEqualTo(expectedDetectedResult.getPersonalizedScore()); 315 assertThat(hotwordDetectedResult.getScore()).isEqualTo(expectedDetectedResult.getScore()); 316 assertThat(hotwordDetectedResult.getBackgroundAudioPower()) 317 .isEqualTo(expectedDetectedResult.getBackgroundAudioPower()); 318 } 319 320 /** 321 * Verify Audio Egress HotwordDetectedResult. 322 */ 323 public static void verifyAudioEgressDetectedResult( 324 AlwaysOnHotwordDetector.EventPayload detectedResult, 325 HotwordDetectedResult expectedDetectedResult) throws Exception { 326 // TODO: Implement HotwordDetectedResult#equals to override the Bundle equality check; then 327 // simply check that the HotwordDetectedResults are equal. 328 HotwordDetectedResult hotwordDetectedResult = detectedResult.getHotwordDetectedResult(); 329 verifyHotwordDetectedResult(expectedDetectedResult, hotwordDetectedResult); 330 331 // Verify the HotwordAudioStream result 332 verifyHotwordAudioStream(hotwordDetectedResult.getAudioStreams().get(0), 333 expectedDetectedResult.getAudioStreams().get(0)); 334 335 ParcelFileDescriptor audioStream = detectedResult.getAudioStream(); 336 assertThat(audioStream).isNull(); 337 } 338 339 private static void verifyHotwordAudioStream(HotwordAudioStream detectedAudioStream, 340 HotwordAudioStream expectedAudioStream) throws Exception { 341 assertThat(detectedAudioStream.getAudioFormat()).isNotNull(); 342 assertThat(detectedAudioStream.getAudioStreamParcelFileDescriptor()).isNotNull(); 343 assertThat(detectedAudioStream.getAudioFormat()).isEqualTo( 344 expectedAudioStream.getAudioFormat()); 345 assertThat(detectedAudioStream.getInitialAudio()).isNotNull(); 346 assertThat(detectedAudioStream.getInitialAudio()).isEqualTo( 347 expectedAudioStream.getInitialAudio()); 348 assertAudioStream(detectedAudioStream.getAudioStreamParcelFileDescriptor(), 349 FAKE_HOTWORD_AUDIO_DATA); 350 assertThat(detectedAudioStream.getTimestamp().framePosition).isEqualTo( 351 expectedAudioStream.getTimestamp().framePosition); 352 assertThat(detectedAudioStream.getTimestamp().nanoTime).isEqualTo( 353 expectedAudioStream.getTimestamp().nanoTime); 354 assertThat(detectedAudioStream.getMetadata().size()).isEqualTo( 355 expectedAudioStream.getMetadata().size()); 356 assertThat(detectedAudioStream.getMetadata().getString(KEY_FAKE_DATA)).isEqualTo( 357 VALUE_FAKE_DATA); 358 } 359 360 private static void assertAudioStream(ParcelFileDescriptor audioStream, byte[] expected) 361 throws IOException { 362 try (InputStream audioSource = new ParcelFileDescriptor.AutoCloseInputStream(audioStream)) { 363 ByteArrayOutputStream result = new ByteArrayOutputStream(); 364 byte[] buffer = new byte[1024]; 365 int count; 366 while ((count = audioSource.read(buffer)) != -1) { 367 result.write(buffer, 0, count); 368 } 369 assertThat(result.toByteArray()).isEqualTo(expected); 370 } 371 372 try (OutputStream audioSource = new ParcelFileDescriptor.AutoCloseOutputStream( 373 audioStream)) { 374 audioSource.write(1); 375 fail("The parcelFileDescriptor should be ready only!"); 376 } catch (IOException exception) { 377 // expected 378 } 379 } 380 381 /** 382 * Returns {@code true} if the device supports multiple detectors, otherwise 383 * returns {@code false}. 384 */ 385 public static boolean isEnableMultipleDetectors() { 386 final boolean enableMultipleHotwordDetectors = CompatChanges.isChangeEnabled( 387 MULTIPLE_ACTIVE_HOTWORD_DETECTORS); 388 Log.d(TAG, "enableMultipleHotwordDetectors = " + enableMultipleHotwordDetectors); 389 return enableMultipleHotwordDetectors; 390 } 391 392 /** 393 * TODO: remove this helper when FutureSubject is available from 394 * {@link com.google.common.truth.Truth} 395 */ 396 public static <V> V waitForFutureDoneAndAssertSuccessful(Future<V> future) { 397 try { 398 return future.get(WAIT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); 399 } catch (InterruptedException | ExecutionException | TimeoutException e) { 400 throw new AssertionError("future failed to complete", e); 401 } 402 } 403 404 /** 405 * TODO: remove this helper when FutureSubject is available from 406 * {@link com.google.common.truth.Truth} 407 */ 408 public static void waitForVoidFutureAndAssertSuccessful(Future<Void> future) { 409 assertThat(waitForFutureDoneAndAssertSuccessful(future)).isNull(); 410 } 411 } 412