blob: 2a25003237b76328ed876261f42540d8adf2eb9e [file] [log] [blame]
/*
* Copyright (C) 2018 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.app.role.cts;
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.Instrumentation;
import android.app.role.OnRoleHoldersChangedListener;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.os.Process;
import android.os.UserHandle;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.FlakyTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.AppOpsUtils;
import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
/**
* Tests {@link RoleManager}.
*/
@RunWith(AndroidJUnit4.class)
public class RoleManagerTest {
private static final String LOG_TAG = "RoleManagerTest";
private static final long TIMEOUT_MILLIS = 15 * 1000;
private static final long UNEXPECTED_TIMEOUT_MILLIS = 1000;
private static final String ROLE_NAME = RoleManager.ROLE_BROWSER;
private static final String APP_APK_PATH = "/data/local/tmp/cts/role/CtsRoleTestApp.apk";
private static final String APP_PACKAGE_NAME = "android.app.role.cts.app";
private static final String APP_IS_ROLE_HELD_ACTIVITY_NAME = APP_PACKAGE_NAME
+ ".IsRoleHeldActivity";
private static final String APP_IS_ROLE_HELD_EXTRA_IS_ROLE_HELD = APP_PACKAGE_NAME
+ ".extra.IS_ROLE_HELD";
private static final String APP_REQUEST_ROLE_ACTIVITY_NAME = APP_PACKAGE_NAME
+ ".RequestRoleActivity";
private static final String PERMISSION_MANAGE_ROLES_FROM_CONTROLLER =
"com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER";
private static final Instrumentation sInstrumentation =
InstrumentationRegistry.getInstrumentation();
private static final Context sContext = InstrumentationRegistry.getTargetContext();
private static final PackageManager sPackageManager = sContext.getPackageManager();
private static final RoleManager sRoleManager = sContext.getSystemService(RoleManager.class);
private static final UiDevice sUiDevice = UiDevice.getInstance(sInstrumentation);
@Rule
public ActivityTestRule<WaitForResultActivity> mActivityRule =
new ActivityTestRule<>(WaitForResultActivity.class);
private String mRoleHolder;
@Before
public void saveRoleHolder() throws Exception {
List<String> roleHolders = getRoleHolders(ROLE_NAME);
mRoleHolder = !roleHolders.isEmpty() ? roleHolders.get(0) : null;
if (Objects.equals(mRoleHolder, APP_PACKAGE_NAME)) {
removeRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
mRoleHolder = null;
}
}
@After
public void restoreRoleHolder() throws Exception {
removeRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
if (mRoleHolder != null) {
addRoleHolder(ROLE_NAME, mRoleHolder);
}
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, false);
}
@Before
public void installApp() throws Exception {
installPackage(APP_APK_PATH);
}
@After
public void uninstallApp() throws Exception {
uninstallPackage(APP_PACKAGE_NAME);
}
@Before
public void wakeUpScreen() throws IOException {
runShellCommand(sInstrumentation, "input keyevent KEYCODE_WAKEUP");
}
@Test
public void requestRoleIntentHasPermissionControllerPackage() throws Exception {
Intent intent = sRoleManager.createRequestRoleIntent(ROLE_NAME);
assertThat(intent.getPackage()).isEqualTo(
sPackageManager.getPermissionControllerPackageName());
}
@Test
public void requestRoleIntentHasExtraRoleName() throws Exception {
Intent intent = sRoleManager.createRequestRoleIntent(ROLE_NAME);
assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(ROLE_NAME);
}
@FlakyTest
@Test
public void requestRoleAndDenyThenIsNotRoleHolder() throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, false);
}
@FlakyTest
@Test
public void requestRoleAndAllowThenIsRoleHolder() throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(true);
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, true);
}
@FlakyTest
@Test
public void requestRoleFirstTimeNoDontAskAgain() throws Exception {
requestRole(ROLE_NAME);
UiObject2 dontAskAgainCheck = findDontAskAgainCheck(false);
assertThat(dontAskAgainCheck).isNull();
respondToRoleRequest(false);
}
@FlakyTest
@Test
public void requestRoleAndDenyThenHasDontAskAgain() throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
requestRole(ROLE_NAME);
UiObject2 dontAskAgainCheck = findDontAskAgainCheck();
assertThat(dontAskAgainCheck).isNotNull();
respondToRoleRequest(false);
}
@FlakyTest
@Test
public void requestRoleAndDenyWithDontAskAgainReturnsCanceled() throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
requestRole(ROLE_NAME);
findDontAskAgainCheck().click();
Pair<Integer, Intent> result = clickButtonAndWaitForResult(true);
assertThat(result.first).isEqualTo(Activity.RESULT_CANCELED);
}
@FlakyTest
@Test
public void requestRoleAndDenyWithDontAskAgainThenDeniedAutomatically() throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
requestRole(ROLE_NAME);
findDontAskAgainCheck().click();
clickButtonAndWaitForResult(true);
requestRole(ROLE_NAME);
Pair<Integer, Intent> result = waitForResult();
assertThat(result.first).isEqualTo(Activity.RESULT_CANCELED);
}
@FlakyTest
@Test
public void requestRoleAndDenyWithDontAskAgainAndClearDataThenShowsUiWithoutDontAskAgain()
throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
requestRole(ROLE_NAME);
findDontAskAgainCheck().click();
clickButtonAndWaitForResult(true);
clearPackageData(APP_PACKAGE_NAME);
// Wait for the don't ask again to be forgotten.
Thread.sleep(2000);
requestRole(ROLE_NAME);
UiObject2 dontAskAgainCheck = findDontAskAgainCheck(false);
assertThat(dontAskAgainCheck).isNull();
respondToRoleRequest(false);
}
@FlakyTest
@Test
public void requestRoleAndDenyWithDontAskAgainAndReinstallThenShowsUiWithoutDontAskAgain()
throws Exception {
requestRole(ROLE_NAME);
respondToRoleRequest(false);
requestRole(ROLE_NAME);
findDontAskAgainCheck().click();
clickButtonAndWaitForResult(true);
uninstallPackage(APP_PACKAGE_NAME);
// Wait for the don't ask again to be forgotten.
Thread.sleep(2000);
installPackage(APP_APK_PATH);
requestRole(ROLE_NAME);
UiObject2 dontAskAgainCheck = findDontAskAgainCheck(false);
assertThat(dontAskAgainCheck).isNull();
respondToRoleRequest(false);
}
private void requestRole(@NonNull String roleName) {
Intent intent = new Intent()
.setComponent(new ComponentName(APP_PACKAGE_NAME, APP_REQUEST_ROLE_ACTIVITY_NAME))
.putExtra(Intent.EXTRA_ROLE_NAME, roleName);
mActivityRule.getActivity().startActivityToWaitForResult(intent);
}
private void respondToRoleRequest(boolean allow) throws InterruptedException, IOException {
if (allow) {
UiObject2 item = sUiDevice.wait(Until.findObject(By.text(APP_PACKAGE_NAME)),
TIMEOUT_MILLIS);
if (item == null) {
dumpWindowHierarchy();
fail("Cannot find item to click");
}
item.click();
}
Pair<Integer, Intent> result = clickButtonAndWaitForResult(allow);
int expectedResult = allow ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
assertThat(result.first).isEqualTo(expectedResult);
}
@Nullable
private UiObject2 findDontAskAgainCheck(boolean expected) {
return sUiDevice.wait(Until.findObject(By.text("Don\u2019t ask again")), expected
? TIMEOUT_MILLIS : UNEXPECTED_TIMEOUT_MILLIS);
}
@Nullable
private UiObject2 findDontAskAgainCheck() {
return findDontAskAgainCheck(true);
}
@NonNull
private Pair<Integer, Intent> clickButtonAndWaitForResult(boolean positive) throws IOException,
InterruptedException {
String buttonId = positive ? "android:id/button1" : "android:id/button2";
UiObject2 button = sUiDevice.wait(Until.findObject(By.res(buttonId)), TIMEOUT_MILLIS);
if (button == null) {
dumpWindowHierarchy();
fail("Cannot find button to click");
}
button.click();
return waitForResult();
}
private void dumpWindowHierarchy() throws InterruptedException, IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
sUiDevice.dumpWindowHierarchy(outputStream);
String windowHierarchy = outputStream.toString(StandardCharsets.UTF_8.name());
Log.w(LOG_TAG, "Window hierarchy:");
for (String line : windowHierarchy.split("\n")) {
Thread.sleep(10);
Log.w(LOG_TAG, line);
}
}
@NonNull
private Pair<Integer, Intent> waitForResult() throws InterruptedException {
return mActivityRule.getActivity().waitForActivityResult(TIMEOUT_MILLIS);
}
private static void clearPackageData(@NonNull String packageName) {
runShellCommand("pm clear " + packageName);
}
private void installPackage(@NonNull String apkPath) {
runShellCommand("pm install -r " + apkPath);
}
private void uninstallPackage(@NonNull String packageName) {
runShellCommand("pm uninstall " + packageName);
}
@Test
public void roleIsAvailable() {
assertThat(sRoleManager.isRoleAvailable(ROLE_NAME)).isTrue();
}
@Test
public void dontAddRoleHolderThenRoleIsNotHeld() throws Exception {
assertRoleIsHeld(ROLE_NAME, false);
}
@Test
public void addRoleHolderThenRoleIsHeld() throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertRoleIsHeld(ROLE_NAME, true);
}
@Test
public void addAndRemoveRoleHolderThenRoleIsNotHeld() throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
removeRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertRoleIsHeld(ROLE_NAME, false);
}
private void assertRoleIsHeld(@NonNull String roleName, boolean isHeld)
throws InterruptedException {
Intent intent = new Intent()
.setComponent(new ComponentName(APP_PACKAGE_NAME, APP_IS_ROLE_HELD_ACTIVITY_NAME))
.putExtra(Intent.EXTRA_ROLE_NAME, roleName);
WaitForResultActivity activity = mActivityRule.getActivity();
activity.startActivityToWaitForResult(intent);
Pair<Integer, Intent> result = activity.waitForActivityResult(TIMEOUT_MILLIS);
assertThat(result.first).isEqualTo(Activity.RESULT_OK);
assertThat(result.second).isNotNull();
assertThat(result.second.hasExtra(APP_IS_ROLE_HELD_EXTRA_IS_ROLE_HELD)).isTrue();
assertThat(result.second.getBooleanExtra(APP_IS_ROLE_HELD_EXTRA_IS_ROLE_HELD, false))
.isEqualTo(isHeld);
}
@Test
public void dontAddRoleHolderThenIsNotRoleHolder() throws Exception {
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, false);
}
@Test
public void addRoleHolderThenIsRoleHolder() throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, true);
}
@Test
public void addAndRemoveRoleHolderThenIsNotRoleHolder() throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
removeRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, false);
}
@Test
public void addAndClearRoleHoldersThenIsNotRoleHolder() throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
clearRoleHolders(ROLE_NAME);
assertIsRoleHolder(ROLE_NAME, APP_PACKAGE_NAME, false);
}
@Test
public void addOnRoleHoldersChangedListenerAndAddRoleHolderThenIsNotified() throws Exception {
assertOnRoleHoldersChangedListenerIsNotified(() -> addRoleHolder(ROLE_NAME,
APP_PACKAGE_NAME));
}
@Test
public void addOnRoleHoldersChangedListenerAndRemoveRoleHolderThenIsNotified()
throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertOnRoleHoldersChangedListenerIsNotified(() -> removeRoleHolder(ROLE_NAME,
APP_PACKAGE_NAME));
}
@Test
public void addOnRoleHoldersChangedListenerAndClearRoleHoldersThenIsNotified()
throws Exception {
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
assertOnRoleHoldersChangedListenerIsNotified(() -> clearRoleHolders(ROLE_NAME));
}
private void assertOnRoleHoldersChangedListenerIsNotified(@NonNull ThrowingRunnable runnable)
throws Exception {
ListenerFuture future = new ListenerFuture();
UserHandle user = Process.myUserHandle();
runWithShellPermissionIdentity(() -> sRoleManager.addOnRoleHoldersChangedListenerAsUser(
sContext.getMainExecutor(), future, user));
Pair<String, UserHandle> result;
try {
runnable.run();
result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} finally {
runWithShellPermissionIdentity(() ->
sRoleManager.removeOnRoleHoldersChangedListenerAsUser(future, user));
}
assertThat(result.first).isEqualTo(ROLE_NAME);
assertThat(result.second).isEqualTo(user);
}
@Test
public void addAndRemoveOnRoleHoldersChangedListenerAndAddRoleHolderThenIsNotNotified()
throws Exception {
ListenerFuture future = new ListenerFuture();
UserHandle user = Process.myUserHandle();
runWithShellPermissionIdentity(() -> {
sRoleManager.addOnRoleHoldersChangedListenerAsUser(sContext.getMainExecutor(), future,
user);
sRoleManager.removeOnRoleHoldersChangedListenerAsUser(future, user);
});
addRoleHolder(ROLE_NAME, APP_PACKAGE_NAME);
try {
future.get(UNEXPECTED_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// Expected
return;
}
throw new AssertionError("OnRoleHoldersChangedListener was notified after removal");
}
@Test
public void setRoleNamesFromControllerShouldRequireManageRolesFromControllerPermission() {
assertRequiresManageRolesFromControllerPermission(
() -> sRoleManager.setRoleNamesFromController(Collections.emptyList()),
"setRoleNamesFromController");
}
@Test
public void addRoleHolderFromControllerShouldRequireManageRolesFromControllerPermission() {
assertRequiresManageRolesFromControllerPermission(
() -> sRoleManager.addRoleHolderFromController(ROLE_NAME, APP_PACKAGE_NAME),
"addRoleHolderFromController");
}
@Test
public void removeRoleHolderFromControllerShouldRequireManageRolesFromControllerPermission() {
assertRequiresManageRolesFromControllerPermission(
() -> sRoleManager.removeRoleHolderFromController(ROLE_NAME, APP_PACKAGE_NAME),
"removeRoleHolderFromController");
}
@Test
public void getHeldRolesFromControllerShouldRequireManageRolesFromControllerPermission() {
assertRequiresManageRolesFromControllerPermission(
() -> sRoleManager.getHeldRolesFromController(APP_PACKAGE_NAME),
"getHeldRolesFromController");
}
private void assertRequiresManageRolesFromControllerPermission(@NonNull Runnable runnable,
@NonNull String methodName) {
try {
runnable.run();
} catch (SecurityException e) {
if (e.getMessage().contains(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)) {
// Expected
return;
}
throw e;
}
fail("RoleManager." + methodName + "() should require "
+ PERMISSION_MANAGE_ROLES_FROM_CONTROLLER);
}
@Test
public void manageRoleFromsFromControllerPermissionShouldBeDeclaredByPermissionController()
throws PackageManager.NameNotFoundException {
PermissionInfo permissionInfo = sPackageManager.getPermissionInfo(
PERMISSION_MANAGE_ROLES_FROM_CONTROLLER, 0);
assertThat(permissionInfo.packageName).isEqualTo(
sPackageManager.getPermissionControllerPackageName());
assertThat(permissionInfo.getProtection()).isEqualTo(PermissionInfo.PROTECTION_SIGNATURE);
assertThat(permissionInfo.getProtectionFlags()).isEqualTo(0);
}
@Test
public void removeSmsRoleHolderThenDialerRoleAppOpIsNotDenied() throws Exception {
if (!(sRoleManager.isRoleAvailable(RoleManager.ROLE_DIALER)
&& sRoleManager.isRoleAvailable(RoleManager.ROLE_SMS))) {
return;
}
addRoleHolder(RoleManager.ROLE_DIALER, APP_PACKAGE_NAME);
addRoleHolder(RoleManager.ROLE_SMS, APP_PACKAGE_NAME);
removeRoleHolder(RoleManager.ROLE_SMS, APP_PACKAGE_NAME);
assertThat(AppOpsUtils.getOpMode(APP_PACKAGE_NAME, AppOpsManager.OPSTR_SEND_SMS))
.isEqualTo(AppOpsManager.opToDefaultMode(AppOpsManager.OPSTR_SEND_SMS));
}
@Test
public void smsRoleHasHolder() throws Exception {
if (!sRoleManager.isRoleAvailable(RoleManager.ROLE_SMS)) {
return;
}
assertThat(getRoleHolders(RoleManager.ROLE_SMS)).isNotEmpty();
}
private List<String> getRoleHolders(@NonNull String roleName) throws Exception {
return callWithShellPermissionIdentity(() -> sRoleManager.getRoleHolders(roleName));
}
private void assertIsRoleHolder(@NonNull String roleName, @NonNull String packageName,
boolean shouldBeRoleHolder) throws Exception {
List<String> packageNames = getRoleHolders(roleName);
if (shouldBeRoleHolder) {
assertThat(packageNames).contains(packageName);
} else {
assertThat(packageNames).doesNotContain(packageName);
}
}
private void addRoleHolder(@NonNull String roleName, @NonNull String packageName)
throws Exception {
CallbackFuture future = new CallbackFuture();
runWithShellPermissionIdentity(() -> sRoleManager.addRoleHolderAsUser(roleName,
packageName, 0, Process.myUserHandle(), sContext.getMainExecutor(), future));
future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
private void removeRoleHolder(@NonNull String roleName, @NonNull String packageName)
throws Exception {
CallbackFuture future = new CallbackFuture();
runWithShellPermissionIdentity(() -> sRoleManager.removeRoleHolderAsUser(roleName,
packageName, 0, Process.myUserHandle(), sContext.getMainExecutor(), future));
future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
private void clearRoleHolders(@NonNull String roleName) throws Exception {
CallbackFuture future = new CallbackFuture();
runWithShellPermissionIdentity(() -> sRoleManager.clearRoleHoldersAsUser(roleName, 0,
Process.myUserHandle(), sContext.getMainExecutor(), future));
future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
private static class ListenerFuture extends CompletableFuture<Pair<String, UserHandle>>
implements OnRoleHoldersChangedListener {
@Override
public void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) {
complete(new Pair<>(roleName, user));
}
}
private static class CallbackFuture extends CompletableFuture<Void>
implements Consumer<Boolean> {
@Override
public void accept(Boolean successful) {
if (successful) {
complete(null);
} else {
completeExceptionally(new RuntimeException());
}
}
}
}