| /* |
| * Copyright (C) 2021 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.devicepolicy.cts; |
| |
| import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; |
| import static android.app.admin.DevicePolicyManager.ACTION_DATA_SHARING_RESTRICTION_APPLIED; |
| import static android.app.admin.DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT; |
| import static android.app.admin.DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED; |
| import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; |
| import static android.os.UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE; |
| |
| import static com.android.bedstead.harrier.DeviceState.UserType.PRIMARY_USER; |
| import static com.android.bedstead.harrier.DeviceState.UserType.WORK_PROFILE; |
| import static com.android.bedstead.remotedpc.RemoteDpc.DPC_COMPONENT_NAME; |
| import static com.android.queryable.queries.ActivityQuery.activity; |
| import static com.android.queryable.queries.IntentFilterQuery.intentFilter; |
| |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ResolveInfo; |
| |
| import com.android.bedstead.harrier.BedsteadJUnit4; |
| import com.android.bedstead.harrier.DeviceState; |
| import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile; |
| import com.android.bedstead.harrier.annotations.Postsubmit; |
| import com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile; |
| import com.android.bedstead.nene.TestApis; |
| import com.android.bedstead.nene.permissions.PermissionContext; |
| import com.android.bedstead.nene.users.UserReference; |
| import com.android.bedstead.remotedpc.RemoteDpc; |
| import com.android.bedstead.testapp.TestApp; |
| import com.android.bedstead.testapp.TestAppInstance; |
| import com.android.bedstead.testapp.TestAppProvider; |
| import com.android.compatibility.common.util.BlockingBroadcastReceiver; |
| |
| import org.junit.ClassRule; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.List; |
| |
| @RunWith(BedsteadJUnit4.class) |
| public final class CrossProfileSharingTest { |
| @ClassRule |
| @Rule |
| public static final DeviceState sDeviceState = new DeviceState(); |
| |
| private static final Context sContext = TestApis.context().instrumentedContext(); |
| private static final TestAppProvider sTestAppProvider = new TestAppProvider(); |
| |
| private static final TestApp sTestApp = sTestAppProvider.query().whereActivities().contains( |
| activity().intentFilters().contains( |
| intentFilter().actions().contains("com.android.testapp.SOME_ACTION"), |
| intentFilter().actions().contains("android.intent.action.PICK") |
| ) |
| ).get(); |
| |
| // Known action that is handled in the opposite profile, used to query forwarder activity. |
| private static final String CROSS_PROFILE_ACTION = "com.android.testapp.SOME_ACTION"; |
| |
| // TODO(b/191640667): use parametrization instead of looping once available. |
| // These are the data sharing intents which can be forwarded to the primary profile. |
| private static final Intent[] OPENING_INTENTS = { |
| new Intent(Intent.ACTION_GET_CONTENT).setType("*/*").addCategory( |
| Intent.CATEGORY_OPENABLE), |
| new Intent(Intent.ACTION_OPEN_DOCUMENT).setType("*/*").addCategory( |
| Intent.CATEGORY_OPENABLE), |
| new Intent(Intent.ACTION_PICK).setType("*/*").addCategory( |
| Intent.CATEGORY_DEFAULT), |
| new Intent(Intent.ACTION_PICK).addCategory(Intent.CATEGORY_DEFAULT) |
| }; |
| |
| // These are the data sharing intents which can be forwarded to the managed profile. |
| private static final Intent[] SHARING_INTENTS = { |
| new Intent(Intent.ACTION_SEND).setType("*/*"), |
| new Intent(Intent.ACTION_SEND_MULTIPLE).setType("*/*") |
| }; |
| |
| /** |
| * Test sharing initiated from the profile side i.e. user tries to pick up personal data within |
| * a work app when DISALLOW_SHARE_INTO_MANAGED_PROFILE is enforced. |
| */ |
| @Test |
| @Postsubmit(reason = "new test") |
| @RequireRunOnWorkProfile |
| public void openingPersonalFromProfile_disallowShareIntoProfile_restrictionApplied() { |
| ResolveInfo toPersonalForwarderInfo = getWorkToPersonalForwarder(); |
| |
| // Enforce the restriction and wait for it to be applied. |
| setSharingIntoProfileEnabled(false); |
| // Test app handles android.intent.action.PICK just in case no other app does. |
| try (TestAppInstance testAppParent = |
| sTestApp.install(sDeviceState.primaryUser())) { |
| // Verify that the intents don't resolve into cross-profile forwarder. |
| assertCrossProfileIntentsResolvability(OPENING_INTENTS, |
| toPersonalForwarderInfo, /* expectForwardable */ false); |
| } finally { |
| // Restore default state. |
| setSharingIntoProfileEnabled(true); |
| } |
| } |
| |
| /** |
| * Test sharing initiated from the profile side i.e. user tries to pick up personal data within |
| * a work app when DISALLOW_SHARE_INTO_MANAGED_PROFILE is NOT enforced. |
| */ |
| @Test |
| @Postsubmit(reason = "new test") |
| @RequireRunOnWorkProfile |
| public void openingPersonalFromProfile_disallowShareIntoProfile_restrictionRemoved() { |
| ResolveInfo workToPersonalForwarder = getWorkToPersonalForwarder(); |
| |
| // Enforce the restriction and wait for it to be applied, then remove it and wait again. |
| setSharingIntoProfileEnabled(false); |
| setSharingIntoProfileEnabled(true); |
| |
| // Test app handles android.intent.action.PICK just in case no other app does. |
| try (TestAppInstance testAppParent = |
| sTestApp.install(sDeviceState.primaryUser())) { |
| // Verify that the intents resolve into cross-profile forwarder. |
| assertCrossProfileIntentsResolvability( |
| OPENING_INTENTS, workToPersonalForwarder, /* expectForwardable */ true); |
| } |
| } |
| |
| @Test |
| @Postsubmit(reason = "new test") |
| @EnsureHasWorkProfile(forUser = PRIMARY_USER) |
| public void sharingFromPersonalToWork_disallowShareIntoProfile_restrictionApplied() { |
| ResolveInfo personalToWorkForwarder = getPersonalToWorkForwarder(); |
| |
| // Enforce the restriction and wait for it to be applied. |
| setSharingIntoProfileEnabled(false); |
| |
| try { |
| // Verify that sharing intent doesn't get resolve into profile forwarder. |
| assertCrossProfileIntentsResolvability( |
| SHARING_INTENTS, personalToWorkForwarder, /* expectForwardable */ false); |
| } finally { |
| setSharingIntoProfileEnabled(true); |
| } |
| |
| } |
| |
| @Test |
| @Postsubmit(reason = "new test") |
| @EnsureHasWorkProfile(forUser = PRIMARY_USER) |
| public void sharingFromPersonalToWork_disallowShareIntoProfile_restrictionRemoved() { |
| ResolveInfo personalToWorkForwarder = getPersonalToWorkForwarder(); |
| |
| // Enforce the restriction and wait for it to be applied, then remove it and wait again. |
| setSharingIntoProfileEnabled(false); |
| setSharingIntoProfileEnabled(true); |
| |
| // Verify that sharing intent gets resolved into profile forwarder successfully. |
| assertCrossProfileIntentsResolvability( |
| SHARING_INTENTS, personalToWorkForwarder, /* expectForwardable */ true); |
| } |
| |
| private ResolveInfo getPersonalToWorkForwarder() { |
| return getResolveInfo(sDeviceState.workProfile(), FLAG_MANAGED_CAN_ACCESS_PARENT); |
| } |
| |
| private ResolveInfo getWorkToPersonalForwarder() { |
| return getResolveInfo(sDeviceState.primaryUser(), FLAG_PARENT_CAN_ACCESS_MANAGED); |
| } |
| |
| /** |
| * Finds ResolveInfo for a system activity that forwards cross-profile intent by resolving an |
| * intent that it handled in one profile from the other profile. |
| * TODO(b/198759180): replace it with a @TestApi |
| */ |
| private ResolveInfo getResolveInfo(UserReference targetProfile, int direction) { |
| ResolveInfo forwarderInfo; |
| try (TestAppInstance testApp = sTestApp.install(targetProfile)) { |
| // Set up cross profile intent filters so we can resolve these to find out framework's |
| // intent forwarder activity as ground truth |
| sDeviceState.profileOwner(WORK_PROFILE).devicePolicyManager() |
| .addCrossProfileIntentFilter(DPC_COMPONENT_NAME, |
| new IntentFilter(CROSS_PROFILE_ACTION), direction); |
| try { |
| forwarderInfo = getCrossProfileIntentForwarder(new Intent(CROSS_PROFILE_ACTION)); |
| } finally { |
| sDeviceState.profileOwner(WORK_PROFILE).devicePolicyManager() |
| .clearCrossProfileIntentFilters(DPC_COMPONENT_NAME); |
| } |
| } |
| return forwarderInfo; |
| } |
| |
| private ResolveInfo getCrossProfileIntentForwarder(Intent intent) { |
| List<ResolveInfo> result = TestApis.context().instrumentedContext().getPackageManager() |
| .queryIntentActivities(intent, MATCH_DEFAULT_ONLY); |
| assertWithMessage("Failed to get intent forwarder component") |
| .that(result.size()).isEqualTo(1); |
| ResolveInfo forwarder = result.get(0); |
| assertWithMessage("Forwarder doesn't consider itself as such") |
| .that(forwarder.isCrossProfileIntentForwarderActivity()).isTrue(); |
| return forwarder; |
| } |
| |
| private void setSharingIntoProfileEnabled(boolean enabled) { |
| RemoteDpc remoteDpc = sDeviceState.profileOwner(WORK_PROFILE); |
| IntentFilter filter = new IntentFilter(ACTION_DATA_SHARING_RESTRICTION_APPLIED); |
| Context remoteCtx = TestApis.context().androidContextAsUser(remoteDpc.user()); |
| try (PermissionContext permissionContext = |
| TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL); |
| BlockingBroadcastReceiver receiver = |
| BlockingBroadcastReceiver.create(remoteCtx, filter).register()) { |
| if (enabled) { |
| remoteDpc.devicePolicyManager().clearUserRestriction( |
| DPC_COMPONENT_NAME, DISALLOW_SHARE_INTO_MANAGED_PROFILE); |
| } else { |
| remoteDpc.devicePolicyManager().addUserRestriction( |
| DPC_COMPONENT_NAME, DISALLOW_SHARE_INTO_MANAGED_PROFILE); |
| } |
| } |
| } |
| |
| private void assertCrossProfileIntentsResolvability( |
| Intent[] intents, ResolveInfo expectedForwarder, boolean expectForwardable) { |
| for (Intent intent : intents) { |
| List<ResolveInfo> resolveInfoList = TestApis.context().instrumentedContext() |
| .getPackageManager().queryIntentActivities(intent, MATCH_DEFAULT_ONLY); |
| if (expectForwardable) { |
| assertWithMessage(String.format( |
| "Expect %s to be forwardable, but resolve list does not contain expected " |
| + "intent forwarder %s", intent, expectedForwarder)) |
| .that(containsResolveInfo(resolveInfoList, expectedForwarder)).isTrue(); |
| } else { |
| assertWithMessage(String.format( |
| "Expect %s not to be forwardable, but resolve list contains intent " |
| + "forwarder %s", intent, expectedForwarder)) |
| .that(containsResolveInfo(resolveInfoList, expectedForwarder)).isFalse(); |
| } |
| } |
| } |
| |
| private boolean containsResolveInfo(List<ResolveInfo> list, ResolveInfo info) { |
| for (ResolveInfo entry : list) { |
| if (entry.activityInfo.packageName.equals(info.activityInfo.packageName) |
| && entry.activityInfo.name.equals(info.activityInfo.name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |