All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowCrossProfileApps Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static com.google.common.base.Preconditions.checkNotNull;

import android.Manifest.permission;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.AppOpsManager.Mode;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.CrossProfileApps;
import android.content.pm.ICrossProfileApps;
import android.content.pm.PackageManager;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Robolectric implementation of {@link CrossProfileApps}. */
@Implements(value = CrossProfileApps.class, minSdk = P)
public class ShadowCrossProfileApps {

  private final Set targetUserProfiles = new LinkedHashSet<>();
  private final List startedMainActivities = new ArrayList<>();
  private final List startedActivities =
      Collections.synchronizedList(new ArrayList<>());

  private Context context;
  private PackageManager packageManager;
  // Whether the current application has the interact across profile AppOps.
  private volatile int canInteractAcrossProfileAppOps = AppOpsManager.MODE_ERRORED;

  // Whether the current application has requested the interact across profile permission.
  private volatile boolean hasRequestedInteractAcrossProfiles = false;

  @Implementation
  protected void __constructor__(Context context, ICrossProfileApps service) {
    this.context = context;
    this.packageManager = context.getPackageManager();
  }

  /**
   * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls
   * to {@link #addTargetUserProfile(UserHandle)}.
   */
  @Implementation
  protected List getTargetUserProfiles() {
    return ImmutableList.copyOf(targetUserProfiles);
  }

  /**
   * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to
   * always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return new ColorDrawable(userHandle.getIdentifier());
  }

  /**
   * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is
   * guaranteed to always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return "Switch to " + userHandle;
  }

  /**
   * Simulates starting the main activity specified in the specified profile, performing the same
   * security checks done by the real {@link CrossProfileApps}.
   *
   * 

The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. */ @Implementation protected void startMainActivity(ComponentName componentName, UserHandle targetUser) { verifyCanAccessUser(targetUser); verifyActivityInManifest(componentName, /* requireMainActivity= */ true); startedMainActivities.add(new StartedMainActivity(componentName, targetUser)); startedActivities.add(new StartedActivity(componentName, targetUser)); } /** * Simulates starting the activity specified in the specified profile, performing the same * security checks done by the real {@link CrossProfileApps}. * *

The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. */ @Implementation(minSdk = Q) @SystemApi @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) protected void startActivity(ComponentName componentName, UserHandle targetUser) { verifyCanAccessUser(targetUser); verifyActivityInManifest(componentName, /* requireMainActivity= */ false); verifyHasInteractAcrossProfilesPermission(); startedActivities.add(new StartedActivity(componentName, targetUser)); } /** * Simulates starting the activity specified in the specified profile, performing the same * security checks done by the real {@link CrossProfileApps}. * *

The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. */ @Implementation(minSdk = R) @SystemApi @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) protected void startActivity(Intent intent, UserHandle targetUser, @Nullable Activity activity) { startActivity(intent, targetUser, activity, /* options= */ null); } /** * Simulates starting the activity specified in the specified profile, performing the same * security checks done by the real {@link CrossProfileApps}. * *

The most recent main activity started can be queried by {@link #peekNextStartedActivity()}. */ @Implementation(minSdk = R) @SystemApi @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) protected void startActivity( Intent intent, UserHandle targetUser, @Nullable Activity activity, @Nullable Bundle options) { ComponentName componentName = intent.getComponent(); if (componentName == null) { throw new IllegalArgumentException("Must set ComponentName on Intent"); } verifyCanAccessUser(targetUser); verifyHasInteractAcrossProfilesPermission(); startedActivities.add( new StartedActivity(componentName, targetUser, intent, activity, options)); } /** Adds {@code userHandle} to the list of accessible handles. */ public void addTargetUserProfile(UserHandle userHandle) { if (userHandle.equals(Process.myUserHandle())) { throw new IllegalArgumentException("Cannot target current user"); } targetUserProfiles.add(userHandle); } /** Removes {@code userHandle} from the list of accessible handles, if present. */ public void removeTargetUserProfile(UserHandle userHandle) { if (userHandle.equals(Process.myUserHandle())) { throw new IllegalArgumentException("Cannot target current user"); } targetUserProfiles.remove(userHandle); } /** Clears the list of accessible handles. */ public void clearTargetUserProfiles() { targetUserProfiles.clear(); } /** * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link * StartedMainActivity}. * * @deprecated Use {@link #peekNextStartedActivity()} instead. */ @Nullable @Deprecated public StartedMainActivity peekNextStartedMainActivity() { if (startedMainActivities.isEmpty()) { return null; } else { return Iterables.getLast(startedMainActivities); } } /** * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}, wrapped * in {@link StartedActivity}. */ @Nullable public StartedActivity peekNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return Iterables.getLast(startedActivities); } } /** * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}, and * returns it wrapped in {@link StartedActivity}. */ @Nullable public StartedActivity getNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return startedActivities.remove(startedActivities.size() - 1); } } /** * Clears all records of {@link StartedActivity}s from calls to {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, {@link #startActivity(Intent, * UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, Activity, Bundle)}. */ public void clearNextStartedActivities() { startedActivities.clear(); } @Implementation(minSdk = P) protected void verifyCanAccessUser(UserHandle userHandle) { if (!targetUserProfiles.contains(userHandle)) { throw new SecurityException( "Not allowed to access " + userHandle + " (did you forget to call addTargetUserProfile?)"); } } /** * Ensure the current package has the permission to interact across profiles. */ protected void verifyHasInteractAcrossProfilesPermission() { if (RuntimeEnvironment.getApiLevel() >= R) { if (!canInteractAcrossProfiles()) { throw new SecurityException("Attempt to launch activity without required the permissions."); } return; } if (context.checkSelfPermission(permission.INTERACT_ACROSS_PROFILES) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException( "Attempt to launch activity without required " + permission.INTERACT_ACROSS_PROFILES + " permission"); } } /** * Ensures that {@code component} is present in the manifest as an exported and enabled activity. * This check and the error thrown are the same as the check done by the real {@link * CrossProfileApps}. * *

If {@code requireMainActivity} is true, then this also asserts that the activity is a * launcher activity. */ private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) { Intent launchIntent = new Intent(); if (requireMainActivity) { launchIntent .setAction(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) .setPackage(component.getPackageName()); } else { launchIntent.setComponent(component); } boolean existsMatchingActivity = Iterables.any( packageManager.queryIntentActivities( launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE), resolveInfo -> { ActivityInfo activityInfo = resolveInfo.activityInfo; return TextUtils.equals(activityInfo.packageName, component.getPackageName()) && TextUtils.equals(activityInfo.name, component.getClassName()) && activityInfo.exported; }); if (!existsMatchingActivity) { throw new SecurityException( "Attempt to launch activity without " + " category Intent.CATEGORY_LAUNCHER or activity is not exported" + component); } } /** * Checks if the current application can interact across profile. * *

This checks for the existence of a target user profile, and if the app has * INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL or INTERACT_ACROSS_PROFILES permission. * Importantly, the {@code interact_across_profiles} AppOps is only checked through the value set * by {@link #setInteractAcrossProfilesAppOp(int)} or by {@link * #setInteractAcrossProfilesAppOp(String, int)}, if the application has the needed permissions. */ @Implementation(minSdk = R) protected boolean canInteractAcrossProfiles() { if (getTargetUserProfiles().isEmpty()) { return false; } return hasPermission(permission.INTERACT_ACROSS_USERS_FULL) || hasPermission(permission.INTERACT_ACROSS_PROFILES) || hasPermission(permission.INTERACT_ACROSS_USERS) || canInteractAcrossProfileAppOps == AppOpsManager.MODE_ALLOWED; } /** * Returns whether the calling package can request to navigate the user to the relevant settings * page to request user consent to interact across profiles. * *

This checks for the existence of a target user profile, and if the app has requested the * INTERACT_ACROSS_PROFILES permission in its manifest. As Robolectric doesn't interpret the * permissions in the manifest, whether or not the app has requested this is defined by {@link * #setHasRequestedInteractAcrossProfiles(boolean)}. * *

If the test uses {@link #setInteractAcrossProfilesAppOp(int)}, it implies the app has * requested the AppOps. * *

In short, compared to {@link #canInteractAcrossProfiles()}, it doesn't check if the user has * the AppOps or not. */ @Implementation(minSdk = R) protected boolean canRequestInteractAcrossProfiles() { if (getTargetUserProfiles().isEmpty()) { return false; } return hasRequestedInteractAcrossProfiles; } /** * Sets whether or not the current application has requested the interact across profile * permission in its manifest. */ public void setHasRequestedInteractAcrossProfiles(boolean value) { hasRequestedInteractAcrossProfiles = value; } /** * Returns an intent with the same action as the one returned by system when requesting the same. * *

Note: Currently, the system will also set the package name as a URI, but as this is not * specified in the main doc, we shouldn't rely on it. The purpose is only to make an intent can * that be recognised in a test. * * @throws SecurityException if this is called while {@link * CrossProfileApps#canRequestInteractAcrossProfiles()} returns false. */ @Implementation(minSdk = R) protected Intent createRequestInteractAcrossProfilesIntent() { if (!canRequestInteractAcrossProfiles()) { throw new SecurityException( "The calling package can not request to interact across profiles."); } return new Intent(Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS); } /** * Checks whether the given intent will redirect toward the screen allowing the user to change the * interact across profiles AppOps. */ public boolean isRequestInteractAcrossProfilesIntent(Intent intent) { return Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction()); } private boolean hasPermission(String permission) { return context.getPackageManager().checkPermission(permission, context.getPackageName()) == PackageManager.PERMISSION_GRANTED; } /** * Forces the {code interact_across_profile} AppOps for the current package. * *

If the value changes, this also sends the {@link * CrossProfileApps#ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED} broadcast. */ public void setInteractAcrossProfilesAppOp(@Mode int newMode) { hasRequestedInteractAcrossProfiles = true; if (canInteractAcrossProfileAppOps != newMode) { canInteractAcrossProfileAppOps = newMode; context.sendBroadcast( new Intent(CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED)); } } /** * Checks permission and changes the AppOps value stored in {@link ShadowCrossProfileApps}. * *

In the real implementation, if there is no target profile, the AppOps is not changed, as it * will be set during the profile's initialization. The real implementation also really changes * the AppOps for all profiles the package is installed in. */ @Implementation(minSdk = R) protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { if (!hasPermission(permission.INTERACT_ACROSS_USERS) || !hasPermission(permission.CONFIGURE_INTERACT_ACROSS_PROFILES)) { throw new SecurityException( "Requires INTERACT_ACROSS_USERS and CONFIGURE_INTERACT_ACROSS_PROFILES permission"); } setInteractAcrossProfilesAppOp(newMode); } /** * Unlike the real system, we will assume a package can always configure its own cross profile * interaction. */ @Implementation(minSdk = R) protected boolean canConfigureInteractAcrossProfiles(String packageName) { return context.getPackageName().equals(packageName); } /** * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, * UserHandle)}. * * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead. */ @Deprecated public static class StartedMainActivity { private final ComponentName componentName; private final UserHandle userHandle; public StartedMainActivity(ComponentName componentName, UserHandle userHandle) { this.componentName = checkNotNull(componentName); this.userHandle = checkNotNull(userHandle); } public ComponentName getComponentName() { return componentName; } public UserHandle getUserHandle() { return userHandle; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StartedMainActivity that = (StartedMainActivity) o; return Objects.equals(componentName, that.componentName) && Objects.equals(userHandle, that.userHandle); } @Override public int hashCode() { return Objects.hash(componentName, userHandle); } } /** * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}, {@link * #startActivity(Intent, UserHandle, Activity)}, {@link #startActivity(Intent, UserHandle, * Activity, Bundle)}. * *

Note: {@link #equals} and {@link #hashCode} are only defined for the {@link ComponentName} * and {@link UserHandle}. */ public static final class StartedActivity { private final ComponentName componentName; private final UserHandle userHandle; @Nullable private final Intent intent; @Nullable private final Activity activity; @Nullable private final Bundle options; public StartedActivity(ComponentName componentName, UserHandle userHandle) { this( componentName, userHandle, /* intent= */ null, /* activity= */ null, /* options= */ null); } public StartedActivity( ComponentName componentName, UserHandle userHandle, @Nullable Intent intent, @Nullable Activity activity, @Nullable Bundle options) { this.componentName = checkNotNull(componentName); this.userHandle = checkNotNull(userHandle); this.intent = intent; this.activity = activity; this.options = options; } public ComponentName getComponentName() { return componentName; } public UserHandle getUserHandle() { return userHandle; } @Nullable public Intent getIntent() { return intent; } @Nullable public Bundle getOptions() { return options; } @Nullable public Activity getActivity() { return activity; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StartedActivity that = (StartedActivity) o; return Objects.equals(componentName, that.componentName) && Objects.equals(userHandle, that.userHandle); } @Override public int hashCode() { return Objects.hash(componentName, userHandle); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy