blob: 086c1c7783e6dce85524f76aefc538c77d82614c [file] [log] [blame]
/*
* Copyright (C) 2020 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.voicerecognition.cts;
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 com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.Manifest;
import android.app.compat.CompatChanges;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.util.Log;
import android.text.TextUtils;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class RecognitionServiceMicIndicatorTest {
private final String TAG = "RecognitionServiceMicIndicatorTest";
// same as Settings.Secure.VOICE_RECOGNITION_SERVICE
private final String VOICE_RECOGNITION_SERVICE = "voice_recognition_service";
// same as Settings.Secure.VOICE_INTERACTION_SERVICE
private final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
// Th notification privacy indicator
private final String PRIVACY_CHIP_PACLAGE_NAME = "com.android.systemui";
private final String PRIVACY_CHIP_ID = "privacy_chip";
// The cts app label
private final String APP_LABEL = "CtsVoiceRecognitionTestCases";
// A simple test voice recognition service implementation
private final String CTS_VOICE_RECOGNITION_SERVICE =
"android.recognitionservice.service/android.recognitionservice.service"
+ ".CtsVoiceRecognitionService";
private final String INDICATORS_FLAG = "camera_mic_icons_enabled";
private final long INDICATOR_DISMISS_TIMEOUT = 5000L;
private final long UI_WAIT_TIMEOUT = 1000L;
private final long PERMISSION_INDICATORS_NOT_PRESENT = 162547999L;
private UiDevice mUiDevice;
private SpeechRecognitionActivity mActivity;
private Context mContext;
private String mOriginalVoiceRecognizer;
private String mCameraLabel;
private boolean mOriginalIndicatorsEnabledState;
private boolean mTestRunnung;
@Rule
public ActivityTestRule<SpeechRecognitionActivity> mActivityTestRule =
new ActivityTestRule<>(SpeechRecognitionActivity.class);
@Before
public void setup() {
// If the change Id is not present, then isChangeEnabled will return true. To bypass this,
// the change is set to "false" if present.
assumeFalse("feature not present on this device", runWithShellPermissionIdentity(
() -> CompatChanges.isChangeEnabled(PERMISSION_INDICATORS_NOT_PRESENT,
Process.SYSTEM_UID)));
final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
boolean hasTvFeature = pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
assumeFalse("Not run in the tv device", hasTvFeature);
mTestRunnung = true;
prepareDevice();
mContext = InstrumentationRegistry.getTargetContext();
mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mActivity = mActivityTestRule.getActivity();
try {
mCameraLabel = pm.getPermissionGroupInfo(Manifest.permission_group.CAMERA, 0).loadLabel(
pm).toString();
} catch (PackageManager.NameNotFoundException e) {
}
// get original indicator enable state
runWithShellPermissionIdentity(() -> {
mOriginalIndicatorsEnabledState =
DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG, false);
});
// get original voice services
mOriginalVoiceRecognizer = Settings.Secure.getString(
mContext.getContentResolver(), VOICE_RECOGNITION_SERVICE);
// QPR is default disabled, we need to enable it
setIndicatorsEnabledStateIfNeeded(/* shouldBeEnabled */ true);
}
@After
public void teardown() {
if (!mTestRunnung) {
return;
}
// press back to close the dialog
mUiDevice.pressBack();
// restore to original voice services
setCurrentRecognizer(mOriginalVoiceRecognizer);
// restore to original indicator enable state
setIndicatorsEnabledStateIfNeeded(mOriginalIndicatorsEnabledState);
}
private void prepareDevice() {
// Unlock screen.
runShellCommand("input keyevent KEYCODE_WAKEUP");
// Dismiss keyguard, in case it's set as "Swipe to unlock".
runShellCommand("wm dismiss-keyguard");
}
private void setIndicatorsEnabledStateIfNeeded(Boolean shouldBeEnabled) {
runWithShellPermissionIdentity(() -> {
final boolean currentlyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
INDICATORS_FLAG, false);
if (currentlyEnabled != shouldBeEnabled) {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG,
shouldBeEnabled.toString(), false);
}
});
}
private void setCurrentRecognizer(String recognizer) {
runWithShellPermissionIdentity(
() -> Settings.Secure.putString(mContext.getContentResolver(),
VOICE_RECOGNITION_SERVICE, recognizer));
mUiDevice.waitForIdle();
}
private boolean hasPreInstalledRecognizer(String packageName) {
Log.v(TAG, "hasPreInstalledRecognizer package=" + packageName);
try {
final PackageManager pm = mContext.getPackageManager();
final ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
return ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
private static String getComponentPackageNameFromString(String from) {
ComponentName componentName = from != null ? ComponentName.unflattenFromString(from) : null;
return componentName != null ? componentName.getPackageName() : "";
}
@Test
public void testNonTrustedRecognitionServiceCannotBlameCallingApp() throws Throwable {
// This is a workaound solution for R QPR. We treat trusted if the current voice recognizer
// is also a preinstalled app. This is a untrusted case.
setCurrentRecognizer(CTS_VOICE_RECOGNITION_SERVICE);
// verify that the untrusted app cannot blame the calling app mic access
testVoiceRecognitionServiceBlameCallingApp(/* trustVoiceService */ false);
}
@Test
public void testTrustedRecognitionServiceCanBlameCallingApp() throws Throwable {
// This is a workaound solution for R QPR. We treat trusted if the current voice recognizer
// is also a preinstalled app. This is a trusted case.
boolean hasPreInstalledRecognizer = hasPreInstalledRecognizer(
getComponentPackageNameFromString(mOriginalVoiceRecognizer));
assumeTrue("No preinstalled recognizer.", hasPreInstalledRecognizer);
// verify that the trusted app can blame the calling app mic access
testVoiceRecognitionServiceBlameCallingApp(/* trustVoiceService */ true);
}
private void testVoiceRecognitionServiceBlameCallingApp(boolean trustVoiceService)
throws Throwable {
// Start SpeechRecognition
mActivity.startListening();
assertPrivacyChipAndIndicatorsPresent(trustVoiceService);
}
private void assertPrivacyChipAndIndicatorsPresent(boolean trustVoiceService) {
// Open notification and verify the privacy indicator is shown
mUiDevice.openNotification();
SystemClock.sleep(UI_WAIT_TIMEOUT);
final UiObject2 privacyChip =
mUiDevice.findObject(By.res(PRIVACY_CHIP_PACLAGE_NAME, PRIVACY_CHIP_ID));
assertWithMessage("Can not find mic indicator").that(privacyChip).isNotNull();
// Click the privacy indicator and verify the calling app name display status in the dialog.
privacyChip.click();
SystemClock.sleep(UI_WAIT_TIMEOUT);
final UiObject2 recognitionCallingAppLabel = mUiDevice.findObject(By.text(APP_LABEL));
if (trustVoiceService) {
// Check trust recognizer can blame calling app mic permission
assertWithMessage(
"Trusted voice recognition service can blame the calling app name " + APP_LABEL
+ ", but does not find it.").that(
recognitionCallingAppLabel).isNotNull();
assertThat(recognitionCallingAppLabel.getText()).isEqualTo(APP_LABEL);
// Check trust recognizer cannot blame non-mic permission
final UiObject2 cemaraLabel = mUiDevice.findObject(By.text(mCameraLabel));
assertWithMessage("Trusted voice recognition service cannot blame non-mic permission")
.that(cemaraLabel).isNull();
} else {
assertWithMessage(
"Untrusted voice recognition service cannot blame the calling app name "
+ APP_LABEL).that(recognitionCallingAppLabel).isNull();
}
// Wait for the privacy indicator to disappear to avoid the test becoming flaky.
SystemClock.sleep(INDICATOR_DISMISS_TIMEOUT);
}
}