blob: 83154886879c14808cc2f83fed455e5eddfa099a [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.app.cts.fgstest;
import static android.app.fgstesthelper.LocalForegroundServiceBase.RESULT_OK;
import static android.app.fgstesthelper.LocalForegroundServiceBase.RESULT_SECURITY_EXCEPTION;
import static android.app.fgstesthelper.LocalForegroundServiceBase.RESULT_TYPE_EXCEPTION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.ForegroundServiceTypePolicy;
import android.app.ForegroundServiceTypePolicy.ForegroundServiceTypePolicyInfo;
import android.app.Instrumentation;
import android.app.cts.android.app.cts.tools.WatchUidRunner;
import android.app.fgstesthelper.LocalForegroundServiceBase;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ServiceInfo;
import android.support.test.uiautomator.UiDevice;
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.SystemUtil;
import com.android.internal.util.ArrayUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public final class ActivityManagerForegroundServiceTypeTest {
private static final String TAG = ActivityManagerForegroundServiceTypeTest.class.getName();
private static final String TEST_PKG_NAME_TARGET = "android.app.fgstesthelper";
private static final String TEST_PKG_NAME_CURRENT = "android.app.fgstesthelpercurrent";
private static final String TEST_PKG_NAME_API33 = "android.app.fgstesthelper33";
private static final String SHELL_PKG_NAME = "com.android.shell";
private static final String TEST_CLS_NAME_NO_TYPE =
"android.app.fgstesthelper.LocalForegroundServiceNoType";
private static final String TEST_CLS_NAME_ALL_TYPE =
"android.app.fgstesthelper.LocalForegroundServiceAllTypes";
private static final String FGS_TYPE_PERMISSION_CHANGE_ID = "FGS_TYPE_PERMISSION_CHANGE_ID";
private static final long WAITFOR_MSEC = 5000;
private static final ComponentName TEST_COMP_TARGET_FGS_NO_TYPE = new ComponentName(
TEST_PKG_NAME_TARGET, TEST_CLS_NAME_NO_TYPE);
private static final ComponentName TEST_COMP_TARGET_FGS_ALL_TYPE = new ComponentName(
TEST_PKG_NAME_TARGET, TEST_CLS_NAME_ALL_TYPE);
private static final ComponentName TEST_COMP_CURRENT_FGS_NO_TYPE = new ComponentName(
TEST_PKG_NAME_CURRENT, TEST_CLS_NAME_NO_TYPE);
private static final ComponentName TEST_COMP_CURRENT_FGS_ALL_TYPE = new ComponentName(
TEST_PKG_NAME_CURRENT, TEST_CLS_NAME_ALL_TYPE);
private static final ComponentName TEST_COMP_API33_FGS_NO_TYPE = new ComponentName(
TEST_PKG_NAME_API33, TEST_CLS_NAME_NO_TYPE);
private static final ComponentName TEST_COMP_API33_FGS_ALL_TYPE = new ComponentName(
TEST_PKG_NAME_API33, TEST_CLS_NAME_ALL_TYPE);
private static final String SPECIAL_PERMISSION_OP_ALLOWLISTED = "SPECIAL_PERM_ALLOWLISTED";
private static final ArrayMap<String, SpecialPermissionOp> sSpecialPermissionOps =
new ArrayMap<>();
private Context mContext;
private Context mTargetContext;
private Instrumentation mInstrumentation;
private ActivityManager mActivityManager;
private PackageManager mPackageManager;
@Before
public void setUp() {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mContext = mInstrumentation.getContext();
mTargetContext = mInstrumentation.getTargetContext();
mActivityManager = mInstrumentation.getContext().getSystemService(ActivityManager.class);
mPackageManager = mInstrumentation.getContext().getPackageManager();
if (sSpecialPermissionOps.isEmpty()) {
sSpecialPermissionOps.put(SPECIAL_PERMISSION_OP_ALLOWLISTED,
new DeviceAllowlistPermissionOp());
}
}
@After
public void tearDown() throws Exception {
SystemUtil.runWithShellPermissionIdentity(() -> {
mActivityManager.forceStopPackage(TEST_PKG_NAME_CURRENT);
mActivityManager.forceStopPackage(TEST_PKG_NAME_API33);
});
}
@Test
public void testForegroundServiceTypeNone() throws Exception {
try {
enablePermissionEnforcement(false, TEST_PKG_NAME_CURRENT);
enablePermissionEnforcement(false, TEST_PKG_NAME_API33);
testForegroundServiceTypeDisabledCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE,
TEST_COMP_API33_FGS_NO_TYPE, TEST_COMP_CURRENT_FGS_NO_TYPE);
testForegroundServiceTypeDisabledCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST,
TEST_COMP_API33_FGS_NO_TYPE, TEST_COMP_CURRENT_FGS_NO_TYPE);
} finally {
clearPermissionEnforcement(TEST_PKG_NAME_CURRENT);
clearPermissionEnforcement(TEST_PKG_NAME_API33);
}
}
@Test
public void testForegroundServiceTypeDataSync() throws Exception {
try {
enablePermissionEnforcement(false, TEST_PKG_NAME_CURRENT);
enablePermissionEnforcement(false, TEST_PKG_NAME_API33);
testForegroundServiceTypeDisabledCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
TEST_COMP_API33_FGS_ALL_TYPE, TEST_COMP_CURRENT_FGS_ALL_TYPE);
} finally {
clearPermissionEnforcement(TEST_PKG_NAME_CURRENT);
clearPermissionEnforcement(TEST_PKG_NAME_API33);
}
}
@Test
public void testForegroundServiceTypeDataSyncPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
}
@Test
public void testForegroundServiceTypeMediaPlaybackPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
}
@Test
public void testForegroundServiceTypePhoneCallPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL);
}
@Test
public void testForegroundServiceTypeLocationPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
}
@Test
public void testForegroundServiceTypeConnectedDevicePermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
}
@Test
public void testForegroundServiceTypeMediaProjectionPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
}
@Test
public void testForegroundServiceTypeCameraPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA);
}
@Test
public void testForegroundServiceTypeMicrophonePermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
}
@Test
public void testForegroundServiceTypeHealthPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH);
}
@Test
public void testForegroundServiceTypeRemoteMessagingPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING);
}
@Test
public void testForegroundServiceTypeSystemExemptedPermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
new String[] {SPECIAL_PERMISSION_OP_ALLOWLISTED});
}
@Test
public void testForegroundServiceTypeSpecialUsePermission() throws Exception {
testPermissionEnforcementCommon(ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
}
@Test
public void testForegroundServiceTypeSpecialUseProperty() throws Exception {
final String expectedPropertyValue = "foo";
try {
final PackageManager.Property prop = mTargetContext.getPackageManager()
.getProperty(PackageManager.PROPERTY_SPECIAL_USE_FGS_SUBTYPE,
TEST_COMP_TARGET_FGS_NO_TYPE);
fail("Property " + PackageManager.PROPERTY_SPECIAL_USE_FGS_SUBTYPE + " not expected.");
} catch (PackageManager.NameNotFoundException e) {
// expected.
}
final PackageManager.Property prop = mTargetContext.getPackageManager()
.getProperty(PackageManager.PROPERTY_SPECIAL_USE_FGS_SUBTYPE,
TEST_COMP_TARGET_FGS_ALL_TYPE);
assertEquals(expectedPropertyValue, prop.getString());
}
private void testForegroundServiceTypeDisabledCommon(int type,
ComponentName api33Comp, ComponentName apiCurComp) throws Exception {
final ApplicationInfo appCurInfo = mTargetContext.getPackageManager().getApplicationInfo(
TEST_PKG_NAME_CURRENT, 0);
final ApplicationInfo app33Info = mTargetContext.getPackageManager().getApplicationInfo(
TEST_PKG_NAME_API33, 0);
final WatchUidRunner uidCurWatcher = new WatchUidRunner(mInstrumentation, appCurInfo.uid,
WAITFOR_MSEC);
final WatchUidRunner uid33Watcher = new WatchUidRunner(mInstrumentation, app33Info.uid,
WAITFOR_MSEC);
final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy();
final ForegroundServiceTypePolicyInfo info = policy.getForegroundServiceTypePolicyInfo(
type, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
try {
SystemUtil.runWithShellPermissionIdentity(() -> {
info.setTypeDisabledForTest(false, TEST_PKG_NAME_CURRENT);
info.setTypeDisabledForTest(false, TEST_PKG_NAME_API33);
});
startAndStopFgsType(api33Comp, type, uid33Watcher);
startAndStopFgsType(apiCurComp, type, uidCurWatcher);
SystemUtil.runWithShellPermissionIdentity(() -> {
info.setTypeDisabledForTest(true, TEST_PKG_NAME_CURRENT);
});
assertEquals(RESULT_TYPE_EXCEPTION, startForegroundServiceWithType(apiCurComp, type));
stopService(apiCurComp, null);
startAndStopFgsType(api33Comp, type, uid33Watcher);
} finally {
SystemUtil.runWithShellPermissionIdentity(() -> {
info.clearTypeDisabledForTest(TEST_PKG_NAME_CURRENT);
info.clearTypeDisabledForTest(TEST_PKG_NAME_API33);
});
}
}
private void startAndStopFgsType(ComponentName compName, int type, WatchUidRunner uidWatcher)
throws Exception {
assertEquals(RESULT_OK, startForegroundServiceWithType(compName, type));
if (uidWatcher != null) {
uidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE, null);
}
stopService(compName, uidWatcher);
}
private int startForegroundServiceWithType(ComponentName compName, int type) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final int[] result = new int[1];
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
result[0] = intent.getIntExtra(LocalForegroundServiceBase.EXTRA_RESULT_CODE,
RESULT_OK);
latch.countDown();
}
};
final Intent intent = new Intent();
intent.setComponent(compName);
intent.putExtra(LocalForegroundServiceBase.EXTRA_COMMAND,
LocalForegroundServiceBase.COMMAND_START_FOREGROUND);
intent.putExtra(LocalForegroundServiceBase.EXTRA_FGS_TYPE, type);
final IntentFilter filter =
new IntentFilter(LocalForegroundServiceBase.ACTION_START_FGS_RESULT);
try {
mTargetContext.registerReceiver(receiver, filter);
mTargetContext.startForegroundService(intent);
latch.await(WAITFOR_MSEC, TimeUnit.MILLISECONDS);
return result[0];
} finally {
mTargetContext.unregisterReceiver(receiver);
}
}
private void stopService(ComponentName compName, WatchUidRunner uidWatcher) throws Exception {
final Intent intent = new Intent();
intent.setComponent(compName);
intent.putExtra(LocalForegroundServiceBase.EXTRA_COMMAND,
LocalForegroundServiceBase.COMMAND_STOP_SELF);
mTargetContext.startService(intent);
if (uidWatcher != null) {
uidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY,
null);
}
}
private void testPermissionEnforcementCommon(int type) throws Exception {
testPermissionEnforcementCommon(type, null);
}
private void testPermissionEnforcementCommon(int type, String[] specialOps) throws Exception {
final String testPackageName = TEST_PKG_NAME_TARGET;
TestPermissionInfo[] allOfPermissions = null;
TestPermissionInfo[] anyOfPermissions = null;
try {
// Enable the permission check.
enablePermissionEnforcement(true, testPackageName);
final ForegroundServiceTypePolicy policy =
ForegroundServiceTypePolicy.getDefaultPolicy();
final ForegroundServiceTypePolicyInfo info = policy.getForegroundServiceTypePolicyInfo(
type, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
assertEquals(type, info.getForegroundServiceType());
allOfPermissions = triagePermissions(
info.getRequiredAllOfPermissionsForTest(mTargetContext).orElse(null));
anyOfPermissions = ArrayUtils.concat(TestPermissionInfo.class,
triagePermissions(info.getRequiredAnyOfPermissionsForTest(
mTargetContext).orElse(null)),
triagePermissions(specialOps));
// If we grant all of the permissions, the foreground service start will succeed.
grantPermissions(ArrayUtils.concat(TestPermissionInfo.class,
allOfPermissions, anyOfPermissions), testPackageName);
startAndStopFgsType(TEST_COMP_TARGET_FGS_ALL_TYPE, type, null);
resetPermissions(anyOfPermissions, testPackageName);
// If we grant all of the "allOf" permission, but none of the "anyOf" permission, it
// should fail to start a foreground service.
if (!ArrayUtils.isEmpty(anyOfPermissions)) {
grantPermissions(allOfPermissions, testPackageName);
assertEquals(RESULT_SECURITY_EXCEPTION,
startForegroundServiceWithType(TEST_COMP_TARGET_FGS_ALL_TYPE, type));
stopService(TEST_COMP_TARGET_FGS_ALL_TYPE, null);
resetPermissions(anyOfPermissions, testPackageName);
// If we grant any of them, it should succeed.
for (TestPermissionInfo perm: anyOfPermissions) {
grantPermissions(ArrayUtils.concat(TestPermissionInfo.class,
allOfPermissions, new TestPermissionInfo[] {perm}),
testPackageName);
startAndStopFgsType(TEST_COMP_TARGET_FGS_ALL_TYPE, type, null);
resetPermissions(anyOfPermissions, testPackageName);
}
}
// If we skip one of the "allOf" permissions, it should fail.
if (!ArrayUtils.isEmpty(allOfPermissions)) {
for (int i = 0; i < allOfPermissions.length; i++) {
final TestPermissionInfo[] perms = getListExceptIndex(allOfPermissions, i);
grantPermissions(ArrayUtils.concat(TestPermissionInfo.class,
perms, anyOfPermissions), testPackageName);
assertEquals(RESULT_SECURITY_EXCEPTION,
startForegroundServiceWithType(TEST_COMP_TARGET_FGS_ALL_TYPE, type));
stopService(TEST_COMP_TARGET_FGS_ALL_TYPE, null);
resetPermissions(anyOfPermissions, testPackageName);
}
}
} finally {
resetPermissions(anyOfPermissions, testPackageName);
enablePermissionEnforcement(false, testPackageName);
}
}
private TestPermissionInfo[] getListExceptIndex(TestPermissionInfo[] list, int exceptIndex) {
final ArrayList<TestPermissionInfo> ret = new ArrayList<>();
for (int i = 0; i < list.length; i++) {
if (i == exceptIndex) {
continue;
}
ret.add(list[i]);
}
if (ret.size() > 0) {
return ret.toArray(new TestPermissionInfo[ret.size()]);
} else {
return null;
}
}
private void enablePermissionEnforcement(boolean enable, String packageName) throws Exception {
if (enable) {
executeShellCommand("am compat enable --no-kill FGS_TYPE_PERMISSION_CHANGE_ID "
+ packageName);
} else {
executeShellCommand("am compat disable --no-kill FGS_TYPE_PERMISSION_CHANGE_ID "
+ packageName);
}
}
private void clearPermissionEnforcement(String packageName) throws Exception {
executeShellCommand("am compat reset --no-kill FGS_TYPE_PERMISSION_CHANGE_ID "
+ packageName);
}
private String executeShellCommand(String cmd) throws Exception {
final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
return uiDevice.executeShellCommand(cmd).trim();
}
private class TestPermissionInfo {
final String mName;
final boolean mIsAppOps;
final SpecialPermissionOp mSpecialOp;
TestPermissionInfo(String name, boolean isAppOps, SpecialPermissionOp specialOp) {
mName = name;
mIsAppOps = isAppOps;
mSpecialOp = specialOp;
}
}
private interface SpecialPermissionOp {
void grantPermission(String packageName) throws Exception;
void revokePermission(String packageName) throws Exception;
}
private class DeviceAllowlistPermissionOp implements SpecialPermissionOp {
@Override
public void grantPermission(String packageName) throws Exception {
executeShellCommand("cmd deviceidle whitelist +" + packageName);
}
@Override
public void revokePermission(String packageName) throws Exception {
executeShellCommand("cmd deviceidle whitelist -" + packageName);
}
}
private TestPermissionInfo[] triagePermissions(String[] permissions) {
final ArrayList<TestPermissionInfo> perms = new ArrayList<>();
if (permissions != null) {
for (String perm : permissions) {
PermissionInfo pi = null;
try {
pi = mPackageManager.getPermissionInfo(perm, 0);
} catch (PackageManager.NameNotFoundException e) {
// It could be an appop.
}
if (pi != null) {
perms.add(new TestPermissionInfo(perm, false, null));
} else if (sSpecialPermissionOps.containsKey(perm)) {
perms.add(new TestPermissionInfo(perm, false, sSpecialPermissionOps.get(perm)));
} else {
try {
AppOpsManager.strOpToOp(perm);
perms.add(new TestPermissionInfo(perm, true, null));
} catch (IllegalArgumentException e) {
// We don't support other type of permissions in CTS tests here.
}
}
}
}
return perms.toArray(new TestPermissionInfo[perms.size()]);
}
private void grantPermissions(TestPermissionInfo[] permissions, String packageName)
throws Exception {
if (ArrayUtils.isEmpty(permissions)) {
return;
}
final String[] regularPermissions = Arrays.stream(permissions)
.filter(p -> !p.mIsAppOps && p.mSpecialOp == null)
.map(p -> p.mName)
.toArray(String[]::new);
final String[] appops = Arrays.stream(permissions)
.filter(p -> p.mIsAppOps && p.mSpecialOp == null)
.map(p -> p.mName)
.toArray(String[]::new);
final SpecialPermissionOp[] specialOps = Arrays.stream(permissions)
.filter(p-> p.mSpecialOp != null)
.map(p -> p.mSpecialOp)
.toArray(SpecialPermissionOp[]::new);
if (!ArrayUtils.isEmpty(regularPermissions)) {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(regularPermissions);
}
if (!ArrayUtils.isEmpty(appops)) {
for (String appop : appops) {
// Because we're adopting the shell identity, we have to set the appop to shell here
executeShellCommand("appops set --uid " + SHELL_PKG_NAME + " " + appop + " allow");
}
}
if (!ArrayUtils.isEmpty(specialOps)) {
for (SpecialPermissionOp op : specialOps) {
op.grantPermission(packageName);
}
}
}
private void resetPermissions(TestPermissionInfo[] permissions, String packageName)
throws Exception {
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
executeShellCommand("appops reset " + SHELL_PKG_NAME);
if (permissions != null) {
final SpecialPermissionOp[] specialOps = Arrays.stream(permissions)
.filter(p-> p.mSpecialOp != null)
.map(p -> p.mSpecialOp)
.toArray(SpecialPermissionOp[]::new);
if (!ArrayUtils.isEmpty(specialOps)) {
for (SpecialPermissionOp op : specialOps) {
op.revokePermission(packageName);
}
}
}
}
}