blob: b4ef5e212856742fbde56ad06d8fe0b44318098c [file] [log] [blame]
/*
* Copyright (C) 2022 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.DEVICE_POWER;
import static android.Manifest.permission.MANAGE_HOTWORD_DETECTION;
import static android.Manifest.permission.MANAGE_SENSOR_PRIVACY;
import static android.Manifest.permission.OBSERVE_SENSOR_PRIVACY;
import static android.Manifest.permission.POWER_SAVER;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.SOUND_TRIGGER_RUN_IN_BATTERY_SAVER;
import static android.content.pm.PackageManager.FEATURE_MICROPHONE;
import static android.service.voice.SoundTriggerFailure.ERROR_CODE_MODULE_DIED;
import static android.service.voice.SoundTriggerFailure.ERROR_CODE_RECOGNITION_RESUME_FAILED;
import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN;
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.createKeyphraseArray;
import static android.voiceinteraction.cts.testcore.Helper.createKeyphraseRecognitionExtraList;
import static android.voiceinteraction.cts.testcore.Helper.waitForFutureDoneAndAssertSuccessful;
import static android.voiceinteraction.cts.testcore.Helper.waitForVoidFutureAndAssertSuccessful;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
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.assumeTrue;
import android.app.AppOpsManager;
import android.content.Context;
import android.hardware.SensorPrivacyManager;
import android.hardware.soundtrigger.SoundTrigger;
import android.media.soundtrigger.SoundTriggerInstrumentation;
import android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionSession;
import android.os.BatterySaverPolicyConfig;
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.service.voice.AlwaysOnHotwordDetector;
import android.service.voice.FailureSuggestedAction;
import android.service.voice.HotwordRejectedResult;
import android.service.voice.SandboxedDetectionInitializer;
import android.service.voice.SoundTriggerFailure;
import android.soundtrigger.cts.instrumentation.SoundTriggerInstrumentationObserver;
import android.soundtrigger.cts.instrumentation.SoundTriggerInstrumentationObserver.ModelSessionObserver;
import android.util.Log;
import android.voiceinteraction.common.Utils;
import android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService;
import android.voiceinteraction.cts.testcore.Helper;
import android.voiceinteraction.cts.testcore.VoiceInteractionServiceConnectedClassRule;
import android.voiceinteraction.cts.testcore.VoiceInteractionServiceOverrideEnrollmentRule;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.RequiredFeatureRule;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Tests for {@link AlwaysOnHotwordDetector} APIs. */
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "No real use case for instant mode hotword detector")
public class AlwaysOnHotwordDetectorTest extends AbstractHdsTestCase {
private static final String TAG = "AlwaysOnHotwordDetectorTest";
// The VoiceInteractionService used by this test
private static final String SERVICE_COMPONENT =
"android.voiceinteraction.cts.services.CtsBasicVoiceInteractionService";
private static final int WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS = 750;
private static final Context sContext = getInstrumentation().getTargetContext();
private static final SoundTrigger.Keyphrase[] KEYPHRASE_ARRAY = createKeyphraseArray(sContext);
private final SoundTriggerInstrumentationObserver mInstrumentationObserver =
new SoundTriggerInstrumentationObserver();
// For destroying in teardown
private AlwaysOnHotwordDetector mAlwaysOnHotwordDetector = null;
@Rule
public RequiredFeatureRule REQUIRES_MIC_RULE = new RequiredFeatureRule(FEATURE_MICROPHONE);
@Rule
public VoiceInteractionServiceOverrideEnrollmentRule mEnrollOverrideRule =
new VoiceInteractionServiceOverrideEnrollmentRule(getService());
@ClassRule
public static final VoiceInteractionServiceConnectedClassRule sServiceRule =
new VoiceInteractionServiceConnectedClassRule(
sContext, getTestVoiceInteractionServiceName());
private static String getTestVoiceInteractionServiceName() {
Log.d(TAG, "getTestVoiceInteractionServiceName()");
return CTS_SERVICE_PACKAGE + "/" + SERVICE_COMPONENT;
}
private static CtsBasicVoiceInteractionService getService() {
return (CtsBasicVoiceInteractionService) sServiceRule.getService();
}
private void adoptSoundTriggerPermissions() {
getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity(
RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD, MANAGE_HOTWORD_DETECTION,
SOUND_TRIGGER_RUN_IN_BATTERY_SAVER, DEVICE_POWER, POWER_SAVER,
MANAGE_SENSOR_PRIVACY, OBSERVE_SENSOR_PRIVACY, MANAGE_VOICE_KEYPHRASES);
}
private void createAndEnrollAlwaysOnHotwordDetector() throws InterruptedException {
createAndEnrollAlwaysOnHotwordDetector(null);
}
private void createAndEnrollAlwaysOnHotwordDetector(@Nullable PersistableBundle options)
throws InterruptedException {
mAlwaysOnHotwordDetector = null;
// Wait onAvailabilityChanged() callback called following AOHD creation.
getService().initAvailabilityChangeLatch();
// Load appropriate keyphrase model
// Required for the model to enter the enrolled state
runWithShellPermissionIdentity(
() -> mEnrollOverrideRule.getModelManager().updateKeyphraseSoundModel(
new SoundTrigger.KeyphraseSoundModel(new UUID(5, 7),
new UUID(7, 5), /* data= */ null, KEYPHRASE_ARRAY)),
MANAGE_VOICE_KEYPHRASES);
// Create alwaysOnHotwordDetector
getService().createAlwaysOnHotwordDetectorWithOnFailureCallback(
/* useExecutor= */ true, /* mainThread= */ true, options);
try {
// Wait onHotwordDetectionServiceInitialized() callback
getService().waitSandboxedDetectionServiceInitializedCalledOrException();
} finally {
// Get the AlwaysOnHotwordDetector instance even if there is an error happened to avoid
// that we don't destroy the detector in tearDown method. It may be null here. We will
// check the status below.
mAlwaysOnHotwordDetector = getService().getAlwaysOnHotwordDetector();
}
// Verify that detector creation didn't throw
assertThat(getService().isCreateDetectorIllegalStateExceptionThrow()).isFalse();
assertThat(getService().isCreateDetectorSecurityExceptionThrow()).isFalse();
// verify callback result
assertThat(getService().getSandboxedDetectionServiceInitializedResult())
.isEqualTo(SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS);
assertThat(mAlwaysOnHotwordDetector).isNotNull();
// verify we have entered the ENROLLED state
getService().waitAvailabilityChangedCalled();
assertThat(getService().getHotwordDetectionServiceAvailabilityResult())
.isEqualTo(AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED);
}
@BeforeClass
public static void setupClass() {
// TODO(b/276393203) delete this
SystemClock.sleep(8_000);
}
@Before
public void setup() {
// Hook up SoundTriggerInstrumentation to inject/observe STHAL operations.
// Requires MANAGE_SOUND_TRIGGER
runWithShellPermissionIdentity(mInstrumentationObserver::attachInstrumentation);
// Set whether voice activation permission check is enabled.
getService().setVoiceActivationPermissionEnabled(mVoiceActivationPermissionEnabled);
}
@After
public void tearDown() {
// Destroy the framework session
if (mAlwaysOnHotwordDetector != null) {
mAlwaysOnHotwordDetector.destroy();
}
// Clean up any unexpected HAL state
try {
mInstrumentationObserver.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Clear the service state
getService().resetState();
// Drop any permissions we may still have
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
})
@Test
public void testStartRecognition_success() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onError",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
})
@Test
public void testHalIsDead_onFailureReceived() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// We don't get callbacks if we don't have a recognition started
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
assertThat(waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
// Cause a restart
getService().initOnFailureLatch();
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation().triggerRestart();
getService().waitOnFailureCalled();
var failure = getService().getSoundTriggerFailure();
assertThat(failure.getErrorCode()).isEqualTo(ERROR_CODE_MODULE_DIED);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onError",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionResumed",
})
@Test
public void testRecognitionResumedFailed_onFailureReceived() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
instrumentation.setResourceContention(true);
getService().initOnRecognitionPausedLatch();
// Induce a recognition pause
recognitionSession.triggerAbortRecognition();
getService().waitOnRecognitionPausedCalled();
getService().initOnFailureLatch();
// Framework will attempt to resume recognition, but will fail due to set contention
instrumentation.triggerOnResourcesAvailable();
getService().waitOnFailureCalled();
var failure = getService().getSoundTriggerFailure();
assertThat(failure.getErrorCode()).isEqualTo(ERROR_CODE_RECOGNITION_RESUME_FAILED);
instrumentation.setResourceContention(false);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionResumed",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testAbortRecognitionAndOnResourceAvailable_recognitionPausedAndResumed()
throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
ModelSessionObserver modelSession = mInstrumentationObserver
.getGlobalCallbackObserver().getOnModelLoadedFuture().get();
modelSession.resetOnRecognitionStartedFuture();
getService().initOnRecognitionPausedLatch();
// Induce a recognition pause
recognitionSession.triggerAbortRecognition();
getService().waitOnRecognitionPausedCalled();
// Check that STService didn't attempt to start immediately on receiving abort
assertThat(modelSession.getOnRecognitionStartedFuture().isDone()).isFalse();
getService().initOnRecognitionResumedLatch();
instrumentation.triggerOnResourcesAvailable();
getService().waitOnRecognitionResumedCalled();
recognitionSession = waitForFutureDoneAndAssertSuccessful(
modelSession.getOnRecognitionStartedFuture());
// Same flow, but ensure we don't get an onError by setting contention
getService().initOnRecognitionPausedLatch();
instrumentation.setResourceContention(true);
// Induce a recognition pause
recognitionSession.triggerAbortRecognition();
getService().waitOnRecognitionPausedCalled();
modelSession.resetOnRecognitionStartedFuture();
getService().initOnRecognitionResumedLatch();
// This will trigger resources available
instrumentation.setResourceContention(false);
getService().waitOnRecognitionResumedCalled();
assertThat(getService().getSoundTriggerFailure()).isNull();
recognitionSession = waitForFutureDoneAndAssertSuccessful(
modelSession.getOnRecognitionStartedFuture());
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionNoFlagBatterySaverAllEnabled_noRecognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_ALL_ENABLED, no onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager, PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
assertThat(getService().waitNoOnRecognitionPausedCalled()).isTrue();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionResumed",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionNoFlagBatterySaverCriticalOnly_recognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_CRITICAL_ONLY, onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager,
PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
getService().waitOnRecognitionPausedCalled();
// disable battery saver, onRecognitionResumed called
getService().initOnRecognitionResumedLatch();
BatteryUtils.enableBatterySaver(/* isEnabled= */ false);
getService().waitOnRecognitionResumedCalled();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionResumed",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionNoFlagBatterySaverAllDisabled_recognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_ALL_DISABLED, onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager,
PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
getService().waitOnRecognitionPausedCalled();
// disable battery saver, onRecognitionResumed called
getService().initOnRecognitionResumedLatch();
BatteryUtils.enableBatterySaver(/* isEnabled= */ false);
getService().waitOnRecognitionResumedCalled();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionWithFlagBatterySaverAllEnabled_noRecognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(
AlwaysOnHotwordDetector.RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER,
new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_ALL_ENABLED, no onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager, PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
assertThat(getService().waitNoOnRecognitionPausedCalled()).isTrue();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionWithFlagBatterySaverCriticalOnly_noRecognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(
AlwaysOnHotwordDetector.RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER,
new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_CRITICAL_ONLY, no onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager,
PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
assertThat(getService().waitNoOnRecognitionPausedCalled()).isTrue();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionPaused",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onRecognitionResumed",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testStartRecognitionWithFlagBatterySaverAllDisabled_recognitionPaused()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
mAlwaysOnHotwordDetector.startRecognition(
AlwaysOnHotwordDetector.RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER,
new byte[]{1, 2, 3, 4, 5});
assertThat(
waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_ALL_DISABLED, onRecognitionPaused
// called
getService().initOnRecognitionPausedLatch();
setSoundTriggerPowerSaveMode(powerManager,
PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
getService().waitOnRecognitionPausedCalled();
// disable battery saver, onRecognitionResumed called
getService().initOnRecognitionResumedLatch();
BatteryUtils.enableBatterySaver(/* isEnabled= */ false);
getService().waitOnRecognitionResumedCalled();
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#destroy",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testAfterDestroy_detectorIsInvalid() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
adoptSoundTriggerPermissions();
mAlwaysOnHotwordDetector.destroy();
assertThrows(IllegalStateException.class, () ->
mAlwaysOnHotwordDetector.startRecognition());
assertThrows(IllegalStateException.class, () ->
mAlwaysOnHotwordDetector.stopRecognition());
assertThrows(IllegalStateException.class, () ->
mAlwaysOnHotwordDetector.getParameter(
AlwaysOnHotwordDetector.MODEL_PARAM_THRESHOLD_FACTOR));
assertThrows(IllegalStateException.class, () ->
mAlwaysOnHotwordDetector.setParameter(
AlwaysOnHotwordDetector.MODEL_PARAM_THRESHOLD_FACTOR, 10));
assertThrows(IllegalStateException.class, () ->
mAlwaysOnHotwordDetector.queryParameter(
AlwaysOnHotwordDetector.MODEL_PARAM_THRESHOLD_FACTOR));
}
@Test
public void testOnPhoneCall_recognitionPausedAndResumed() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
final var modelSessionFuture = mInstrumentationObserver.getGlobalCallbackObserver()
.getOnModelLoadedFuture();
final var firstRecogSessionFuture = Futures.transformAsync(modelSessionFuture,
ModelSessionObserver::getOnRecognitionStartedFuture, Runnable::run);
mAlwaysOnHotwordDetector.startRecognition();
final ModelSessionObserver modelSession
= waitForFutureDoneAndAssertSuccessful(modelSessionFuture);
final RecognitionSession firstRecogSession = waitForFutureDoneAndAssertSuccessful(
firstRecogSessionFuture);
assertThat(modelSession).isNotNull();
assertThat(firstRecogSession).isNotNull();
getService().initOnRecognitionPausedLatch();
instrumentation.setInPhoneCallState(true);
getService().waitOnRecognitionPausedCalled();
modelSession.resetOnRecognitionStartedFuture();
final var secondRecogSessionFuture = modelSession.getOnRecognitionStartedFuture();
getService().initOnRecognitionResumedLatch();
instrumentation.setInPhoneCallState(false);
getService().waitOnRecognitionResumedCalled();
// Check that no failure received. Technically racey.
assertThat(getService().getHotwordDetectionServiceFailure()).isNull();
// Assert that recognition is properly restarted
final RecognitionSession secondRecogSession = waitForFutureDoneAndAssertSuccessful(
secondRecogSessionFuture);
assertThat(secondRecogSession).isNotNull();
assertThat(secondRecogSession).isNotEqualTo(firstRecogSession);
getService().initDetectRejectLatch();
secondRecogSession.triggerRecognitionEvent(
new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
@Test
public void testStartRecognitionDuringContention_succeedsPausesThenResumes()
throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
instrumentation.setResourceContention(true);
getService().initOnRecognitionPausedLatch();
assertThat(mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5}))
.isTrue();
final var recogFuture = mInstrumentationObserver.getOnRecognitionStartedFuture();
assertThrows(TimeoutException.class, () -> recogFuture.get(
WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS,
TimeUnit.MILLISECONDS));
getService().waitOnRecognitionPausedCalled();
getService().initOnRecognitionResumedLatch();
instrumentation.setResourceContention(false);
getService().waitOnRecognitionResumedCalled();
// Verify that recognition is really resumed
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
recogFuture);
assertThat(recognitionSession).isNotNull();
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
@Test
public void testStartRecognitionDuringBatterySaver_succeedsPausesThenResumes()
throws Exception {
BatteryUtils.assumeBatterySaverFeature();
final PowerManager powerManager = sContext.getSystemService(PowerManager.class);
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
try {
BatteryUtils.runDumpsysBatteryUnplug();
// enable battery saver with SOUND_TRIGGER_MODE_CRITICAL_ONLY, onRecognitionPaused
// called
setSoundTriggerPowerSaveMode(powerManager,
PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY);
BatteryUtils.enableBatterySaver(/* isEnabled= */ true);
getService().initOnRecognitionPausedLatch();
assertThat(mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5}))
.isTrue();
getService().waitOnRecognitionPausedCalled();
// No recognition session, since device state prohibits it
final var recogFuture = mInstrumentationObserver.getOnRecognitionStartedFuture();
assertThrows(TimeoutException.class, () -> recogFuture.get(
WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS,
TimeUnit.MILLISECONDS));
// disable battery saver, onRecognitionResumed called
getService().initOnRecognitionResumedLatch();
BatteryUtils.enableBatterySaver(/* isEnabled= */ false);
getService().waitOnRecognitionResumedCalled();
// Verify that recognition is really resumed
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
recogFuture);
assertThat(recognitionSession).isNotNull();
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
} finally {
BatteryUtils.runDumpsysBatteryReset();
BatteryUtils.resetBatterySaver();
}
}
@Test
public void testStartRecognitionFail_leavesModelUnrequested()
throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
instrumentation.setResourceContention(true);
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
// Should fail without permissions
assertThat(mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5}))
.isFalse();
final var recogFuture = mInstrumentationObserver.getOnRecognitionStartedFuture();
assertThrows(TimeoutException.class, () -> recogFuture.get(
WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS,
TimeUnit.MILLISECONDS));
instrumentation.setResourceContention(false);
// Verify that recognition is not resumed
assertThrows(TimeoutException.class, () -> recogFuture.get(
WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS,
TimeUnit.MILLISECONDS));
// Attempt to successfully start recognition
adoptSoundTriggerPermissions();
assertThat(mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5}))
.isTrue();
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
recogFuture);
assertThat(recognitionSession).isNotNull();
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
@Test
public void testOnDetected_appropriateAppOpsNoted() throws Exception {
// Set up recognition
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
// Hook up AppOps Listener
final SettableFuture<String> appOpFuture = SettableFuture.create();
AppOpsManager appOpsManager = sContext.getSystemService(AppOpsManager.class);
final String[] OPS_TO_WATCH =
new String[] {
AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
AppOpsManager.OPSTR_RECORD_AUDIO,
RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR,
};
getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity();
appOpsManager.startWatchingNoted(
OPS_TO_WATCH,
(op, uid, pkgName, attributionTag, flags, result) -> {
appOpFuture.set(op);
});
// Trigger recognition
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
var receivedOp = waitForFutureDoneAndAssertSuccessful(appOpFuture);
// We have noted one of the record ops
assertThat(Arrays.asList(OPS_TO_WATCH)).contains(receivedOp);
}
@Test
public void testOnRejected_noAppOpsNoted() throws Exception {
// Set up recognition
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
// Hook up AppOps Listener
final SettableFuture<String> appOpFuture = SettableFuture.create();
AppOpsManager appOpsManager = sContext.getSystemService(AppOpsManager.class);
final String[] OPS_TO_WATCH =
new String[] {
AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
AppOpsManager.OPSTR_RECORD_AUDIO,
RECEIVE_SANDBOX_TRIGGER_AUDIO_OP_STR,
};
getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity();
appOpsManager.startWatchingNoted(
OPS_TO_WATCH,
(op, uid, pkgName, attributionTag, flags, result) -> {
appOpFuture.set(op);
});
// Trigger recognition which will be rejected (empty data)
getService().initDetectRejectLatch();
recognitionSession.triggerRecognitionEvent(new byte[0],
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
HotwordRejectedResult rejectResult = getService().getHotwordServiceOnRejectedResult();
assertThat(rejectResult).isEqualTo(Helper.REJECTED_RESULT);
// Verify that no ops were noted
assertThat(appOpFuture.isDone()).isFalse();
}
@Test
public void testAppOpsLostReacquired_recognitionPausedResumed() throws Exception {
// We use the privacy sensor toggle to revoke appops, and not all devices support it
// Changing runtime permissions using the shell doesn't fire callbacks (b/280692605)
assumeTrue(sContext.getSystemService(SensorPrivacyManager.class).supportsSensorToggle(
SensorPrivacyManager.Sensors.MICROPHONE));
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Wire futures
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
final var modelSessionFuture = mInstrumentationObserver.getGlobalCallbackObserver()
.getOnModelLoadedFuture();
final var firstRecogSessionFuture = Futures.transformAsync(modelSessionFuture,
ModelSessionObserver::getOnRecognitionStartedFuture, Runnable::run);
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
final ModelSessionObserver modelSession
= waitForFutureDoneAndAssertSuccessful(modelSessionFuture);
assertThat(waitForFutureDoneAndAssertSuccessful(firstRecogSessionFuture)).isNotNull();
assertThat(modelSession).isNotNull();
getService().initOnRecognitionPausedLatch();
// Wire futures for resumed recognition
getService().initOnRecognitionResumedLatch();
modelSession.resetOnRecognitionStartedFuture();
final var secondRecogSessionFuture = modelSession.getOnRecognitionStartedFuture();
// Toggle the privacy sensor, which will cause us to lose appops
sContext.getSystemService(SensorPrivacyManager.class).setSensorPrivacy(
SensorPrivacyManager.Sensors.MICROPHONE, true);
try {
getService().waitOnRecognitionPausedCalled();
} finally {
// Toggle the privacy sensor off, which will cause us regain appops
sContext.getSystemService(SensorPrivacyManager.class).setSensorPrivacy(
SensorPrivacyManager.Sensors.MICROPHONE, false);
}
getService().waitOnRecognitionResumedCalled();
// Verify recognition is properly restarted by triggering an event
final RecognitionSession secondRecogSession = waitForFutureDoneAndAssertSuccessful(
secondRecogSessionFuture);
getService().initDetectRejectLatch();
secondRecogSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
Helper.verifyDetectedResult(detectResult, Helper.DETECTED_RESULT);
}
@Test
public void testRecognitionNotRequested_afterResumeFailed() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
SoundTriggerInstrumentation instrumentation =
mInstrumentationObserver.getGlobalCallbackObserver().getInstrumentation();
final var modelSessionFuture = mInstrumentationObserver.getGlobalCallbackObserver()
.getOnModelLoadedFuture();
final var firstRecogSessionFuture = Futures.transformAsync(modelSessionFuture,
ModelSessionObserver::getOnRecognitionStartedFuture, Runnable::run);
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
final var modelSession = waitForFutureDoneAndAssertSuccessful(modelSessionFuture);
assertThat(modelSession).isNotNull();
final var firstRecogSession = waitForFutureDoneAndAssertSuccessful(
firstRecogSessionFuture);
assertThat(firstRecogSession).isNotNull();
modelSession.resetOnRecognitionStartedFuture();
final var secondRecogSessionFuture = modelSession.getOnRecognitionStartedFuture();
instrumentation.setResourceContention(true);
getService().initOnRecognitionPausedLatch();
// Induce a recognition pause
firstRecogSession.triggerAbortRecognition();
getService().waitOnRecognitionPausedCalled();
getService().initOnFailureLatch();
// Framework will attempt to resume recognition, but will fail due to set contention
instrumentation.triggerOnResourcesAvailable();
getService().waitOnFailureCalled();
var failure = getService().getSoundTriggerFailure();
assertThat(failure.getErrorCode()).isEqualTo(ERROR_CODE_RECOGNITION_RESUME_FAILED);
assertThat(secondRecogSessionFuture.isDone()).isFalse();
// Triggers available callback, and start will now succeed
instrumentation.setResourceContention(false);
// We should now be in the not requested state
assertThrows(TimeoutException.class, () -> secondRecogSessionFuture.get(
WAIT_EXPECTED_NO_CALL_TIMEOUT_IN_MS,
TimeUnit.MILLISECONDS));
}
private static void setSoundTriggerPowerSaveMode(PowerManager powerManager, int mode) {
final BatterySaverPolicyConfig newFullPolicyConfig =
new BatterySaverPolicyConfig.Builder(powerManager.getFullPowerSavePolicy())
.setSoundTriggerMode(mode)
.build();
powerManager.setFullPowerSavePolicy(newFullPolicyConfig);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#destroy",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testDestroy_halModelUnloadedAndClientDetached() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
assertThat(waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture())).isNotNull();
ModelSessionObserver modelSessionObserver = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnModelLoadedFuture());
assertThat(modelSessionObserver).isNotNull();
// destroy detector to trigger a client detach
mAlwaysOnHotwordDetector.destroy();
waitForVoidFutureAndAssertSuccessful(modelSessionObserver.getOnRecognitionStoppedFuture());
waitForVoidFutureAndAssertSuccessful(modelSessionObserver.getOnModelUnloadedFuture());
waitForVoidFutureAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnClientDetachedFuture());
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
"android.service.voice.AlwaysOnHotwordDetector#destroy",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
})
@Test
public void testDestroy_clientDetachedWhenNoModelLoaded() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
waitForVoidFutureAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnClientAttachedFuture());
// destroy detector to trigger a client detach
mAlwaysOnHotwordDetector.destroy();
waitForVoidFutureAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnClientDetachedFuture());
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#destroy",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testDestroy_doubleCallsAreNoop() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
waitForVoidFutureAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnClientAttachedFuture());
// destroy detector to trigger a client detach
mAlwaysOnHotwordDetector.destroy();
waitForVoidFutureAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnClientDetachedFuture());
mInstrumentationObserver.getGlobalCallbackObserver().resetOnClientAttachedFuture();
mInstrumentationObserver.getGlobalCallbackObserver().resetOnClientDetachedFuture();
// 2nd destroy does nothing to HAL
mAlwaysOnHotwordDetector.destroy();
// Verify that the client attach/detach futures were never completed after the second
// destroy. It is okay to not wait for this as the destroy call is synchronous.
assertThat(mInstrumentationObserver.getGlobalCallbackObserver()
.getOnClientAttachedFuture().isDone()).isFalse();
assertThat(mInstrumentationObserver.getGlobalCallbackObserver()
.getOnClientDetachedFuture().isDone()).isFalse();
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testDestroy_destroyAndRecreateCreatesNewHotwordDetectionService() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
mAlwaysOnHotwordDetector.destroy();
PersistableBundle options = new PersistableBundle();
options.putInt(Helper.KEY_TEST_SCENARIO,
Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_SUCCESS_IF_CREATED_AFTER);
options.putLong(Utils.KEY_TIMESTAMP_MILLIS, SystemClock.elapsedRealtime());
// create call verifies initialize success.
// This means the HotwordDetectionService was recreated between AOHD destroy and create.
createAndEnrollAlwaysOnHotwordDetector(options);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onDetected",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testOnDetected_timestampIsAfterRecognitionStarted() throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
RecognitionSession recognitionSession = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getOnRecognitionStartedFuture());
assertThat(recognitionSession).isNotNull();
getService().initDetectRejectLatch();
long timestampCheckpoint = SystemClock.elapsedRealtime();
recognitionSession.triggerRecognitionEvent(new byte[]{0x11, 0x22},
createKeyphraseRecognitionExtraList());
getService().waitOnDetectOrRejectCalled();
AlwaysOnHotwordDetector.EventPayload detectResult =
getService().getHotwordServiceOnDetectedResult();
assertThat(detectResult.getHalEventReceivedMillis()).isGreaterThan(timestampCheckpoint);
}
@ApiTest(apis = {
"android.media.voice.KeyphraseModelManager#updateKeyphraseSoundModel",
"android.service.voice.AlwaysOnHotwordDetector#startRecognition",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onAvailabilityChanged",
"android.service.voice.AlwaysOnHotwordDetector.Callback#onFailure",
"android.service.voice.AlwaysOnHotwordDetector"
+ ".Callback#onHotwordDetectionServiceInitialized",
"android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector",
})
@Test
public void testDspEnrollment_enrollmentStopsRunningSession_onFailureAndEnrolledReceived()
throws Exception {
createAndEnrollAlwaysOnHotwordDetector();
// Grab permissions for more than a single call since we get callbacks
adoptSoundTriggerPermissions();
// Start recognition
mAlwaysOnHotwordDetector.startRecognition(0, new byte[]{1, 2, 3, 4, 5});
ModelSessionObserver modelSessionObserver = waitForFutureDoneAndAssertSuccessful(
mInstrumentationObserver.getGlobalCallbackObserver().getOnModelLoadedFuture());
assertThat(modelSessionObserver).isNotNull();
assertThat(waitForFutureDoneAndAssertSuccessful(
modelSessionObserver.getOnRecognitionStartedFuture())).isNotNull();
getService().initOnFailureLatch();
getService().initAvailabilityChangeLatch();
// enroll a new model triggering a stopRecognition
mEnrollOverrideRule.getModelManager().updateKeyphraseSoundModel(
new SoundTrigger.KeyphraseSoundModel(new UUID(5, 7),
new UUID(7, 5), /* data= */ null, KEYPHRASE_ARRAY));
// verify that both a failure callback was received and the model was stopped
// this indicates that the stopRecognition call internal to AlwaysOnHotwordDetector was
// made successfully
getService().waitOnFailureCalled();
waitForVoidFutureAndAssertSuccessful(modelSessionObserver.getOnRecognitionStoppedFuture());
SoundTriggerFailure soundTriggerFailure = getService().getSoundTriggerFailure();
assertThat(soundTriggerFailure.getErrorCode()).isEqualTo(ERROR_CODE_UNKNOWN);
assertThat(soundTriggerFailure.getErrorMessage()).isEqualTo(
"stopped recognition because of enrollment update");
assertThat(soundTriggerFailure.getSuggestedAction()).isEqualTo(
FailureSuggestedAction.RESTART_RECOGNITION);
getService().waitAvailabilityChangedCalled();
assertThat(getService().getHotwordDetectionServiceAvailabilityResult()).isEqualTo(
AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED);
// a second update to the enrollment database wil not trigger another onFailure because
// the model is already stopped
getService().initOnFailureLatch();
getService().initAvailabilityChangeLatch();
mEnrollOverrideRule.getModelManager().updateKeyphraseSoundModel(
new SoundTrigger.KeyphraseSoundModel(new UUID(5, 7),
new UUID(7, 5), /* data= */ null, KEYPHRASE_ARRAY));
getService().waitAvailabilityChangedCalled();
assertThat(getService().getHotwordDetectionServiceAvailabilityResult()).isEqualTo(
AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED);
assertThat(getService().isOnFailureLatchOpen()).isTrue();
}
}