blob: f243b3b5095da66c5d176ed2dab14642fe82aae2 [file] [log] [blame]
/*
* Copyright (C) 2019 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.permission.cts;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static com.android.compatibility.common.util.SystemUtil.eventually;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.provider.DeviceConfig;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.UiAutomatorUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class OneTimePermissionTest {
private static final String APP_PKG_NAME = "android.permission.cts.appthatrequestpermission";
private static final String APK =
"/data/local/tmp/cts/permissions/CtsAppThatRequestsOneTimePermission.apk";
private static final String EXTRA_FOREGROUND_SERVICE_LIFESPAN =
"android.permission.cts.OneTimePermissionTest.EXTRA_FOREGROUND_SERVICE_LIFESPAN";
private static final String EXTRA_FOREGROUND_SERVICE_STICKY =
"android.permission.cts.OneTimePermissionTest.EXTRA_FOREGROUND_SERVICE_STICKY";
private static final long ONE_TIME_TIMEOUT_MILLIS = 5000;
private static final long ONE_TIME_TIMER_LOWER_GRACE_PERIOD = 1000;
private static final long ONE_TIME_TIMER_UPPER_GRACE_PERIOD = 10000;
private final Context mContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
private final UiDevice mUiDevice =
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
private final ActivityManager mActivityManager =
mContext.getSystemService(ActivityManager.class);
private String mOldOneTimePermissionTimeoutValue;
@Rule
public IgnoreAllTestsRule mIgnoreAutomotive = new IgnoreAllTestsRule(
mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
@Before
public void wakeUpScreen() {
SystemUtil.runShellCommand("input keyevent KEYCODE_WAKEUP");
SystemUtil.runShellCommand("input keyevent 82");
}
@Before
public void installApp() {
runShellCommand("pm install -r " + APK);
}
@Before
public void prepareDeviceForOneTime() {
runWithShellPermissionIdentity(() -> {
mOldOneTimePermissionTimeoutValue = DeviceConfig.getProperty("permissions",
"one_time_permissions_timeout_millis");
DeviceConfig.setProperty("permissions", "one_time_permissions_timeout_millis",
Long.toString(ONE_TIME_TIMEOUT_MILLIS), false);
});
}
@After
public void uninstallApp() {
runShellCommand("pm uninstall " + APP_PKG_NAME);
}
@After
public void restoreDeviceForOneTime() {
runWithShellPermissionIdentity(
() -> DeviceConfig.setProperty("permissions", "one_time_permissions_timeout_millis",
mOldOneTimePermissionTimeoutValue, false));
}
@Test
public void testOneTimePermission() throws Throwable {
startApp();
CompletableFuture<Long> exitTime = registerAppExitListener();
clickOneTimeButton();
exitApp();
assertGranted(5000);
assertDenied(ONE_TIME_TIMEOUT_MILLIS + ONE_TIME_TIMER_UPPER_GRACE_PERIOD);
assertExpectedLifespan(exitTime, ONE_TIME_TIMEOUT_MILLIS);
}
@Ignore
@Test
public void testForegroundServiceMaintainsPermission() throws Throwable {
startApp();
CompletableFuture<Long> exitTime = registerAppExitListener();
clickOneTimeButton();
long expectedLifespanMillis = 2 * ONE_TIME_TIMEOUT_MILLIS;
startAppForegroundService(expectedLifespanMillis, false);
exitApp();
assertGranted(5000);
assertDenied(expectedLifespanMillis + ONE_TIME_TIMER_UPPER_GRACE_PERIOD);
assertExpectedLifespan(exitTime, expectedLifespanMillis);
}
@Test
public void testPermissionRevokedOnKill() throws Throwable {
startApp();
clickOneTimeButton();
exitApp();
assertGranted(5000);
mUiDevice.waitForIdle();
SystemUtil.runWithShellPermissionIdentity(() ->
mActivityManager.killBackgroundProcesses(APP_PKG_NAME));
runWithShellPermissionIdentity(
() -> Thread.sleep(DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
"one_time_permissions_killed_delay_millis", 5000L)));
assertDenied(500);
}
@Test
public void testStickyServiceMaintainsPermissionOnRestart() throws Throwable {
startApp();
clickOneTimeButton();
startAppForegroundService(2 * ONE_TIME_TIMEOUT_MILLIS, true);
exitApp();
assertGranted(5000);
mUiDevice.waitForIdle();
Thread.sleep(ONE_TIME_TIMEOUT_MILLIS);
runShellCommand("am crash " + APP_PKG_NAME);
eventually(() -> runWithShellPermissionIdentity(() -> {
if (mActivityManager.getPackageImportance(APP_PKG_NAME) <= IMPORTANCE_CACHED) {
throw new AssertionError("App was never killed");
}
}));
eventually(() -> runWithShellPermissionIdentity(() -> {
if (mActivityManager.getPackageImportance(APP_PKG_NAME)
> IMPORTANCE_FOREGROUND_SERVICE) {
throw new AssertionError("Foreground service never resumed");
}
Assert.assertEquals("Service resumed without permission",
PackageManager.PERMISSION_GRANTED, mContext.getPackageManager()
.checkPermission(ACCESS_FINE_LOCATION, APP_PKG_NAME));
}));
}
private void assertGrantedState(String s, int permissionGranted, long timeoutMillis) {
eventually(() -> Assert.assertEquals(s,
permissionGranted, mContext.getPackageManager()
.checkPermission(ACCESS_FINE_LOCATION, APP_PKG_NAME)), timeoutMillis);
}
private void assertGranted(long timeoutMillis) {
assertGrantedState("Permission was never granted", PackageManager.PERMISSION_GRANTED,
timeoutMillis);
}
private void assertDenied(long timeoutMillis) {
assertGrantedState("Permission was never revoked", PackageManager.PERMISSION_DENIED,
timeoutMillis);
}
private void assertExpectedLifespan(CompletableFuture<Long> exitTime, long expectedLifespan)
throws InterruptedException, java.util.concurrent.ExecutionException,
java.util.concurrent.TimeoutException {
long grantedLength = System.currentTimeMillis() - exitTime.get(0, TimeUnit.MILLISECONDS);
if (grantedLength + ONE_TIME_TIMER_LOWER_GRACE_PERIOD < expectedLifespan) {
throw new AssertionError(
"The one time permission lived shorter than expected. expected: "
+ expectedLifespan + "ms but was: " + grantedLength + "ms");
}
}
private void exitApp() {
boolean[] hasExited = {false};
try {
new Thread(() -> {
while (!hasExited[0]) {
mUiDevice.pressHome();
mUiDevice.pressBack();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}).start();
eventually(() -> {
runWithShellPermissionIdentity(() -> {
if (mActivityManager.getPackageImportance(APP_PKG_NAME)
<= IMPORTANCE_FOREGROUND) {
throw new AssertionError("Unable to exit application");
}
});
});
} finally {
hasExited[0] = true;
}
}
private void clickOneTimeButton() throws Throwable {
final UiObject2 uiObject = UiAutomatorUtils.waitFindObject(By.res(
"com.android.permissioncontroller:id/permission_allow_one_time_button"), 10000);
Thread.sleep(500);
uiObject.click();
}
/**
* Start the app. The app will request the permissions.
*/
private void startApp() {
Intent startApp = new Intent();
startApp.setComponent(new ComponentName(APP_PKG_NAME, APP_PKG_NAME + ".RequestPermission"));
startApp.setFlags(FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(startApp);
}
private void startAppForegroundService(long lifespanMillis, boolean sticky) {
Intent intent = new Intent()
.setComponent(new ComponentName(
APP_PKG_NAME, APP_PKG_NAME + ".KeepAliveForegroundService"))
.putExtra(EXTRA_FOREGROUND_SERVICE_LIFESPAN, lifespanMillis)
.putExtra(EXTRA_FOREGROUND_SERVICE_STICKY, sticky);
mContext.startService(intent);
}
private CompletableFuture<Long> registerAppExitListener() {
CompletableFuture<Long> exitTimeCallback = new CompletableFuture<>();
try {
int uid = mContext.getPackageManager().getPackageUid(APP_PKG_NAME, 0);
runWithShellPermissionIdentity(() ->
mActivityManager.addOnUidImportanceListener(new SingleAppExitListener(
uid, IMPORTANCE_FOREGROUND, exitTimeCallback), IMPORTANCE_FOREGROUND));
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError("Package not found.", e);
}
return exitTimeCallback;
}
private class SingleAppExitListener implements ActivityManager.OnUidImportanceListener {
private final int mUid;
private final int mImportance;
private final CompletableFuture<Long> mCallback;
SingleAppExitListener(int uid, int importance, CompletableFuture<Long> callback) {
mUid = uid;
mImportance = importance;
mCallback = callback;
}
@Override
public void onUidImportance(int uid, int importance) {
if (uid == mUid && importance > mImportance) {
mCallback.complete(System.currentTimeMillis());
mActivityManager.removeOnUidImportanceListener(this);
}
}
}
}