1 package org.robolectric.shadows; 2 3 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; 4 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; 5 import static android.os.Build.VERSION_CODES.P; 6 import static android.os.Build.VERSION_CODES.Q; 7 import static android.os.Build.VERSION_CODES.R; 8 import static com.google.common.base.Preconditions.checkNotNull; 9 import static org.robolectric.util.reflector.Reflector.reflector; 10 11 import android.Manifest.permission; 12 import android.annotation.RequiresPermission; 13 import android.annotation.SystemApi; 14 import android.app.Activity; 15 import android.app.AppOpsManager; 16 import android.app.AppOpsManager.Mode; 17 import android.content.ComponentName; 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.pm.ActivityInfo; 21 import android.content.pm.CrossProfileApps; 22 import android.content.pm.PackageManager; 23 import android.graphics.drawable.ColorDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.os.Bundle; 26 import android.os.Process; 27 import android.os.UserHandle; 28 import android.provider.Settings; 29 import android.text.TextUtils; 30 import com.google.common.collect.ImmutableList; 31 import com.google.common.collect.Iterables; 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.LinkedHashSet; 35 import java.util.List; 36 import java.util.Objects; 37 import java.util.Set; 38 import javax.annotation.Nullable; 39 import org.robolectric.RuntimeEnvironment; 40 import org.robolectric.annotation.Implementation; 41 import org.robolectric.annotation.Implements; 42 import org.robolectric.annotation.RealObject; 43 import org.robolectric.annotation.Resetter; 44 import org.robolectric.util.reflector.Accessor; 45 import org.robolectric.util.reflector.ForType; 46 47 /** Robolectric implementation of {@link CrossProfileApps}. */ 48 @Implements(value = CrossProfileApps.class, minSdk = P) 49 public class ShadowCrossProfileApps { 50 51 @RealObject private CrossProfileApps realObject; 52 53 private static final Set<UserHandle> targetUserProfiles = new LinkedHashSet<>(); 54 private static final List<StartedMainActivity> startedMainActivities = new ArrayList<>(); 55 private static final List<StartedActivity> startedActivities = 56 Collections.synchronizedList(new ArrayList<>()); 57 58 // Whether the current application has the interact across profile AppOps. 59 private static volatile int canInteractAcrossProfileAppOps = AppOpsManager.MODE_ERRORED; 60 61 // Whether the current application has requested the interact across profile permission. 62 private static volatile boolean hasRequestedInteractAcrossProfiles = false; 63 64 @Resetter reset()65 public static void reset() { 66 targetUserProfiles.clear(); 67 startedMainActivities.clear(); 68 startedActivities.clear(); 69 canInteractAcrossProfileAppOps = AppOpsManager.MODE_ERRORED; 70 hasRequestedInteractAcrossProfiles = false; 71 } 72 73 /** 74 * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls 75 * to {@link #addTargetUserProfile(UserHandle)}. 76 */ 77 @Implementation getTargetUserProfiles()78 protected List<UserHandle> getTargetUserProfiles() { 79 return ImmutableList.copyOf(targetUserProfiles); 80 } 81 82 /** 83 * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to 84 * always be the same for a particular user and to be distinct between users. 85 */ 86 @Implementation getProfileSwitchingIconDrawable(UserHandle userHandle)87 protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) { 88 verifyCanAccessUser(userHandle); 89 return new ColorDrawable(userHandle.getIdentifier()); 90 } 91 92 /** 93 * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is 94 * guaranteed to always be the same for a particular user and to be distinct between users. 95 */ 96 @Implementation getProfileSwitchingLabel(UserHandle userHandle)97 protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) { 98 verifyCanAccessUser(userHandle); 99 return "Switch to " + userHandle; 100 } 101 102 /** 103 * Simulates starting the main activity specified in the specified profile, performing the same 104 * security checks done by the real {@link CrossProfileApps}. 105 * 106 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. 107 */ 108 @Implementation startMainActivity(ComponentName componentName, UserHandle targetUser)109 protected void startMainActivity(ComponentName componentName, UserHandle targetUser) { 110 verifyCanAccessUser(targetUser); 111 verifyActivityInManifest(componentName, /* requireMainActivity= */ true); 112 startedMainActivities.add(new StartedMainActivity(componentName, targetUser)); 113 startedActivities.add(new StartedActivity(componentName, targetUser)); 114 } 115 116 /** 117 * Simulates starting the activity specified in the specified profile, performing the same 118 * security checks done by the real {@link CrossProfileApps}. 119 * 120 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. 121 */ 122 @Implementation(minSdk = Q) 123 @SystemApi 124 @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) startActivity(ComponentName componentName, UserHandle targetUser)125 protected void startActivity(ComponentName componentName, UserHandle targetUser) { 126 verifyCanAccessUser(targetUser); 127 verifyActivityInManifest(componentName, /* requireMainActivity= */ false); 128 verifyHasInteractAcrossProfilesPermission(); 129 startedActivities.add(new StartedActivity(componentName, targetUser)); 130 } 131 132 /** 133 * Simulates starting the activity specified in the specified profile, performing the same 134 * security checks done by the real {@link CrossProfileApps}. 135 * 136 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. 137 */ 138 @Implementation(minSdk = R) 139 @SystemApi 140 @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) startActivity(Intent intent, UserHandle targetUser, @Nullable Activity activity)141 protected void startActivity(Intent intent, UserHandle targetUser, @Nullable Activity activity) { 142 startActivity(intent, targetUser, activity, /* options= */ null); 143 } 144 145 /** 146 * Simulates starting the activity specified in the specified profile, performing the same 147 * security checks done by the real {@link CrossProfileApps}. 148 * 149 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. 150 */ 151 @Implementation(minSdk = R) 152 @SystemApi 153 @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) startActivity( Intent intent, UserHandle targetUser, @Nullable Activity activity, @Nullable Bundle options)154 protected void startActivity( 155 Intent intent, UserHandle targetUser, @Nullable Activity activity, @Nullable Bundle options) { 156 ComponentName componentName = intent.getComponent(); 157 if (componentName == null) { 158 throw new IllegalArgumentException("Must set ComponentName on Intent"); 159 } 160 verifyCanAccessUser(targetUser); 161 verifyHasInteractAcrossProfilesPermission(); 162 startedActivities.add( 163 new StartedActivity(componentName, targetUser, intent, activity, options)); 164 } 165 166 /** Adds {@code userHandle} to the list of accessible handles. */ addTargetUserProfile(UserHandle userHandle)167 public void addTargetUserProfile(UserHandle userHandle) { 168 if (userHandle.equals(Process.myUserHandle())) { 169 throw new IllegalArgumentException("Cannot target current user"); 170 } 171 targetUserProfiles.add(userHandle); 172 } 173 174 /** Removes {@code userHandle} from the list of accessible handles, if present. */ removeTargetUserProfile(UserHandle userHandle)175 public void removeTargetUserProfile(UserHandle userHandle) { 176 if (userHandle.equals(Process.myUserHandle())) { 177 throw new IllegalArgumentException("Cannot target current user"); 178 } 179 targetUserProfiles.remove(userHandle); 180 } 181 182 /** Clears the list of accessible handles. */ clearTargetUserProfiles()183 public void clearTargetUserProfiles() { 184 targetUserProfiles.clear(); 185 } 186 187 /** 188 * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 189 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link 190 * StartedMainActivity}. 191 * 192 * @deprecated Use {@link #peekNextStartedActivity()} instead. 193 */ 194 @Nullable 195 @Deprecated peekNextStartedMainActivity()196 public StartedMainActivity peekNextStartedMainActivity() { 197 if (startedMainActivities.isEmpty()) { 198 return null; 199 } else { 200 return Iterables.getLast(startedMainActivities); 201 } 202 } 203 204 /** 205 * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 206 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link 207 * CrossProfileApps#startActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, 208 * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}, wrapped 209 * in {@link StartedActivity}. 210 */ 211 @Nullable peekNextStartedActivity()212 public StartedActivity peekNextStartedActivity() { 213 if (startedActivities.isEmpty()) { 214 return null; 215 } else { 216 return Iterables.getLast(startedActivities); 217 } 218 } 219 220 /** 221 * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 222 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link 223 * CrossProfileApps#startActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, 224 * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}, and 225 * returns it wrapped in {@link StartedActivity}. 226 */ 227 @Nullable getNextStartedActivity()228 public StartedActivity getNextStartedActivity() { 229 if (startedActivities.isEmpty()) { 230 return null; 231 } else { 232 return startedActivities.remove(startedActivities.size() - 1); 233 } 234 } 235 236 /** 237 * Clears all records of {@link StartedActivity}s from calls to {@link 238 * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link 239 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, 240 * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}. 241 */ clearNextStartedActivities()242 public void clearNextStartedActivities() { 243 startedActivities.clear(); 244 } 245 246 @Implementation(minSdk = P) verifyCanAccessUser(UserHandle userHandle)247 protected void verifyCanAccessUser(UserHandle userHandle) { 248 if (!targetUserProfiles.contains(userHandle)) { 249 throw new SecurityException( 250 "Not allowed to access " 251 + userHandle 252 + " (did you forget to call addTargetUserProfile?)"); 253 } 254 } 255 256 /** Ensure the current package has the permission to interact across profiles. */ verifyHasInteractAcrossProfilesPermission()257 protected void verifyHasInteractAcrossProfilesPermission() { 258 if (RuntimeEnvironment.getApiLevel() >= R) { 259 if (!canInteractAcrossProfiles()) { 260 throw new SecurityException("Attempt to launch activity without required the permissions."); 261 } 262 return; 263 } 264 if (getContext().checkSelfPermission(permission.INTERACT_ACROSS_PROFILES) 265 != PackageManager.PERMISSION_GRANTED) { 266 throw new SecurityException( 267 "Attempt to launch activity without required " 268 + permission.INTERACT_ACROSS_PROFILES 269 + " permission"); 270 } 271 } 272 273 /** 274 * Ensures that {@code component} is present in the manifest as an exported and enabled activity. 275 * This check and the error thrown are the same as the check done by the real {@link 276 * CrossProfileApps}. 277 * 278 * <p>If {@code requireMainActivity} is true, then this also asserts that the activity is a 279 * launcher activity. 280 */ verifyActivityInManifest(ComponentName component, boolean requireMainActivity)281 private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) { 282 Intent launchIntent = new Intent(); 283 if (requireMainActivity) { 284 launchIntent 285 .setAction(Intent.ACTION_MAIN) 286 .addCategory(Intent.CATEGORY_LAUNCHER) 287 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 288 .setPackage(component.getPackageName()); 289 } else { 290 launchIntent.setComponent(component); 291 } 292 293 boolean existsMatchingActivity = 294 Iterables.any( 295 getContext() 296 .getPackageManager() 297 .queryIntentActivities( 298 launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE), 299 resolveInfo -> { 300 ActivityInfo activityInfo = resolveInfo.activityInfo; 301 return TextUtils.equals(activityInfo.packageName, component.getPackageName()) 302 && TextUtils.equals(activityInfo.name, component.getClassName()) 303 && activityInfo.exported; 304 }); 305 if (!existsMatchingActivity) { 306 throw new SecurityException( 307 "Attempt to launch activity without " 308 + " category Intent.CATEGORY_LAUNCHER or activity is not exported" 309 + component); 310 } 311 } 312 313 /** 314 * Checks if the current application can interact across profile. 315 * 316 * <p>This checks for the existence of a target user profile, and if the app has 317 * INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL or INTERACT_ACROSS_PROFILES permission. 318 * Importantly, the {@code interact_across_profiles} AppOps is only checked through the value set 319 * by {@link #setInteractAcrossProfilesAppOp(int)} or by {@link 320 * #setInteractAcrossProfilesAppOp(String, int)}, if the application has the needed permissions. 321 */ 322 @Implementation(minSdk = R) canInteractAcrossProfiles()323 protected boolean canInteractAcrossProfiles() { 324 if (getTargetUserProfiles().isEmpty()) { 325 return false; 326 } 327 return hasPermission(permission.INTERACT_ACROSS_USERS_FULL) 328 || hasPermission(permission.INTERACT_ACROSS_PROFILES) 329 || hasPermission(permission.INTERACT_ACROSS_USERS) 330 || canInteractAcrossProfileAppOps == AppOpsManager.MODE_ALLOWED; 331 } 332 333 /** 334 * Returns whether the calling package can request to navigate the user to the relevant settings 335 * page to request user consent to interact across profiles. 336 * 337 * <p>This checks for the existence of a target user profile, and if the app has requested the 338 * INTERACT_ACROSS_PROFILES permission in its manifest. As Robolectric doesn't interpret the 339 * permissions in the manifest, whether or not the app has requested this is defined by {@link 340 * #setHasRequestedInteractAcrossProfiles(boolean)}. 341 * 342 * <p>If the test uses {@link #setInteractAcrossProfilesAppOp(int)}, it implies the app has 343 * requested the AppOps. 344 * 345 * <p>In short, compared to {@link #canInteractAcrossProfiles()}, it doesn't check if the user has 346 * the AppOps or not. 347 */ 348 @Implementation(minSdk = R) canRequestInteractAcrossProfiles()349 protected boolean canRequestInteractAcrossProfiles() { 350 if (getTargetUserProfiles().isEmpty()) { 351 return false; 352 } 353 return hasRequestedInteractAcrossProfiles; 354 } 355 356 /** 357 * Sets whether or not the current application has requested the interact across profile 358 * permission in its manifest. 359 */ setHasRequestedInteractAcrossProfiles(boolean value)360 public void setHasRequestedInteractAcrossProfiles(boolean value) { 361 hasRequestedInteractAcrossProfiles = value; 362 } 363 364 /** 365 * Returns an intent with the same action as the one returned by system when requesting the same. 366 * 367 * <p>Note: Currently, the system will also set the package name as a URI, but as this is not 368 * specified in the main doc, we shouldn't rely on it. The purpose is only to make an intent can 369 * that be recognised in a test. 370 * 371 * @throws SecurityException if this is called while {@link 372 * CrossProfileApps#canRequestInteractAcrossProfiles()} returns false. 373 */ 374 @Implementation(minSdk = R) createRequestInteractAcrossProfilesIntent()375 protected Intent createRequestInteractAcrossProfilesIntent() { 376 if (!canRequestInteractAcrossProfiles()) { 377 throw new SecurityException( 378 "The calling package can not request to interact across profiles."); 379 } 380 return new Intent(Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS); 381 } 382 383 /** 384 * Checks whether the given intent will redirect toward the screen allowing the user to change the 385 * interact across profiles AppOps. 386 */ isRequestInteractAcrossProfilesIntent(Intent intent)387 public boolean isRequestInteractAcrossProfilesIntent(Intent intent) { 388 return Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction()); 389 } 390 hasPermission(String permission)391 private boolean hasPermission(String permission) { 392 return getContext() 393 .getPackageManager() 394 .checkPermission(permission, getContext().getPackageName()) 395 == PackageManager.PERMISSION_GRANTED; 396 } 397 getContext()398 protected Context getContext() { 399 return reflector(CrossProfileAppsReflector.class, realObject).getContext(); 400 } 401 402 /** 403 * Forces the {code interact_across_profile} AppOps for the current package. 404 * 405 * <p>If the value changes, this also sends the {@link 406 * CrossProfileApps#ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED} broadcast. 407 */ setInteractAcrossProfilesAppOp(@ode int newMode)408 public void setInteractAcrossProfilesAppOp(@Mode int newMode) { 409 hasRequestedInteractAcrossProfiles = true; 410 if (canInteractAcrossProfileAppOps != newMode) { 411 canInteractAcrossProfileAppOps = newMode; 412 getContext() 413 .sendBroadcast(new Intent(CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED)); 414 } 415 } 416 417 /** 418 * Checks permission and changes the AppOps value stored in {@link ShadowCrossProfileApps}. 419 * 420 * <p>In the real implementation, if there is no target profile, the AppOps is not changed, as it 421 * will be set during the profile's initialization. The real implementation also really changes 422 * the AppOps for all profiles the package is installed in. 423 */ 424 @Implementation(minSdk = R) setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode)425 protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { 426 if (!hasPermission(permission.INTERACT_ACROSS_USERS) 427 || !hasPermission(permission.CONFIGURE_INTERACT_ACROSS_PROFILES)) { 428 throw new SecurityException( 429 "Requires INTERACT_ACROSS_USERS and CONFIGURE_INTERACT_ACROSS_PROFILES permission"); 430 } 431 setInteractAcrossProfilesAppOp(newMode); 432 } 433 434 /** 435 * Unlike the real system, we will assume a package can always configure its own cross profile 436 * interaction. 437 */ 438 @Implementation(minSdk = R) canConfigureInteractAcrossProfiles(String packageName)439 protected boolean canConfigureInteractAcrossProfiles(String packageName) { 440 return getContext().getPackageName().equals(packageName); 441 } 442 443 /** 444 * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, 445 * UserHandle)}. 446 * 447 * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead. 448 */ 449 @Deprecated 450 public static class StartedMainActivity { 451 452 private final ComponentName componentName; 453 private final UserHandle userHandle; 454 StartedMainActivity(ComponentName componentName, UserHandle userHandle)455 public StartedMainActivity(ComponentName componentName, UserHandle userHandle) { 456 this.componentName = checkNotNull(componentName); 457 this.userHandle = checkNotNull(userHandle); 458 } 459 getComponentName()460 public ComponentName getComponentName() { 461 return componentName; 462 } 463 getUserHandle()464 public UserHandle getUserHandle() { 465 return userHandle; 466 } 467 468 @Override equals(Object o)469 public boolean equals(Object o) { 470 if (this == o) { 471 return true; 472 } 473 if (o == null || getClass() != o.getClass()) { 474 return false; 475 } 476 StartedMainActivity that = (StartedMainActivity) o; 477 return Objects.equals(componentName, that.componentName) 478 && Objects.equals(userHandle, that.userHandle); 479 } 480 481 @Override hashCode()482 public int hashCode() { 483 return Objects.hash(componentName, userHandle); 484 } 485 } 486 487 /** 488 * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, 489 * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}, {@link 490 * #startActivity(Intent, UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, 491 * Activity, Bundle)}. 492 * 493 * <p>Note: {@link #equals} and {@link #hashCode} are only defined for the {@link ComponentName} 494 * and {@link UserHandle}. 495 */ 496 public static final class StartedActivity { 497 498 private final ComponentName componentName; 499 private final UserHandle userHandle; 500 @Nullable private final Intent intent; 501 @Nullable private final Activity activity; 502 @Nullable private final Bundle options; 503 StartedActivity(ComponentName componentName, UserHandle userHandle)504 public StartedActivity(ComponentName componentName, UserHandle userHandle) { 505 this( 506 componentName, userHandle, /* intent= */ null, /* activity= */ null, /* options= */ null); 507 } 508 StartedActivity( ComponentName componentName, UserHandle userHandle, @Nullable Intent intent, @Nullable Activity activity, @Nullable Bundle options)509 public StartedActivity( 510 ComponentName componentName, 511 UserHandle userHandle, 512 @Nullable Intent intent, 513 @Nullable Activity activity, 514 @Nullable Bundle options) { 515 this.componentName = checkNotNull(componentName); 516 this.userHandle = checkNotNull(userHandle); 517 this.intent = intent; 518 this.activity = activity; 519 this.options = options; 520 } 521 getComponentName()522 public ComponentName getComponentName() { 523 return componentName; 524 } 525 getUserHandle()526 public UserHandle getUserHandle() { 527 return userHandle; 528 } 529 530 @Nullable getIntent()531 public Intent getIntent() { 532 return intent; 533 } 534 535 @Nullable getOptions()536 public Bundle getOptions() { 537 return options; 538 } 539 540 @Nullable getActivity()541 public Activity getActivity() { 542 return activity; 543 } 544 545 @Override equals(Object o)546 public boolean equals(Object o) { 547 if (this == o) { 548 return true; 549 } 550 if (o == null || getClass() != o.getClass()) { 551 return false; 552 } 553 StartedActivity that = (StartedActivity) o; 554 return Objects.equals(componentName, that.componentName) 555 && Objects.equals(userHandle, that.userHandle); 556 } 557 558 @Override hashCode()559 public int hashCode() { 560 return Objects.hash(componentName, userHandle); 561 } 562 } 563 564 @ForType(CrossProfileApps.class) 565 interface CrossProfileAppsReflector { 566 @Accessor("mContext") getContext()567 Context getContext(); 568 } 569 } 570