1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.devicepolicy.cts; 18 19 import static android.app.admin.DevicePolicyManager.ACTION_DATA_SHARING_RESTRICTION_APPLIED; 20 import static android.app.admin.DevicePolicyManager.EXTRA_RESTRICTION; 21 import static android.app.admin.DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT; 22 import static android.app.admin.DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED; 23 import static android.content.Intent.ACTION_SEND; 24 import static android.content.Intent.CATEGORY_DEFAULT; 25 import static android.content.Intent.EXTRA_TEXT; 26 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; 27 import static android.os.UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE; 28 29 import static com.android.bedstead.enterprise.EnterpriseDeviceStateExtensionsKt.dpc; 30 import static com.android.bedstead.enterprise.EnterpriseDeviceStateExtensionsKt.profileOwner; 31 import static com.android.bedstead.enterprise.EnterpriseDeviceStateExtensionsKt.workProfile; 32 import static com.android.bedstead.harrier.UserType.WORK_PROFILE; 33 import static com.android.bedstead.nene.userrestrictions.CommonUserRestrictions.DISALLOW_CROSS_PROFILE_COPY_PASTE; 34 import static com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS_FULL; 35 import static com.android.bedstead.testapps.TestAppsDeviceStateExtensionsKt.testApps; 36 import static com.android.queryable.queries.ActivityQuery.activity; 37 import static com.android.queryable.queries.IntentFilterQuery.intentFilter; 38 39 import static com.google.common.truth.Truth.assertThat; 40 import static com.google.common.truth.Truth.assertWithMessage; 41 42 import android.content.ComponentName; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.content.IntentFilter; 46 import android.content.pm.ActivityInfo; 47 import android.content.pm.PackageManager; 48 import android.content.pm.ResolveInfo; 49 50 import com.android.bedstead.enterprise.annotations.EnsureDoesNotHaveUserRestriction; 51 import com.android.bedstead.enterprise.annotations.EnsureHasUserRestriction; 52 import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile; 53 import com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile; 54 import com.android.bedstead.harrier.BedsteadJUnit4; 55 import com.android.bedstead.harrier.DeviceState; 56 import com.android.bedstead.harrier.annotations.Postsubmit; 57 import com.android.bedstead.nene.TestApis; 58 import com.android.bedstead.nene.exceptions.NeneException; 59 import com.android.bedstead.nene.packages.ComponentReference; 60 import com.android.bedstead.nene.users.UserReference; 61 import com.android.bedstead.nene.utils.BlockingBroadcastReceiver; 62 import com.android.bedstead.nene.utils.Poll; 63 import com.android.bedstead.nene.utils.ResolveInfoWrapper; 64 import com.android.bedstead.permissions.annotations.EnsureHasPermission; 65 import com.android.bedstead.remotedpc.RemoteDpc; 66 import com.android.bedstead.testapp.TestApp; 67 import com.android.bedstead.testapp.TestAppInstance; 68 import com.android.compatibility.common.util.ApiTest; 69 70 import org.junit.ClassRule; 71 import org.junit.Rule; 72 import org.junit.Test; 73 import org.junit.runner.RunWith; 74 75 import java.util.List; 76 import java.util.stream.Collectors; 77 78 @RunWith(BedsteadJUnit4.class) 79 public final class CrossProfileSharingTest { 80 @ClassRule 81 @Rule 82 public static final DeviceState sDeviceState = new DeviceState(); 83 84 private static final TestApp sTestApp = testApps(sDeviceState).query() 85 .whereActivities().contains( 86 activity().where().intentFilters().contains( 87 intentFilter().where().actions().contains("com.android.testapp.SOME_ACTION"), 88 intentFilter().where().actions().contains("android.intent.action.PICK"), 89 intentFilter().where().actions().contains("android.intent.action.SEND_MULTIPLE") 90 )).get(); 91 92 // Known action that is handled in the opposite profile, used to query forwarder activity. 93 private static final String CROSS_PROFILE_ACTION = "com.android.testapp.SOME_ACTION"; 94 95 // TODO(b/191640667): use parametrization instead of looping once available. 96 // These are the data sharing intents which can be forwarded to the primary profile. 97 private static final Intent[] OPENING_INTENTS = { 98 new Intent(Intent.ACTION_GET_CONTENT).setType("*/*").addCategory( 99 Intent.CATEGORY_OPENABLE), 100 new Intent(Intent.ACTION_OPEN_DOCUMENT).setType("*/*").addCategory( 101 Intent.CATEGORY_OPENABLE), 102 new Intent(Intent.ACTION_PICK).setType("*/*").addCategory( 103 Intent.CATEGORY_DEFAULT), 104 new Intent(Intent.ACTION_PICK).addCategory(Intent.CATEGORY_DEFAULT) 105 }; 106 107 // These are the data sharing intents which can be forwarded to the managed profile. 108 private static final Intent[] SHARING_INTENTS = { 109 new Intent(ACTION_SEND).setType("*/*"), 110 new Intent(Intent.ACTION_SEND_MULTIPLE).setType("*/*") 111 }; 112 113 private static final IntentFilter SEND_INTENT_FILTER = 114 IntentFilter.create(ACTION_SEND, /* dataType= */ "*/*"); 115 static { 116 SEND_INTENT_FILTER.addCategory(CATEGORY_DEFAULT); 117 } 118 119 private static final Intent SEND_INTENT = 120 new Intent(ACTION_SEND).setType("text/plain").addCategory(CATEGORY_DEFAULT) 121 .putExtra(EXTRA_TEXT, "example"); 122 private static final Intent CHOOSER_INTENT = 123 Intent.createChooser(SEND_INTENT, /* title= */ null); 124 125 /** 126 * Test sharing initiated from the profile side i.e. user tries to pick up personal data within 127 * a work app when DISALLOW_SHARE_INTO_MANAGED_PROFILE is enforced. 128 */ 129 @Test 130 @Postsubmit(reason = "new test") 131 @RequireRunOnWorkProfile 132 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) openingPersonalFromProfile_disallowShareIntoProfile_restrictionApplied()133 public void openingPersonalFromProfile_disallowShareIntoProfile_restrictionApplied() { 134 ResolveInfo toPersonalForwarderInfo = getWorkToPersonalForwarder(); 135 136 // Enforce the restriction and wait for it to be applied. 137 setSharingIntoProfileEnabled(false); 138 // Test app handles android.intent.action.PICK just in case no other app does. 139 try (TestAppInstance testAppParent = 140 sTestApp.install(sDeviceState.primaryUser())) { 141 // Verify that the intents don't resolve into cross-profile forwarder. 142 assertCrossProfileIntentsResolvability(OPENING_INTENTS, 143 toPersonalForwarderInfo, /* expectForwardable */ false); 144 } finally { 145 // Restore default state. 146 setSharingIntoProfileEnabled(true); 147 } 148 } 149 150 /** 151 * Test sharing initiated from the profile side i.e. user tries to pick up personal data within 152 * a work app when DISALLOW_SHARE_INTO_MANAGED_PROFILE is NOT enforced. 153 */ 154 @Test 155 @Postsubmit(reason = "new test") 156 @RequireRunOnWorkProfile 157 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) openingPersonalFromProfile_disallowShareIntoProfile_restrictionRemoved()158 public void openingPersonalFromProfile_disallowShareIntoProfile_restrictionRemoved() { 159 ResolveInfo workToPersonalForwarder = getWorkToPersonalForwarder(); 160 161 // Enforce the restriction and wait for it to be applied, then remove it and wait again. 162 setSharingIntoProfileEnabled(false); 163 setSharingIntoProfileEnabled(true); 164 165 // Test app handles android.intent.action.PICK just in case no other app does. 166 try (TestAppInstance testAppParent = 167 sTestApp.install(sDeviceState.primaryUser())) { 168 // Verify that the intents resolve into cross-profile forwarder. 169 assertCrossProfileIntentsResolvability( 170 OPENING_INTENTS, workToPersonalForwarder, /* expectForwardable */ true); 171 } 172 } 173 174 @Test 175 @Postsubmit(reason = "new test") 176 @EnsureHasWorkProfile 177 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) sharingFromPersonalToWork_disallowShareIntoProfile_restrictionApplied()178 public void sharingFromPersonalToWork_disallowShareIntoProfile_restrictionApplied() { 179 ResolveInfo personalToWorkForwarder = getPersonalToWorkForwarder(); 180 181 // Enforce the restriction and wait for it to be applied. 182 setSharingIntoProfileEnabled(false); 183 184 try { 185 // Verify that sharing intent doesn't get resolve into profile forwarder. 186 assertCrossProfileIntentsResolvability( 187 SHARING_INTENTS, personalToWorkForwarder, /* expectForwardable */ false); 188 } finally { 189 setSharingIntoProfileEnabled(true); 190 } 191 192 } 193 194 @Test 195 @Postsubmit(reason = "new test") 196 @EnsureHasWorkProfile 197 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) sharingFromPersonalToWork_disallowShareIntoProfile_restrictionRemoved()198 public void sharingFromPersonalToWork_disallowShareIntoProfile_restrictionRemoved() { 199 try (TestAppInstance testApp = sTestApp.install(workProfile(sDeviceState))) { 200 ResolveInfo personalToWorkForwarder = getPersonalToWorkForwarder(); 201 202 // Enforce the restriction and wait for it to be applied, then remove it and wait again. 203 setSharingIntoProfileEnabled(false); 204 setSharingIntoProfileEnabled(true); 205 206 // Verify that sharing intent gets resolved into profile forwarder successfully. 207 assertCrossProfileIntentsResolvability( 208 SHARING_INTENTS, personalToWorkForwarder, /* expectForwardable */ true); 209 } 210 } 211 212 @ApiTest(apis = "android.app.admin.DevicePolicyManager#addCrossProfileIntentFilter") 213 @Postsubmit(reason = "new test") 214 @Test 215 @EnsureHasWorkProfile(dpcIsPrimary = true) addCrossProfileIntentFilter_switchToOtherProfile_chooserActivityLaunched()216 public void addCrossProfileIntentFilter_switchToOtherProfile_chooserActivityLaunched() { 217 try { 218 IntentFilter sendIntentFilter = IntentFilter.create(ACTION_SEND, /* dataType= */ "*/*"); 219 sendIntentFilter.addCategory(CATEGORY_DEFAULT); 220 dpc(sDeviceState).devicePolicyManager().addCrossProfileIntentFilter( 221 dpc(sDeviceState).componentName(), sendIntentFilter, 222 FLAG_MANAGED_CAN_ACCESS_PARENT | FLAG_PARENT_CAN_ACCESS_MANAGED); 223 Intent switchToOtherProfileIntent = getSwitchToOtherProfileIntent(); 224 225 TestApis.context().instrumentedContext().startActivity(switchToOtherProfileIntent); 226 227 ComponentReference chooserActivityComponentReference = TestApis.activities() 228 .getResolvedActivityOfIntent(CHOOSER_INTENT, 229 PackageManager.MATCH_DEFAULT_ONLY); 230 231 if (chooserActivityComponentReference == null) { 232 throw new NeneException("Unable to resolve activity of Intent: " + CHOOSER_INTENT); 233 } 234 235 Poll.forValue("Chooser activity launched", 236 () -> TestApis.activities().foregroundActivity().componentName()) 237 .toBeEqualTo(chooserActivityComponentReference.componentName()) 238 .errorOnFail() 239 .await(); 240 } finally { 241 dpc(sDeviceState).devicePolicyManager().clearCrossProfileIntentFilters( 242 dpc(sDeviceState).componentName()); 243 } 244 } 245 246 @ApiTest(apis = {"android.app.admin.DevicePolicyManager#createAdminSupportIntent", 247 "android.os.UserManager#DISALLOW_CROSS_PROFILE_COPY_PASTE"}) 248 @Postsubmit(reason = "new test") 249 @EnsureHasUserRestriction(DISALLOW_CROSS_PROFILE_COPY_PASTE) 250 @Test createAdminSupportIntent_disallowCrossProfileCopyPaste_createsIntent()251 public void createAdminSupportIntent_disallowCrossProfileCopyPaste_createsIntent() { 252 Intent intent = TestApis.devicePolicy().createAdminSupportIntent( 253 DISALLOW_CROSS_PROFILE_COPY_PASTE); 254 255 assertThat(intent.getStringExtra(EXTRA_RESTRICTION)) 256 .isEqualTo(DISALLOW_CROSS_PROFILE_COPY_PASTE); 257 } 258 259 @ApiTest(apis = {"android.app.admin.DevicePolicyManager#createAdminSupportIntent", 260 "android.os.UserManager#DISALLOW_CROSS_PROFILE_COPY_PASTE"}) 261 @Postsubmit(reason = "new test") 262 @EnsureDoesNotHaveUserRestriction(DISALLOW_CROSS_PROFILE_COPY_PASTE) 263 @Test createAdminSupportIntent_allowCrossProfileCopyPaste_doesNotCreate()264 public void createAdminSupportIntent_allowCrossProfileCopyPaste_doesNotCreate() { 265 Intent intent = TestApis.devicePolicy().createAdminSupportIntent( 266 DISALLOW_CROSS_PROFILE_COPY_PASTE); 267 268 assertThat(intent).isNull(); 269 } 270 getSwitchToOtherProfileIntent()271 private Intent getSwitchToOtherProfileIntent() { 272 ResolveInfoWrapper switchToOtherProfileResolveInfo = getSwitchToOtherProfileResolveInfo( 273 ); 274 assertWithMessage("Could not retrieve the switch to other profile resolve info.") 275 .that(switchToOtherProfileResolveInfo) 276 .isNotNull(); 277 278 Intent switchToOtherProfileIntent = new Intent(CHOOSER_INTENT); 279 ActivityInfo activityInfo = switchToOtherProfileResolveInfo.activityInfo(); 280 switchToOtherProfileIntent.setComponent( 281 new ComponentName(activityInfo.packageName, activityInfo.name)); 282 switchToOtherProfileIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 283 return switchToOtherProfileIntent; 284 } 285 286 /** 287 * Returns the ResolveInfo of the "Switch Profiles" dialog if one exists for this intent 288 * (otherwise null). 289 */ getSwitchToOtherProfileResolveInfo()290 private ResolveInfoWrapper getSwitchToOtherProfileResolveInfo() { 291 // match == 0 means that this intent actually doesn't match to any activity on this profile, 292 // that means it should be on the other profile. 293 294 return TestApis.packages().queryIntentActivities(CrossProfileSharingTest.SEND_INTENT, 295 android.content.pm.PackageManager.MATCH_DEFAULT_ONLY) 296 .stream() 297 .filter(r -> r.match() == 0) 298 .findFirst() 299 .orElse(null); 300 } 301 getPersonalToWorkForwarder()302 private ResolveInfo getPersonalToWorkForwarder() { 303 return getResolveInfo(workProfile(sDeviceState), FLAG_MANAGED_CAN_ACCESS_PARENT); 304 } 305 getWorkToPersonalForwarder()306 private ResolveInfo getWorkToPersonalForwarder() { 307 return getResolveInfo(sDeviceState.primaryUser(), FLAG_PARENT_CAN_ACCESS_MANAGED); 308 } 309 310 /** 311 * Finds ResolveInfo for a system activity that forwards cross-profile intent by resolving an 312 * intent that it handled in one profile from the other profile. 313 * TODO(b/198759180): replace it with a @TestApi 314 */ getResolveInfo(UserReference targetProfile, int direction)315 private ResolveInfo getResolveInfo(UserReference targetProfile, int direction) { 316 ResolveInfo forwarderInfo; 317 try (TestAppInstance testApp = sTestApp.install(targetProfile)) { 318 // Set up cross profile intent filters so we can resolve these to find out framework's 319 // intent forwarder activity as ground truth 320 profileOwner(sDeviceState, WORK_PROFILE).devicePolicyManager() 321 .addCrossProfileIntentFilter(profileOwner(sDeviceState, WORK_PROFILE) 322 .componentName(), 323 new IntentFilter(CROSS_PROFILE_ACTION), direction); 324 try { 325 forwarderInfo = getCrossProfileIntentForwarder(new Intent(CROSS_PROFILE_ACTION)); 326 } finally { 327 profileOwner(sDeviceState, WORK_PROFILE).devicePolicyManager() 328 .clearCrossProfileIntentFilters( 329 profileOwner(sDeviceState, WORK_PROFILE).componentName()); 330 } 331 } 332 return forwarderInfo; 333 } 334 getCrossProfileIntentForwarder(Intent intent)335 private ResolveInfo getCrossProfileIntentForwarder(Intent intent) { 336 List<ResolveInfo> result = TestApis.context().instrumentedContext().getPackageManager() 337 .queryIntentActivities(intent, MATCH_DEFAULT_ONLY) 338 .stream().filter(ResolveInfo::isCrossProfileIntentForwarderActivity) 339 .collect(Collectors.toList()); 340 341 assertWithMessage("Failed to get intent forwarder component") 342 .that(result.size()).isEqualTo(1); 343 344 return result.get(0); 345 } 346 setSharingIntoProfileEnabled(boolean enabled)347 private void setSharingIntoProfileEnabled(boolean enabled) { 348 RemoteDpc remoteDpc = profileOwner(sDeviceState, WORK_PROFILE); 349 IntentFilter filter = new IntentFilter(ACTION_DATA_SHARING_RESTRICTION_APPLIED); 350 Context remoteCtx = TestApis.context().androidContextAsUser(remoteDpc.user()); 351 try (BlockingBroadcastReceiver receiver = 352 BlockingBroadcastReceiver.create(remoteCtx, filter).register()) { 353 if (enabled) { 354 remoteDpc.devicePolicyManager().clearUserRestriction( 355 remoteDpc.componentName(), DISALLOW_SHARE_INTO_MANAGED_PROFILE); 356 } else { 357 remoteDpc.devicePolicyManager().addUserRestriction( 358 remoteDpc.componentName(), DISALLOW_SHARE_INTO_MANAGED_PROFILE); 359 } 360 } 361 } 362 assertCrossProfileIntentsResolvability( Intent[] intents, ResolveInfo expectedForwarder, boolean expectForwardable)363 private void assertCrossProfileIntentsResolvability( 364 Intent[] intents, ResolveInfo expectedForwarder, boolean expectForwardable) { 365 for (Intent intent : intents) { 366 List<ResolveInfo> resolveInfoList = TestApis.context().instrumentedContext() 367 .getPackageManager().queryIntentActivities(intent, MATCH_DEFAULT_ONLY); 368 if (expectForwardable) { 369 assertWithMessage(String.format( 370 "Expect %s to be forwardable, but resolve list does not contain expected " 371 + "intent forwarder %s", intent, expectedForwarder)) 372 .that(containsResolveInfo(resolveInfoList, expectedForwarder)).isTrue(); 373 } else { 374 assertWithMessage(String.format( 375 "Expect %s not to be forwardable, but resolve list contains intent " 376 + "forwarder %s", intent, expectedForwarder)) 377 .that(containsResolveInfo(resolveInfoList, expectedForwarder)).isFalse(); 378 } 379 } 380 } 381 containsResolveInfo(List<ResolveInfo> list, ResolveInfo info)382 private boolean containsResolveInfo(List<ResolveInfo> list, ResolveInfo info) { 383 for (ResolveInfo entry : list) { 384 if (entry.activityInfo.packageName.equals(info.activityInfo.packageName) 385 && entry.activityInfo.name.equals(info.activityInfo.name)) { 386 return true; 387 } 388 } 389 return false; 390 } 391 } 392