blob: 98b519787087e48823403ddbb246d43bf38044f3 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.voiceinteraction.cts;
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
import static android.Manifest.permission.MANAGE_HOTWORD_DETECTION;
import static android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE;
import static android.Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.content.pm.PackageManager.FEATURE_MICROPHONE;
import static android.service.voice.HotwordDetectionServiceFailure.ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED;
import static android.voiceinteraction.common.Utils.AUDIO_EGRESS_DETECTED_RESULT;
import static android.voiceinteraction.common.Utils.AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE;
import static android.voiceinteraction.common.Utils.EXTRA_HOTWORD_DETECTION_SERVICE_CAN_READ_AUDIO_DATA_IS_NOT_ZERO;
import static android.voiceinteraction.cts.testcore.Helper.CTS_SERVICE_PACKAGE;
import static android.voiceinteraction.cts.testcore.Helper.MANAGE_VOICE_KEYPHRASES;
import static android.voiceinteraction.cts.testcore.Helper.WAIT_TIMEOUT_IN_MS;
import static android.voiceinteraction.cts.testcore.Helper.createKeyphraseRecognitionExtraList;
import static android.voiceinteraction.cts.testcore.Helper.waitForFutureDoneAndAssertSuccessful;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.AppOpsManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.app.wearable.WearableSensingManager;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.hardware.soundtrigger.SoundTrigger;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionSession;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.permission.flags.Flags;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.DeviceConfig;
import android.service.voice.AlwaysOnHotwordDetector;
import android.service.voice.HotwordDetectionService;
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordDetector;
import android.service.voice.HotwordRejectedResult;
import android.service.voice.SandboxedDetectionInitializer;
import android.soundtrigger.cts.instrumentation.SoundTriggerInstrumentationObserver;
import android.util.Log;
import android.voiceinteraction.common.Utils;
import android.voiceinteraction.cts.services.BaseVoiceInteractionService;
import android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService;
import android.voiceinteraction.cts.testcore.Helper;
import android.voiceinteraction.cts.testcore.VoiceInteractionServiceConnectedRule;
import android.voiceinteraction.service.MainWearableSensingService;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.RequiresDevice;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.DisableAnimationRule;
import com.android.compatibility.common.util.RequiredFeatureRule;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests for {@link HotwordDetectionService}.
*/
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "No real use case for instant mode hotword detection service")
public class HotwordDetectionServiceBasicTest extends AbstractHdsTestCase {
private static final String TAG = "HotwordDetectionServiceTest";
// The VoiceInteractionService used by this test
private static final String SERVICE_COMPONENT =
"android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService";
private static final int USER_ID = UserHandle.myUserId();
private static final String MAIN_WEARABLE_SENSING_SERVICE_NAME =
"android.voiceinteraction.cts/android.voiceinteraction.service."
+ "MainWearableSensingService";
private static final ComponentName VIS_COMPONENT_NAME =
new ComponentName("android.voiceinteraction.cts", SERVICE_COMPONENT);
private static final int TEMPORARY_SERVICE_DURATION_MS = 10000;
private static final String KEY_WEARABLE_SENSING_SERVICE_ENABLED = "service_enabled";
private final CountDownLatch mLatch = new CountDownLatch(1);
private String mOpNoted = "";
private final AppOpsManager mAppOpsManager = sInstrumentation.getContext()
.getSystemService(AppOpsManager.class);
private final AppOpsManager.OnOpNotedListener mOnOpNotedListener =
(op, uid, pkgName, attributionTag, flags, result) -> {
Log.d(TAG, "Get OnOpNotedListener callback op = " + op + ", uid = " + uid);
// We adopt ShellPermissionIdentity for RECORD_AUDIO to pass the permission check,
// so the uid should be the shell uid.
if (Process.SHELL_UID == uid && op.equals(AppOpsManager.OPSTR_RECORD_AUDIO)) {
if (mLatch != null) {
mLatch.countDown();
}
}
// We do not adopt ShellPermissionIdentity for RECEIVE_SANDBOX_TRIGGER_AUDIO so the
// uid should be the current process uid.
if (Process.myUid() == uid && op.equals(RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR)) {
if (mLatch != null) {
mLatch.countDown();
}
}
mOpNoted = op;
};
private CtsBasicVoiceInteractionService mService;
private static String sWasIndicatorEnabled;
private static String sDefaultScreenOffTimeoutValue;
private static String sDefaultHotwordDetectionServiceRestartPeriodValue;
private static final Instrumentation sInstrumentation =
InstrumentationRegistry.getInstrumentation();
private static final PackageManager sPkgMgr = sInstrumentation.getContext().getPackageManager();
private static final WearableSensingManager sWearableSensingManager =
sInstrumentation.getContext().getSystemService(WearableSensingManager.class);
@Rule
public VoiceInteractionServiceConnectedRule mConnectedRule =
new VoiceInteractionServiceConnectedRule(
getInstrumentation().getTargetContext(), getTestVoiceInteractionService());
@Rule
public DisableAnimationRule mDisableAnimationRule = new DisableAnimationRule();
@Rule
public RequiredFeatureRule REQUIRES_MIC_RULE = new RequiredFeatureRule(FEATURE_MICROPHONE);
@Rule
public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private SoundTrigger.Keyphrase[] mKeyphraseArray;
private final SoundTriggerInstrumentationObserver mInstrumentationObserver =
new SoundTriggerInstrumentationObserver();
private final Executor mExecutor = Executors.newSingleThreadExecutor();
private String mOriginalWearableSensingServiceEnabledConfig;
@BeforeClass
public static void enableIndicators() {
sWasIndicatorEnabled = Helper.getIndicatorEnabledState();
Helper.setIndicatorEnabledState(Boolean.toString(true));
}
@AfterClass
public static void resetIndicators() {
Helper.setIndicatorEnabledState(sWasIndicatorEnabled);
}
@BeforeClass
public static void extendScreenOffTimeout() throws Exception {
// Change screen off timeout to 20 minutes.
sDefaultScreenOffTimeoutValue = SystemUtil.runShellCommand(
"settings get system screen_off_timeout");
SystemUtil.runShellCommand("settings put system screen_off_timeout 1200000");
}
@AfterClass
public static void restoreScreenOffTimeout() {
SystemUtil.runShellCommand(
"settings put system screen_off_timeout " + sDefaultScreenOffTimeoutValue);
}
@BeforeClass
public static void getHotwordDetectionServiceRestartPeriodValue() {
sDefaultHotwordDetectionServiceRestartPeriodValue =
Helper.getHotwordDetectionServiceRestartPeriod();
}
@AfterClass
public static void resetHotwordDetectionServiceRestartPeriodValue() {
Helper.setHotwordDetectionServiceRestartPeriod(
sDefaultHotwordDetectionServiceRestartPeriodValue);
}
@Before
public void setup() {
// VoiceInteractionServiceConnectedRule handles the service connected,
// the test should be able to get service
mService = (CtsBasicVoiceInteractionService) BaseVoiceInteractionService.getService();
// Check the test can get the service
Objects.requireNonNull(mService);
// Set whether voice activation permission check is enabled.
mService.setVoiceActivationPermissionEnabled(mVoiceActivationPermissionEnabled);
mKeyphraseArray = Helper.createKeyphraseArray(mService);
// Hook up SoundTriggerInjection to inject/observe STHAL operations.
// Requires MANAGE_SOUND_TRIGGER
runWithShellPermissionIdentity(() ->
mInstrumentationObserver.attachInstrumentation());
// Wait the original HotwordDetectionService finish clean up to avoid flaky
// This also waits for mic indicator disappear
SystemClock.sleep(5_000);
}
@After
public void tearDown() {
// Clean up any unexpected HAL state
try {
mInstrumentationObserver.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Set voice activation op to true for shell after each test.
runWithShellPermissionIdentity(() ->
setVoiceActivationOpForShellIdentity(/* allow= */ true));
mService.resetState();
mService = null;
clearTestableWearableSensingService();
}
public String getTestVoiceInteractionService() {
Log.d(TAG, "getTestVoiceInteractionService()");
return CTS_SERVICE_PACKAGE + "/" + SERVICE_COMPONENT;
}
@Test
public void testHotwordDetectionService_getMaxCustomInitializationStatus()
throws Throwable {
// TODO: not use Deprecated method
assertThat(HotwordDetectionService.getMaxCustomInitializationStatus()).isEqualTo(2);
}
@Test
public void testHotwordDetectionService_createDspDetector_sendOverMaxResult_getException()
throws Throwable {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Utils.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_OVER_MAX_INIT_STATUS);
try {
// Create AlwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false, persistableBundle);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// When the HotwordDetectionService sends the initialization status that overs the
// getMaxCustomInitializationStatus, the HotwordDetectionService will get the
// IllegalArgumentException. In order to test this case, we send the max custom
// initialization status when the HotwordDetectionService gets the
// IllegalArgumentException.
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
SandboxedDetectionInitializer.getMaxCustomInitializationStatus());
} finally {
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
if (alwaysOnHotwordDetector != null) {
alwaysOnHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_createSoftwareDetector_sendOverMaxResult_getException()
throws Throwable {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Utils.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_OVER_MAX_INIT_STATUS);
try {
// Create SoftwareHotwordDetector and wait result
mService.createSoftwareHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false, persistableBundle);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// When the HotwordDetectionService sends the initialization status that overs the
// getMaxCustomInitializationStatus, the HotwordDetectionService will get the
// IllegalArgumentException. In order to test this case, we send the max custom
// initialization status when the HotwordDetectionService gets the
// IllegalArgumentException.
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
SandboxedDetectionInitializer.getMaxCustomInitializationStatus());
} finally {
HotwordDetector softwareHotwordDetector = mService.getSoftwareHotwordDetector();
if (softwareHotwordDetector != null) {
softwareHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_createDspDetector_customResult_getCustomStatus()
throws Throwable {
final int customStatus = SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS + 1;
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Utils.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_CUSTOM_INIT_STATUS);
persistableBundle.putInt(Utils.KEY_INITIALIZATION_STATUS, customStatus);
try {
// Create AlwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false, persistableBundle);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify callback result
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
customStatus);
} finally {
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
if (alwaysOnHotwordDetector != null) {
alwaysOnHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_createSoftwareDetector_customResult_getCustomStatus()
throws Throwable {
final int customStatus = SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS + 1;
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Utils.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_CUSTOM_INIT_STATUS);
persistableBundle.putInt(Utils.KEY_INITIALIZATION_STATUS, customStatus);
try {
// Create SoftwareHotwordDetector and wait result
mService.createSoftwareHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false, persistableBundle);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify callback result
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
customStatus);
} finally {
HotwordDetector softwareHotwordDetector = mService.getSoftwareHotwordDetector();
if (softwareHotwordDetector != null) {
softwareHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_validHotwordDetectionComponentName_triggerSuccess()
throws Throwable {
// Create alwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetector();
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify callback result
// TODO: not use Deprecated variable
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
// The AlwaysOnHotwordDetector should be created correctly
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
Objects.requireNonNull(alwaysOnHotwordDetector);
alwaysOnHotwordDetector.destroy();
}
@Test
@CddTest(requirements = {"9.8/H-1-9"})
public void testVoiceInteractionService_withoutManageHotwordDetectionPermission_triggerFailure()
throws Throwable {
// Create alwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetectorWithoutManageHotwordDetectionPermission();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
public void testVoiceInteractionService_holdBindHotwordDetectionPermission_triggerFailure()
throws Throwable {
// Create alwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetectorHoldBindHotwordDetectionPermission();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
@CddTest(requirements = {"9.8/H-1-9"})
public void testVoiceInteractionService_createSoftwareWithoutPermission_triggerFailure()
throws Throwable {
// Create SoftwareHotwordDetector and wait result
mService.createSoftwareHotwordDetectorWithoutManageHotwordDetectionPermission();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
public void testVoiceInteractionService_createSoftwareBindHotwordDetectionPermission_Failure()
throws Throwable {
// Create SoftwareHotwordDetector and wait result
mService.createSoftwareHotwordDetectorHoldBindHotwordDetectionPermission();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
public void testVoiceInteractionService_disallowCreateAlwaysOnHotwordDetectorTwice()
throws Throwable {
final boolean enableMultipleHotwordDetectors = Helper.isEnableMultipleDetectors();
assumeTrue("Not support multiple hotword detectors", enableMultipleHotwordDetectors);
// Create first AlwaysOnHotwordDetector, it's fine.
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
// Create second AlwaysOnHotwordDetector, it will get the IllegalStateException due to
// the previous AlwaysOnHotwordDetector is not destroy.
mService.createAlwaysOnHotwordDetector();
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify IllegalStateException throws
assertThat(mService.isCreateDetectorIllegalStateExceptionThrow()).isTrue();
alwaysOnHotwordDetector.destroy();
}
@Test
public void testVoiceInteractionService_disallowCreateSoftwareHotwordDetectorTwice()
throws Throwable {
final boolean enableMultipleHotwordDetectors = Helper.isEnableMultipleDetectors();
assumeTrue("Not support multiple hotword detectors", enableMultipleHotwordDetectors);
// Create first SoftwareHotwordDetector and wait the HotwordDetectionService ready
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
// Create second SoftwareHotwordDetector, it will get the IllegalStateException due to
// the previous SoftwareHotwordDetector is not destroy.
mService.createSoftwareHotwordDetector();
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify IllegalStateException throws
assertThat(mService.isCreateDetectorIllegalStateExceptionThrow()).isTrue();
softwareHotwordDetector.destroy();
}
@Test
public void testHotwordDetectionService_processDied_triggerOnError() throws Throwable {
// Create first AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
mService.initOnErrorLatch();
// Use AlwaysOnHotwordDetector to test process died of HotwordDetectionService
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Helper.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_CRASH);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
mService.waitOnErrorCalled();
// ActivityManager will schedule a timer to restart the HotwordDetectionService due to
// we crash the service in this test case. It may impact the other test cases when
// ActivityManager restarts the HotwordDetectionService again. Add the sleep time to wait
// ActivityManager to restart the HotwordDetectionService, so that the service can be
// destroyed after finishing this test case.
Thread.sleep(5000);
alwaysOnHotwordDetector.destroy();
}
@Test
public void testHotwordDetectionService_processDied_triggerOnFailure() throws Throwable {
// Create alwaysOnHotwordDetector with onFailure callback
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
mService.initOnFailureLatch();
// Use AlwaysOnHotwordDetector to test process died of HotwordDetectionService
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Helper.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_CRASH);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_BINDING_DIED);
// ActivityManager will schedule a timer to restart the HotwordDetectionService due to
// we crash the service in this test case. It may impact the other test cases when
// ActivityManager restarts the HotwordDetectionService again. Add the sleep time to
// wait
// ActivityManager to restart the HotwordDetectionService, so that the service can be
// destroyed after finishing this test case.
Thread.sleep(5000);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_softwareDetector_processDied_triggerOnFailure()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/* useOnFailure= */ true);
try {
// Use SoftwareHotwordDetector to test process died of HotwordDetectionService
mService.initOnFailureLatch();
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Helper.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_CRASH);
softwareHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
// wait OnFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_BINDING_DIED);
// ActivityManager will schedule a timer to restart the HotwordDetectionService due to
// we crash the service in this test case. It may impact the other test cases when
// ActivityManager restarts the HotwordDetectionService again. Add the sleep time to
// wait ActivityManager to restart the HotwordDetectionService, so that the service
// can be destroyed after finishing this test case.
Thread.sleep(5000);
} finally {
// destroy detector
softwareHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_onDetectFromDspTimeout_triggerOnFailure()
throws Throwable {
// Create alwaysOnHotwordDetector with onFailure callback
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
// Update HotwordDetectionService options to delay detection, to cause a timeout
runWithShellPermissionIdentity(() -> {
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putInt(Utils.KEY_DETECTION_DELAY_MS, 5000);
alwaysOnHotwordDetector.updateState(options,
Helper.createFakeSharedMemoryData());
});
adoptShellPermissionIdentityForHotword();
mService.initOnFailureLatch();
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_DETECT_TIMEOUT);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testHotwordDetectionService_onDetectFromDspSecurityException_onFailure()
throws Throwable {
// Create alwaysOnHotwordDetector with onFailure callback
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
mService.initOnFailureLatch();
runWithShellPermissionIdentity(() -> {
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
});
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_onDetectFromExternalSourceSecurityException_onFailure()
throws Throwable {
// Create alwaysOnHotwordDetector with onFailure callback
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
mService.initOnFailureLatch();
runWithShellPermissionIdentity(() -> {
alwaysOnHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), Helper.createFakePersistableBundleData());
});
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_software_externalSourceSecurityException_onFailure()
throws Throwable {
// Create SoftwareHotwordDetector with onFailure callback
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
true);
try {
mService.initOnFailureLatch();
runWithShellPermissionIdentity(() -> {
softwareHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), Helper.createFakePersistableBundleData());
});
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION);
} finally {
// Destroy detector
softwareHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_onDetectFromMicSecurityException_onFailure()
throws Throwable {
// Create SoftwareHotwordDetector with onFailure callback
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
true);
try {
mService.initOnFailureLatch();
runWithShellPermissionIdentity(() -> {
softwareHotwordDetector.startRecognition();
});
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION);
} finally {
// destroy detector
softwareHotwordDetector.destroy();
}
}
@Test
@Ignore("b/272527340")
public void testHotwordDetectionService_onDetectFromExternalSourceAudioBroken_onFailure()
throws Throwable {
// Create alwaysOnHotwordDetector with onFailure callback
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
adoptShellPermissionIdentityForHotword();
// Create the ParcelFileDescriptor to read/write audio stream
final ParcelFileDescriptor[] parcelFileDescriptors = ParcelFileDescriptor.createPipe();
// After the client calls the startRecognition method, the system side will start to
// read the audio stream. When no data is read, the system side will normally end the
// process. If the client closes the audio stream when the system is still reading the
// audio stream, the system will get the IOException and use the onFailure callback to
// inform the client.
// In order to simulate the IOException case, it would be better to write 5 * 10 * 1024
// bytes data first before calling startRecognition to avoid the timing issue that no
// data is read from the system and make sure to close the audio stream during system
// is still reading the audio stream.
final CountDownLatch writeAudioStreamLatch = new CountDownLatch(5);
Executors.newCachedThreadPool().execute(() -> {
try (OutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(
parcelFileDescriptors[1])) {
byte[] largeData = new byte[10 * 1024];
int count = 1000;
while (count-- > 0) {
Random random = new Random();
random.nextBytes(largeData);
fos.write(largeData, 0, 10 * 1024);
writeAudioStreamLatch.countDown();
}
} catch (IOException e) {
Log.w(TAG, "Failed to pipe audio data : ", e);
}
});
writeAudioStreamLatch.await(WAIT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
mService.initOnFailureLatch();
alwaysOnHotwordDetector.startRecognition(parcelFileDescriptors[0],
Helper.createFakeAudioFormat(), Helper.createFakePersistableBundleData());
// Close the parcelFileDescriptors to cause the IOException when reading audio
// stream in the system side.
parcelFileDescriptors[0].close();
parcelFileDescriptors[1].close();
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
HotwordDetectionServiceFailure.ERROR_CODE_COPY_AUDIO_DATA_FAILURE);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresDevice
public void testHotwordDetectionService_createDetectorTwiceQuickly_triggerSuccess()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
// destroy software hotword detector
softwareHotwordDetector.destroy();
// Create AlwaysOnHotwordDetector
startWatchingNoted();
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
runWithShellPermissionIdentity(() -> {
// Update state with test scenario HotwordDetectionService can read audio and
// check the data is not zero
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
EXTRA_HOTWORD_DETECTION_SERVICE_CAN_READ_AUDIO_DATA_IS_NOT_ZERO);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotword();
verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(alwaysOnHotwordDetector);
// Verify RECORD_AUDIO noted
verifyRecordAudioNote(/* shouldNote= */ true);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
@CddTest(requirements = {"9.8/H-1-15"})
public void testHotwordDetectionServiceDspWithAudioEgress() throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
// start recognition and trigger recognition event via recognition session
alwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
recognitionSession.triggerRecognitionEvent(new byte[1024],
createKeyphraseRecognitionExtraList());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyAudioEgressDetectedResult(detectResult, AUDIO_EGRESS_DETECTED_RESULT);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
disableTestModel();
}
}
@Test
@CddTest(requirements = {"9.8/H-1-15"})
public void testHotwordDetectionService_softwareDetectorWithAudioEgress() throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
softwareHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, AUDIO_EGRESS_DETECTED_RESULT);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testCreateAlwaysOnHotwordDetector_withoutVoiceActivationPerm_throwsException()
throws Throwable {
// Ensure that the voice interaction service does not add additional voice activation
// permission when creating hotword detector.
mService.setVoiceActivationPermissionEnabled(false);
mService.createAlwaysOnHotwordDetector();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testCreateSoftwareHotwordDetector_withoutVoiceActivationPerm_throwsException()
throws Throwable {
// Ensure that the voice interaction service does not add additional voice activation
// permission when creating hotword detector.
mService.setVoiceActivationPermissionEnabled(false);
mService.createSoftwareHotwordDetector();
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify SecurityException throws
assertThat(mService.isCreateDetectorSecurityExceptionThrow()).isTrue();
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testSoftwareHotwordDetector_onDetect_withoutVoiceActivationOp_throwsException()
throws Throwable {
// Create SoftwareHotwordDetector.
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/* useOnFailure= */ true);
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Disable voice activation op.
setVoiceActivationOpForShellIdentity(/* allow= */ false);
softwareHotwordDetector.startRecognition();
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testSoftwareExternalHotwordDetector_onDetect_withoutVoiceActivOp_throwsException()
throws Throwable {
// Create SoftwareHotwordDetector.
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/* useOnFailure= */ true);
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Disable voice activation op.
setVoiceActivationOpForShellIdentity(/* allow= */ false);
softwareHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), Helper.createFakePersistableBundleData());
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testAlwaysOnHotwordDetector_onDetect_withoutVoiceActivationOp_throwsException()
throws Throwable {
// Create SoftwareHotwordDetector.
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Disable voice activation op.
setVoiceActivationOpForShellIdentity(/* allow= */ false);
// Trigger test detection event.
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testSoftwareDetector_voiceActivationOpDisabledDuringDetection_shutdownHds()
throws Throwable {
// Create SoftwareHotwordDetector.
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/* useOnFailure= */ true);
// Update HotwordDetectionService options to delay detection by a bit.
runWithShellPermissionIdentity(() -> {
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putInt(Utils.KEY_DETECTION_DELAY_MS, 2000);
softwareHotwordDetector.updateState(options,
Helper.createFakeSharedMemoryData());
});
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Trigger test detection event.
softwareHotwordDetector.startRecognition();
// Reset voice activation op during detection being run (should shutdown HDS).
setVoiceActivationOpForShellIdentity(/* allow= */ false);
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testSoftwareExternalDetector_voiceActivationOpDisabledDuringDetection_shutdownHds()
throws Throwable {
// Create SoftwareHotwordDetector.
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/* useOnFailure= */ true);
// Update HotwordDetectionService options to delay detection by a bit.
runWithShellPermissionIdentity(() -> {
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putInt(Utils.KEY_DETECTION_DELAY_MS, 2000);
softwareHotwordDetector.updateState(options,
Helper.createFakeSharedMemoryData());
});
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Trigger test detection event.
softwareHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), Helper.createFakePersistableBundleData());
// Reset voice activation op during detection being run (should shutdown HDS).
setVoiceActivationOpForShellIdentity(/* allow= */ false);
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(Flags.FLAG_VOICE_ACTIVATION_PERMISSION_APIS)
public void testAlwaysOnHotwordDetector_voiceActivationOpDisabledDuringDetection_shutdownHds()
throws Throwable {
// Create SoftwareHotwordDetector.
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ true);
// Update HotwordDetectionService options to delay detection by a bit.
runWithShellPermissionIdentity(() -> {
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putInt(Utils.KEY_DETECTION_DELAY_MS, 2000);
alwaysOnHotwordDetector.updateState(options,
Helper.createFakeSharedMemoryData());
});
try {
// Adopt necessary permissions.
adoptShellPermissionIdentityForHotwordWithVoiceActivation();
mService.initOnFailureLatch();
// Trigger test detection event.
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
// Reset voice activation op during detection being run (should shutdown HDS).
setVoiceActivationOpForShellIdentity(/* allow= */ false);
// wait onFailure() called and verify the result
mService.waitOnFailureCalled();
verifyHotwordDetectionServiceFailure(mService.getHotwordDetectionServiceFailure(),
ERROR_CODE_SHUTDOWN_HDS_ON_VOICE_ACTIVATION_OP_DISABLED);
} finally {
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@CddTest(requirements = {"9.8/H-1-15"})
public void testHotwordDetectionService_onDetectFromExternalSourceWithAudioEgress()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
ParcelFileDescriptor audioStream = Helper.createFakeAudioStream();
mService.initDetectRejectLatch();
alwaysOnHotwordDetector.startRecognition(audioStream,
Helper.createFakeAudioFormat(),
Helper.createFakePersistableBundleData());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, AUDIO_EGRESS_DETECTED_RESULT);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_onDetectFromWearableWithAudioEgress() throws Throwable {
assumeFalse(isWatch()); // WearableSensingManagerService is not supported on WearOS
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
setupForWearableTests(alwaysOnHotwordDetector);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
VIS_COMPONENT_NAME,
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
mService.initDetectRejectLatch();
sendAudioStreamFromWearable();
// wait for onDetected() to be called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, AUDIO_EGRESS_DETECTED_RESULT);
// Wait for the async call into WearableSensingService.
SystemClock.sleep(2000);
verifyWearableSensingServiceHotwordValidatedCalled();
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_onDetectFromWearable_doesNotNoteRecordAudioOp()
throws Throwable {
assumeFalse(isWatch());
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
setupForWearableTests(alwaysOnHotwordDetector);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
VIS_COMPONENT_NAME,
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
mService.initDetectRejectLatch();
sendAudioStreamFromWearable();
// wait for onDetected() to be called
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
// make sure this is onDetected and not onRejected
assertThat(detectResult).isNotNull();
verifyRecordAudioNote(/* shouldNote= */ false);
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_onRejectWearableHotword_notifiesWearable()
throws Throwable {
assumeFalse(isWatch()); // WearableSensingManagerService is not supported on WearOS
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
setupForWearableTests(alwaysOnHotwordDetector);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
VIS_COMPONENT_NAME,
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
sendNonHotwordAudioStreamFromWearable();
// Wait for the async call into WearableSensingService.
SystemClock.sleep(2000);
verifyWearableSensingServiceAudioStopCalled();
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_receivesOptionsFromWearable() throws Throwable {
assumeFalse(isWatch()); // WearableSensingManagerService is not supported on WearOS
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
setupForWearableTests(alwaysOnHotwordDetector);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
VIS_COMPONENT_NAME,
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
mService.initDetectRejectLatch();
// The HotwordDetectionService should reject the non-hotword audio stream, but if it
// receives the expected options, it will call onDetect instead. This is an indirect
// way to verify that options are received because it is difficult to send a message
// from HotwordDetectionService back to this test.
sendNonHotwordAudioStreamWithAcceptDetectionOptionsFromWearable();
// verify that onDetect is called
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
assertThat(detectResult).isNotNull();
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_wearableHotwordWithWrongVisComponent_notifiesWearable()
throws Throwable {
assumeFalse(isWatch()); // WearableSensingManagerService is not supported on WearOS
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
setupForWearableTests(alwaysOnHotwordDetector);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
new ComponentName("my.package", "my.package.MyClass"),
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
mService.initDetectRejectLatch();
sendAudioStreamFromWearable();
// Wait for the async call into WearableSensingService.
SystemClock.sleep(2000);
verifyWearableSensingServiceAudioStopCalled();
// The audio stream is not sent to HDS, so neither onDetect nor onReject should be
// called
assertThrows(AssertionError.class, () -> mService.waitOnDetectOrRejectCalled());
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
@RequiresFlagsEnabled(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public void testHotwordDetectionService_closePipeInWearableHotwordResult_notifiesWearable()
throws Throwable {
assumeFalse(isWatch()); // WearableSensingManagerService is not supported on WearOS
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
// Do not close the audio stream in HotwordDetectionService immediately after
// read. Wait until this test requests it.
setupForWearableTests(alwaysOnHotwordDetector, /* closeStreamAfterRead= */ false);
CountDownLatch statusLatch = new CountDownLatch(1);
sWearableSensingManager.startHotwordRecognition(
VIS_COMPONENT_NAME,
mExecutor,
(status) -> {
statusLatch.countDown();
});
assertThat(statusLatch.await(3, SECONDS)).isTrue();
mService.initDetectRejectLatch();
sendAudioStreamFromWearable();
// wait for onDetected() called and close the PFD in the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
detectResult
.getHotwordDetectedResult()
.getAudioStreams()
.get(0)
.getAudioStreamParcelFileDescriptor()
.close();
/*
* Check if the output PFD sent by HotwordDetectionService is broken (it should).
* If yes, HotwordDetectionService will close the audio stream it received.
* This is indirect because there is no simple way for HotwordDetectionService to
* propagate a test failure signal back to the test driver.
*/
runWithShellPermissionIdentity(
() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putBoolean(
Utils.KEY_CLOSE_INPUT_AUDIO_STREAM_IF_OUTPUT_PIPE_BROKEN, true);
alwaysOnHotwordDetector.updateState(
persistableBundle, Helper.createFakeSharedMemoryData());
},
MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotwordAndWearableSensing();
// Wait for all the PFDs involved with the call above to be cleaned up
SystemClock.sleep(4000);
/*
* Write more data from wearable onto the same stream. This should trigger a pipe
* broken exception in the system_server thread that copies from the output of
* WearableSensingService to the input of HotwordDetectionService, which triggers
* the callback to stop the wearable stream.
*/
sendMoreAudioDataFromWearable();
// Wait for the async call into WearableSensingService.
SystemClock.sleep(2000);
verifyWearableSensingServiceAudioStopCalled();
} finally {
cleanupForWearableTests(alwaysOnHotwordDetector);
}
}
@Test
public void testHotwordDetectionServiceDspWithAudioEgressWrongCopyBufferSize()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
persistableBundle.putBoolean(Utils.KEY_AUDIO_EGRESS_USE_ILLEGAL_COPY_BUFFER_SIZE, true);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
// start recognition and trigger recognition event via recognition session
alwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
recognitionSession.triggerRecognitionEvent(new byte[1024],
createKeyphraseRecognitionExtraList());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyAudioEgressDetectedResult(detectResult,
AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
disableTestModel();
}
}
@Test
public void testHotwordDetectionService_softwareDetectorWithAudioEgressWrongCopyBufferSize()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
persistableBundle.putBoolean(Utils.KEY_AUDIO_EGRESS_USE_ILLEGAL_COPY_BUFFER_SIZE, true);
softwareHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyAudioEgressDetectedResult(detectResult,
AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testHotwordDetectionService_externalSourceWithAudioEgressWrongCopyBufferSize()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
persistableBundle.putBoolean(Utils.KEY_AUDIO_EGRESS_USE_ILLEGAL_COPY_BUFFER_SIZE, true);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
try {
adoptShellPermissionIdentityForHotword();
ParcelFileDescriptor audioStream = Helper.createFakeAudioStream();
mService.initDetectRejectLatch();
alwaysOnHotwordDetector.startRecognition(audioStream,
Helper.createFakeAudioFormat(),
Helper.createFakePersistableBundleData());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyAudioEgressDetectedResult(detectResult,
AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@CddTest(requirement = "9.8/H-1-2,H-1-8,H-1-14")
public void testHotwordDetectionService_onDetectFromDsp_success() throws Throwable {
startWatchingNoted();
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
adoptShellPermissionIdentityForHotword();
verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(alwaysOnHotwordDetector);
// Verify RECORD_AUDIO noted
verifyRecordAudioNote(/* shouldNote= */ true);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
public void testHotwordDetectionService_onDetectFromDsp_rejection() throws Throwable {
startWatchingNoted();
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
try {
mService.initDetectRejectLatch();
runWithShellPermissionIdentity(() -> {
// pass null data parameter
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), null,
Helper.createFakeKeyphraseRecognitionExtraList());
});
// wait onRejected() called and verify the result
mService.waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectedResult =
mService.getHotwordServiceOnRejectedResult();
assertThat(rejectedResult).isEqualTo(Helper.REJECTED_RESULT);
// Verify RECORD_AUDIO does not note
verifyRecordAudioNote(/* shouldNote= */ false);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
stopWatchingNoted();
}
}
@Test
@CddTest(requirement = "9.8/H-1-3")
public void testHotwordDetectionService_onDetectFromDsp_timeout() throws Throwable {
startWatchingNoted();
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
// Update HotwordDetectionService options to delay detection, to cause a timeout
runWithShellPermissionIdentity(() -> {
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putInt(Utils.KEY_DETECTION_DELAY_MS, 5000);
alwaysOnHotwordDetector.updateState(options,
Helper.createFakeSharedMemoryData());
});
try {
adoptShellPermissionIdentityForHotword();
mService.initOnErrorLatch();
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
// wait onError() called and verify the result
mService.waitOnErrorCalled();
// Verify RECORD_AUDIO does not note
verifyRecordAudioNote(/* shouldNote= */ false);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
public void testHotwordDetectionService_destroyDspDetector_activeDetectorRemoved()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
// destroy detector
alwaysOnHotwordDetector.destroy();
try {
adoptShellPermissionIdentityForHotword();
assertThrows(IllegalStateException.class, () -> {
// Can no longer use the detector because it is in an invalid state
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), new byte[1024],
Helper.createFakeKeyphraseRecognitionExtraList());
});
} finally {
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@CddTest(requirement = "9.8/H-1-2,H-1-8,H-1-14")
public void testHotwordDetectionService_onDetectFromExternalSource_success() throws Throwable {
startWatchingNoted();
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
try {
adoptShellPermissionIdentityForHotword();
ParcelFileDescriptor audioStream = Helper.createFakeAudioStream();
mService.initDetectRejectLatch();
alwaysOnHotwordDetector.startRecognition(audioStream,
Helper.createFakeAudioFormat(),
Helper.createFakePersistableBundleData());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
// Verify RECORD_AUDIO noted
verifyRecordAudioNote(/* shouldNote= */ true);
} finally {
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
public void testHotwordDetectionService_dspDetector_onDetectFromExternalSource_rejected()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
try {
adoptShellPermissionIdentityForHotword();
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putBoolean(Utils.KEY_DETECTION_REJECTED, true);
mService.initDetectRejectLatch();
alwaysOnHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), options);
// Wait onRejected() called and verify the result
mService.waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectedResult =
mService.getHotwordServiceOnRejectedResult();
assertThat(rejectedResult).isEqualTo(Helper.REJECTED_RESULT);
} finally {
// Destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testHotwordDetectionService_softwareDetector_onDetectFromExternalSource_rejected()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
try {
adoptShellPermissionIdentityForHotword();
PersistableBundle options = Helper.createFakePersistableBundleData();
options.putBoolean(Utils.KEY_DETECTION_REJECTED, true);
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition(Helper.createFakeAudioStream(),
Helper.createFakeAudioFormat(), options);
// Wait onRejected() called and verify the result
mService.waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectedResult =
mService.getHotwordServiceOnRejectedResult();
assertThat(rejectedResult).isEqualTo(Helper.REJECTED_RESULT);
} finally {
// Destroy detector
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@CddTest(requirement = "9.8/H-1-2,H-1-8,H-1-14")
public void testHotwordDetectionService_onDetectFromMic_success() throws Throwable {
startWatchingNoted();
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
try {
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
// Verify RECORD_AUDIO noted
verifyRecordAudioNote(/* shouldNote= */ true);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
public void testHotwordDetectionService_destroySoftwareDetector_activeDetectorRemoved()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
// Destroy SoftwareHotwordDetector
softwareHotwordDetector.destroy();
try {
adoptShellPermissionIdentityForHotword();
// Can no longer use the detector because it is in an invalid state
assertThrows(IllegalStateException.class, softwareHotwordDetector::startRecognition);
} finally {
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testHotwordDetectionService_onStopDetection() throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
try {
adoptShellPermissionIdentityForHotword();
// The HotwordDetectionService can't report any result after recognition is stopped. So
// restart it after stopping; then the service can report a special result.
softwareHotwordDetector.startRecognition();
softwareHotwordDetector.stopRecognition();
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT_AFTER_STOP_DETECTION);
} finally {
softwareHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
@RequiresDevice
public void testHotwordDetectionService_concurrentCapture() throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
try {
SystemUtil.runWithShellPermissionIdentity(() -> {
AudioRecord record =
new AudioRecord.Builder()
.setAudioAttributes(
new AudioAttributes.Builder()
.setInternalCapturePreset(
MediaRecorder.AudioSource.MIC).build())
.setAudioFormat(
new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build())
.setBufferSizeInBytes(10240) // something large enough to not fail
.build();
assertThat(record.getState()).isEqualTo(AudioRecord.STATE_INITIALIZED);
try {
record.startRecording();
// Update state with test scenario HotwordDetectionService can read audio
// and check the data is not zero
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
EXTRA_HOTWORD_DETECTION_SERVICE_CAN_READ_AUDIO_DATA_IS_NOT_ZERO);
softwareHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
// TODO: Test that it still works after restarting the process or killing audio
// server.
} finally {
record.release();
}
});
} finally {
softwareHotwordDetector.destroy();
}
}
@Test
@RequiresDevice
public void testMultipleDetectors_onDetectFromDspAndMic_success() throws Throwable {
assumeTrue("Not support multiple hotword detectors",
Helper.isEnableMultipleDetectors());
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector = createSoftwareHotwordDetector(/* useOnFailure= */
false);
try {
runWithShellPermissionIdentity(() -> {
// Update state with test scenario HotwordDetectionService can read audio and
// check the data is not zero
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
EXTRA_HOTWORD_DETECTION_SERVICE_CAN_READ_AUDIO_DATA_IS_NOT_ZERO);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotword();
// Test AlwaysOnHotwordDetector to be able to detect well
verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(
alwaysOnHotwordDetector, /* shouldDisableTestModel= */ false);
// Test SoftwareHotwordDetector to be able to detect well
verifySoftwareDetectorDetectSuccess(softwareHotwordDetector);
} finally {
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
// Destroy the always on detector
alwaysOnHotwordDetector.destroy();
// Destroy the software detector
softwareHotwordDetector.destroy();
disableTestModel();
}
}
@Test
public void testHotwordDetectionService_onHotwordDetectionServiceRestarted() throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
mService.initOnHotwordDetectionServiceRestartedLatch();
// force re-start by shell command
runShellCommand("cmd voiceinteraction restart-detection");
// wait onHotwordDetectionServiceRestarted() called
mService.waitOnHotwordDetectionServiceRestartedCalled();
// Destroy the always on detector
alwaysOnHotwordDetector.destroy();
}
@Test
public void testHotwordDetectionService_dspDetector_serviceScheduleRestarted()
throws Throwable {
// Change the period of restarting hotword detection
final String restartPeriod = Helper.getHotwordDetectionServiceRestartPeriod();
Helper.setHotwordDetectionServiceRestartPeriod("8");
try {
mService.initOnHotwordDetectionServiceRestartedLatch();
// Create AlwaysOnHotwordDetector and wait result
mService.createAlwaysOnHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify callback result
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
// Wait onHotwordDetectionServiceRestarted() called
mService.waitOnHotwordDetectionServiceRestartedCalled();
} finally {
Helper.setHotwordDetectionServiceRestartPeriod(restartPeriod);
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
if (alwaysOnHotwordDetector != null) {
alwaysOnHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_softwareDetector_serviceScheduleRestarted()
throws Throwable {
// Change the period of restarting hotword detection
final String restartPeriod = Helper.getHotwordDetectionServiceRestartPeriod();
Helper.setHotwordDetectionServiceRestartPeriod("8");
try {
mService.initOnHotwordDetectionServiceRestartedLatch();
// Create AlwaysOnHotwordDetector and wait result
mService.createSoftwareHotwordDetector(/* useExecutor= */ false, /* runOnMainThread= */
false);
// Wait the result and verify expected result
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// Verify callback result
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
// Wait onHotwordDetectionServiceRestarted() called
mService.waitOnHotwordDetectionServiceRestartedCalled();
} finally {
Helper.setHotwordDetectionServiceRestartPeriod(restartPeriod);
HotwordDetector softwareHotwordDetector = mService.getSoftwareHotwordDetector();
if (softwareHotwordDetector != null) {
softwareHotwordDetector.destroy();
}
}
}
@Test
public void testHotwordDetectionService_onDetectedTwice_clientOnlyOneOnDetected()
throws Throwable {
// Create SoftwareHotwordDetector
HotwordDetector softwareHotwordDetector =
createSoftwareHotwordDetector(/*useOnFailure=*/ false);
try {
runWithShellPermissionIdentity(() -> {
// Update state with test scenario unexpected onDetect callback
// HDS will call back onDetected() twice
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_UNEXPECTED_CALLBACK);
softwareHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotword();
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and only once (even HDS callback onDetected() many times,
// only one onDetected() on VIS will be called)
mService.waitOnDetectOrRejectCalled();
// Wait for a while to make sure no 2nd onDetected() will be called
Thread.sleep(500);
assertThat(mService.getSoftwareOnDetectedCount()).isEqualTo(1);
} finally {
softwareHotwordDetector.destroy();
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testHotwordDetectionService_dspDetector_onDetectedTwice_clientOnlyOneOnDetected()
throws Throwable {
startWatchingNoted();
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
runWithShellPermissionIdentity(() -> {
// Update state with test scenario unexpected onDetect callback
// HDS will call back onDetected() twice
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_UNEXPECTED_CALLBACK);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotword();
verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(alwaysOnHotwordDetector);
// Verify RECORD_AUDIO noted
verifyRecordAudioNote(/* shouldNote= */ true);
// Wait for a while to make sure no 2nd onDetected() will be called
Thread.sleep(500);
assertThat(mService.getDspOnDetectedCount()).isEqualTo(1);
} finally {
// Destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
stopWatchingNoted();
}
}
@Test
public void testHotwordDetectionService_dspDetector_onRejectedTwice_clientOnlyOneOnRejected()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetector(/* useOnFailure= */ false);
try {
runWithShellPermissionIdentity(() -> {
// Update state with test scenario unexpected onRejected callback
// HDS will call back onRejected() twice
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_UNEXPECTED_CALLBACK);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
mService.initDetectRejectLatch();
runWithShellPermissionIdentity(() -> {
// Pass null data parameter
alwaysOnHotwordDetector.triggerHardwareRecognitionEventForTest(
/* status= */ 0, /* soundModelHandle= */ 100,
/* halEventReceivedMillis */ 12345, /* captureAvailable= */ true,
/* captureSession= */ 101, /* captureDelayMs= */ 1000,
/* capturePreambleMs= */ 1001, /* triggerInData= */ true,
Helper.createFakeAudioFormat(), null,
Helper.createFakeKeyphraseRecognitionExtraList());
});
// Wait onRejected() called and verify the result
mService.waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectedResult =
mService.getHotwordServiceOnRejectedResult();
assertThat(rejectedResult).isEqualTo(Helper.REJECTED_RESULT);
// Wait for a while to make sure no 2nd onDetected() will be called
Thread.sleep(500);
assertThat(mService.getDspOnRejectedCount()).isEqualTo(1);
} finally {
// Destroy detector
alwaysOnHotwordDetector.destroy();
}
}
@Test
public void testHotwordDetectionService_dspDetector_duringOnDetect_serviceRestart()
throws Throwable {
// Create AlwaysOnHotwordDetector
AlwaysOnHotwordDetector alwaysOnHotwordDetector =
createAlwaysOnHotwordDetectorWithSoundTriggerInjection();
try {
runWithShellPermissionIdentity(() -> {
// Inform the HotwordDetectionService to ignore the onDetect
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_NO_NEED_ACTION_DURING_DETECTION);
alwaysOnHotwordDetector.updateState(
persistableBundle,
Helper.createFakeSharedMemoryData());
}, MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotword();
alwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
// Verify we received model load, recognition start
recognitionSession.triggerRecognitionEvent(new byte[1024],
createKeyphraseRecognitionExtraList());
// Wait for a while to make sure onDetect() will be called in the
// MainHotwordDetectionService
Thread.sleep(500);
mService.initDetectRejectLatch();
// Force re-start by shell command
runShellCommand("cmd voiceinteraction restart-detection");
// Wait onRejected() called and verify the result
mService.waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectedResult =
mService.getHotwordServiceOnRejectedResult();
assertThat(rejectedResult).isNotNull();
} finally {
// Destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
disableTestModel();
}
}
/**
* Before using this method, please call {@link #adoptShellPermissionIdentityForHotword()} to
* grant permissions. And please use
* {@link #createAlwaysOnHotwordDetectorWithSoundTriggerInjection()} to create
* AlwaysOnHotwordDetector. This method will disable test model after verifying the result.
* If you want to disable the test model manually, please use
* {@link #verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(
* AlwaysOnHotwordDetector, boolean)}
*/
private void verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(
AlwaysOnHotwordDetector alwaysOnHotwordDetector) throws Throwable {
verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(
alwaysOnHotwordDetector, /* shouldDisableTestModel= */ true);
}
/**
* Before using this method, please call {@link #adoptShellPermissionIdentityForHotword()} to
* grant permissions. And please use
* {@link #createAlwaysOnHotwordDetectorWithSoundTriggerInjection()} to create
* AlwaysOnHotwordDetector. If {@code shouldDisableTestModel} is true, it will disable the test
* model. Otherwise, it doesn't disable the test model. Please call {@link #disableTestModel}
* manually.
*/
private void verifyOnDetectFromDspWithSoundTriggerInjectionSuccess(
AlwaysOnHotwordDetector alwaysOnHotwordDetector,
boolean shouldDisableTestModel) throws Throwable {
try {
alwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
mService.initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[1024],
createKeyphraseRecognitionExtraList());
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
} finally {
if (shouldDisableTestModel) {
disableTestModel();
}
}
}
private void disableTestModel() {
runWithShellPermissionIdentity(() -> mService.disableOverrideRegisterModel(),
MANAGE_VOICE_KEYPHRASES);
}
private void verifySoftwareDetectorDetectSuccess(HotwordDetector softwareHotwordDetector)
throws Exception {
mService.initDetectRejectLatch();
softwareHotwordDetector.startRecognition();
// wait onDetected() called and verify the result
mService.waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
mService.getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
private void verifyHotwordDetectionServiceFailure(
HotwordDetectionServiceFailure hotwordDetectionServiceFailure, int errorCode)
throws Throwable {
assertThat(hotwordDetectionServiceFailure).isNotNull();
assertThat(hotwordDetectionServiceFailure.getErrorCode()).isEqualTo(errorCode);
}
/**
* Create software hotword detector and wait for ready
*/
private HotwordDetector createSoftwareHotwordDetector(boolean useOnFailure) throws Throwable {
// Create SoftwareHotwordDetector
if (useOnFailure) {
mService.createSoftwareHotwordDetectorWithOnFailureCallback(/* useExecutor= */
false, /* runOnMainThread= */ false);
} else {
mService.createSoftwareHotwordDetector();
}
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify callback result
// TODO: not use Deprecated variable
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
HotwordDetector softwareHotwordDetector = mService.getSoftwareHotwordDetector();
Objects.requireNonNull(softwareHotwordDetector);
return softwareHotwordDetector;
}
/**
* Create AlwaysOnHotwordDetector and wait for ready
*/
private AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(boolean useOnFailure)
throws Throwable {
// Create AlwaysOnHotwordDetector and wait ready.
if (useOnFailure) {
mService.createAlwaysOnHotwordDetectorWithOnFailureCallback(/* useExecutor= */
false, /* runOnMainThread= */ false);
} else {
mService.createAlwaysOnHotwordDetector();
}
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify callback result
// TODO: not use Deprecated variable
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
Objects.requireNonNull(alwaysOnHotwordDetector);
return alwaysOnHotwordDetector;
}
/**
* Create AlwaysOnHotwordDetector with SoundTrigger injection. Please call
* {@link #verifyOnDetectFromDspWithSoundTriggerInjectionSuccess} or {@link #disableTestModel()}
* before finishing the test.
*/
private AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorWithSoundTriggerInjection()
throws Throwable {
final UUID uuid = new UUID(5, 7);
final UUID vendorUuid = new UUID(7, 5);
// Start override enrolled model with a custom model for test purposes.
runWithShellPermissionIdentity(() -> mService.enableOverrideRegisterModel(
new SoundTrigger.KeyphraseSoundModel(uuid, vendorUuid, /* data= */ null,
mKeyphraseArray)), MANAGE_VOICE_KEYPHRASES);
// Call initAvailabilityChangeLatch for onAvailabilityChanged() callback called
// following AlwaysOnHotwordDetector creation.
mService.initAvailabilityChangeLatch();
// Create AlwaysOnHotwordDetector and wait ready.
mService.createAlwaysOnHotwordDetector();
mService.waitSandboxedDetectionServiceInitializedCalledOrException();
// verify initialization callback result
// TODO: not use Deprecated variable
assertThat(mService.getSandboxedDetectionServiceInitializedResult()).isEqualTo(
HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS);
AlwaysOnHotwordDetector alwaysOnHotwordDetector = mService.getAlwaysOnHotwordDetector();
Objects.requireNonNull(alwaysOnHotwordDetector);
// verify have entered the ENROLLED state
mService.waitAvailabilityChangedCalled();
assertThat(mService.getHotwordDetectionServiceAvailabilityResult()).isEqualTo(
AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED);
return alwaysOnHotwordDetector;
}
private void adoptShellPermissionIdentityForHotword() {
// Drop any identity adopted earlier.
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.dropShellPermissionIdentity();
// need to retain the identity until the callback is triggered
uiAutomation.adoptShellPermissionIdentity(RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD);
}
private void adoptShellPermissionIdentityForHotwordAndWearableSensing() {
// Drop any identity adopted earlier.
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.dropShellPermissionIdentity();
// need to retain the identity until the callback is triggered
uiAutomation.adoptShellPermissionIdentity(
RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD, MANAGE_WEARABLE_SENSING_SERVICE);
}
private void adoptShellPermissionIdentityForHotwordWithVoiceActivation() {
// Drop any identity adopted earlier.
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.dropShellPermissionIdentity();
// need to retain the identity until the callback is triggered
uiAutomation.adoptShellPermissionIdentity(RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD,
RECEIVE_SANDBOX_TRIGGER_AUDIO, MANAGE_APP_OPS_MODES);
}
private void startWatchingNoted() {
runWithShellPermissionIdentity(() -> {
if (mAppOpsManager != null) {
mAppOpsManager.startWatchingNoted(new String[]{
AppOpsManager.OPSTR_RECORD_AUDIO, RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR},
mOnOpNotedListener);
}
});
}
private void stopWatchingNoted() {
runWithShellPermissionIdentity(() -> {
if (mAppOpsManager != null) {
mAppOpsManager.stopWatchingNoted(mOnOpNotedListener);
}
});
}
private void verifyRecordAudioNote(boolean shouldNote) throws Exception {
if (sPkgMgr.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// TODO: test TV indicator
} else if (sPkgMgr.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
// TODO: test Auto indicator
} else if (sPkgMgr.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
// The privacy chips/indicators are not implemented on Wear
} else if (mVoiceActivationPermissionEnabled) {
boolean isNoted = mLatch.await(Helper.CLEAR_CHIP_MS, TimeUnit.MILLISECONDS);
assertThat(isNoted).isEqualTo(shouldNote);
if (isNoted) {
assertThat(mOpNoted).isEqualTo(RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR);
}
} else {
boolean isNoted = mLatch.await(Helper.CLEAR_CHIP_MS, TimeUnit.MILLISECONDS);
assertThat(isNoted).isEqualTo(shouldNote);
if (isNoted) {
assertThat(mOpNoted).isEqualTo(AppOpsManager.OPSTR_RECORD_AUDIO);
}
}
}
/**
* This method sets the voice activation app op for shell identity.
* <p> This should be used if
* {@link HotwordDetectionServiceBasicTest#adoptShellPermissionIdentityForHotwordWithVoiceActivation()}
* has been called and should only be called while the permission identity is in effect. When
* adopting shell permission identity, both permissions and app-op checks are redirected from
* the test app to the shell identity (package name: "com.android.shell").
* See {@link com.android.server.am.ActivityManagerService#startDelegateShellPermissionIdentity(int, String[])}
* to see how the delegation is performed </p>
*/
private void setVoiceActivationOpForShellIdentity(boolean allow) {
// We expect this call to be made after
// adoptShellPermissionIdentityForHotwordWithVoiceActivation() is in effect so that we have
// MANAGE_OP_MODES permission. While we could wrap this call run with shell permission
// identity, that would OVERRIDE any existing permission adopted and break the flow of
// running tests.
mAppOpsManager.setUidMode(RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR,
UserHandle.getUid(UserHandle.getUserId(Process.myUid()), Process.SHELL_UID),
allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
}
private void setupForWearableTests(AlwaysOnHotwordDetector alwaysOnHotwordDetector)
throws Exception {
setupForWearableTests(alwaysOnHotwordDetector, true);
}
private void setupForWearableTests(
AlwaysOnHotwordDetector alwaysOnHotwordDetector, boolean closeStreamAfterRead)
throws Exception {
mOriginalWearableSensingServiceEnabledConfig =
getWearableSensingServiceEnabledDeviceConfig();
setWearableSensingServiceEnabledDeviceConfig("true");
// Update HotwordDetectionService options to enable Audio egress
runWithShellPermissionIdentity(
() -> {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt(
Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS);
persistableBundle.putBoolean(
Utils.KEY_AUDIO_EGRESS_CLOSE_AUDIO_STREAM_AFTER_READ,
closeStreamAfterRead);
alwaysOnHotwordDetector.updateState(
persistableBundle, Helper.createFakeSharedMemoryData());
},
MANAGE_HOTWORD_DETECTION);
adoptShellPermissionIdentityForHotwordAndWearableSensing();
setTestableWearableSensingService();
resetWearableSensingServiceStates();
alwaysOnHotwordDetector.startRecognition(0, new byte[] {1, 2, 3, 4, 5});
RecognitionSession recognitionSession =
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
}
private void cleanupForWearableTests(AlwaysOnHotwordDetector alwaysOnHotwordDetector)
throws Exception {
if (mOriginalWearableSensingServiceEnabledConfig != null) {
setWearableSensingServiceEnabledDeviceConfig(
mOriginalWearableSensingServiceEnabledConfig);
mOriginalWearableSensingServiceEnabledConfig = null;
}
// destroy detector
alwaysOnHotwordDetector.destroy();
// Drop identity adopted.
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
clearTestableWearableSensingService();
}
private static String getWearableSensingServiceEnabledDeviceConfig() {
return runWithShellPermissionIdentity(
() -> {
return DeviceConfig.getProperty(
DeviceConfig.NAMESPACE_WEARABLE_SENSING,
KEY_WEARABLE_SENSING_SERVICE_ENABLED);
});
}
private static void setWearableSensingServiceEnabledDeviceConfig(String newValue) {
runWithShellPermissionIdentity(
() -> {
DeviceConfig.setProperty(
DeviceConfig.NAMESPACE_WEARABLE_SENSING,
KEY_WEARABLE_SENSING_SERVICE_ENABLED,
newValue,
/* makeDefault= */ false);
});
}
/** Temporarily sets the WearableSensingService to the test implementation. */
private void setTestableWearableSensingService() {
runShellCommand(
"cmd wearable_sensing set-temporary-service %d %s %d",
USER_ID, MAIN_WEARABLE_SENSING_SERVICE_NAME, TEMPORARY_SERVICE_DURATION_MS);
}
/** Resets the WearableSensingService implementation. */
private void clearTestableWearableSensingService() {
runShellCommand("cmd wearable_sensing set-temporary-service %d", USER_ID);
}
private void resetWearableSensingServiceStates() throws Exception {
provideDataToWearableSensingServiceAndWait(MainWearableSensingService.ACTION_RESET);
}
private void sendAudioStreamFromWearable() throws Exception {
provideDataToWearableSensingServiceAndWait(MainWearableSensingService.ACTION_SEND_AUDIO);
}
private void sendNonHotwordAudioStreamFromWearable() throws Exception {
provideDataToWearableSensingServiceAndWait(
MainWearableSensingService.ACTION_SEND_NON_HOTWORD_AUDIO);
}
private void sendNonHotwordAudioStreamWithAcceptDetectionOptionsFromWearable()
throws Exception {
provideDataToWearableSensingServiceAndWait(
MainWearableSensingService
.ACTION_SEND_NON_HOTWORD_AUDIO_WITH_ACCEPT_DETECTION_OPTIONS);
}
private void sendMoreAudioDataFromWearable() throws Exception {
provideDataToWearableSensingServiceAndWait(
MainWearableSensingService.ACTION_SEND_MORE_AUDIO_DATA);
}
private void verifyWearableSensingServiceHotwordValidatedCalled() throws Exception {
assertThat(
provideDataToWearableSensingServiceAndWait(
MainWearableSensingService.ACTION_VERIFY_HOTWORD_VALIDATED_CALLED))
.isEqualTo(WearableSensingManager.STATUS_SUCCESS);
}
private void verifyWearableSensingServiceAudioStopCalled() throws Exception {
assertThat(
provideDataToWearableSensingServiceAndWait(
MainWearableSensingService.ACTION_VERIFY_AUDIO_STOP_CALLED))
.isEqualTo(WearableSensingManager.STATUS_SUCCESS);
}
private int provideDataToWearableSensingServiceAndWait(String value) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger statusRef = new AtomicInteger();
PersistableBundle data = new PersistableBundle();
data.putString(MainWearableSensingService.BUNDLE_ACTION_KEY, value);
sWearableSensingManager.provideData(
data,
null,
mExecutor,
(status) -> {
statusRef.set(status);
latch.countDown();
});
assertThat(latch.await(3, SECONDS)).isTrue();
return statusRef.get();
}
private boolean isWatch() {
return sPkgMgr.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
}