• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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