blob: 99e6d91a61ae0b91873b6870d1e287c34805f5f1 [file] [log] [blame]
/*
* Copyright (C) 2017 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.content.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.Activity;
import android.app.AppOpsManager.Mode;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import com.android.internal.R;
import com.android.internal.util.UserIcons;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Class for handling cross profile operations. Apps can use this class to interact with its
* instance in any profile that is in {@link #getTargetUserProfiles()}. For example, app can
* use this class to start its main activity in managed profile.
*/
public class CrossProfileApps {
/**
* Broadcast signalling that the receiving app's permission to interact across profiles has
* changed. This includes the user, admin, or OEM changing their consent such that the
* permission for the app to interact across profiles has changed.
*
* <p>This broadcast is not sent when other circumstances result in a change to being able to
* interact across profiles in practice, such as the profile being turned off or removed, apps
* being uninstalled, etc. The methods {@link #canInteractAcrossProfiles()} and {@link
* #canRequestInteractAcrossProfiles()} can be used by apps prior to attempting to interact
* across profiles or attempting to request user consent to interact across profiles.
*
* <p>Apps that have set the {@code android:crossProfile} manifest attribute to {@code true}
* can receive this broadcast in manifest broadcast receivers. Otherwise, it can only be
* received by dynamically-registered broadcast receivers.
*/
public static final String ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED =
"android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED";
private final Context mContext;
private final ICrossProfileApps mService;
private final UserManager mUserManager;
private final Resources mResources;
/** @hide */
public CrossProfileApps(Context context, ICrossProfileApps service) {
mContext = context;
mService = service;
mUserManager = context.getSystemService(UserManager.class);
mResources = context.getResources();
}
/**
* Starts the specified main activity of the caller package in the specified profile.
*
* @param component The ComponentName of the activity to launch, it must be exported and has
* action {@link android.content.Intent#ACTION_MAIN}, category
* {@link android.content.Intent#CATEGORY_LAUNCHER}. Otherwise, SecurityException will
* be thrown.
* @param targetUser The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
*/
public void startMainActivity(@NonNull ComponentName component,
@NonNull UserHandle targetUser) {
try {
mService.startActivityAsUser(
mContext.getIApplicationThread(),
mContext.getPackageName(),
mContext.getAttributionTag(),
component,
targetUser.getIdentifier(),
true);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Starts the specified activity of the caller package in the specified profile.
*
* <p>The caller must have the {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES},
* {@code android.Manifest.permission#INTERACT_ACROSS_USERS}, or {@code
* android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. Both the caller and
* target user profiles must be in the same profile group. The target user must be a valid user
* returned from {@link #getTargetUserProfiles()}.
*
* @param intent The intent to launch. A component in the caller package must be specified.
* @param targetUser The {@link UserHandle} of the profile; must be one of the users returned by
* {@link #getTargetUserProfiles()} if different to the calling user, otherwise a
* {@link SecurityException} will be thrown.
* @param callingActivity The activity to start the new activity from for the purposes of
* deciding which task the new activity should belong to. If {@code null}, the activity
* will always be started in a new task.
*/
@RequiresPermission(anyOf = {
android.Manifest.permission.INTERACT_ACROSS_PROFILES,
android.Manifest.permission.INTERACT_ACROSS_USERS})
public void startActivity(
@NonNull Intent intent,
@NonNull UserHandle targetUser,
@Nullable Activity callingActivity) {
startActivity(intent, targetUser, callingActivity, /* options= */ null);
}
/**
* Starts the specified activity of the caller package in the specified profile.
*
* <p>The caller must have the {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES},
* {@code android.Manifest.permission#INTERACT_ACROSS_USERS}, or {@code
* android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. Both the caller and
* target user profiles must be in the same profile group. The target user must be a valid user
* returned from {@link #getTargetUserProfiles()}.
*
* @param intent The intent to launch. A component in the caller package must be specified.
* @param targetUser The {@link UserHandle} of the profile; must be one of the users returned by
* {@link #getTargetUserProfiles()} if different to the calling user, otherwise a
* {@link SecurityException} will be thrown.
* @param callingActivity The activity to start the new activity from for the purposes of
* deciding which task the new activity should belong to. If {@code null}, the activity
* will always be started in a new task.
* @param options The activity options or {@code null}. See {@link android.app.ActivityOptions}.
*/
@RequiresPermission(anyOf = {
android.Manifest.permission.INTERACT_ACROSS_PROFILES,
android.Manifest.permission.INTERACT_ACROSS_USERS})
public void startActivity(
@NonNull Intent intent,
@NonNull UserHandle targetUser,
@Nullable Activity callingActivity,
@Nullable Bundle options) {
try {
mService.startActivityAsUserByIntent(
mContext.getIApplicationThread(),
mContext.getPackageName(),
mContext.getAttributionTag(),
intent,
targetUser.getIdentifier(),
callingActivity != null ? callingActivity.getActivityToken() : null,
options);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Starts the specified activity of the caller package in the specified profile. Unlike
* {@link #startMainActivity}, this can start any activity of the caller package, not just
* the main activity.
* The caller must have the {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES}
* permission and both the caller and target user profiles must be in the same profile group.
*
* @param component The ComponentName of the activity to launch. It must be exported.
* @param targetUser The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES)
public void startActivity(@NonNull ComponentName component, @NonNull UserHandle targetUser) {
try {
mService.startActivityAsUser(mContext.getIApplicationThread(),
mContext.getPackageName(), mContext.getAttributionTag(), component,
targetUser.getIdentifier(), false);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Return a list of user profiles that that the caller can use when calling other APIs in this
* class.
* <p>
* A user profile would be considered as a valid target user profile, provided that:
* <ul>
* <li>It gets caller app installed</li>
* <li>It is not equal to the calling user</li>
* <li>It is in the same profile group of calling user profile</li>
* <li>It is enabled</li>
* </ul>
*
* @see UserManager#getUserProfiles()
*/
public @NonNull List<UserHandle> getTargetUserProfiles() {
try {
return mService.getTargetUserProfiles(mContext.getPackageName());
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Return a label that calling app can show to user for the semantic of profile switching --
* launching its own activity in specified user profile. For example, it may return
* "Switch to work" if the given user handle is the managed profile one.
*
* @param userHandle The UserHandle of the target profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
* @return a label that calling app can show user for the semantic of launching its own
* activity in the specified user profile.
*
* @see #startMainActivity(ComponentName, UserHandle)
*/
public @NonNull CharSequence getProfileSwitchingLabel(@NonNull UserHandle userHandle) {
verifyCanAccessUser(userHandle);
final int stringRes = mUserManager.isManagedProfile(userHandle.getIdentifier())
? R.string.managed_profile_label
: R.string.user_owner_label;
return mResources.getString(stringRes);
}
/**
* Return a drawable that calling app can show to user for the semantic of profile switching --
* launching its own activity in specified user profile. For example, it may return a briefcase
* icon if the given user handle is the managed profile one.
*
* @param userHandle The UserHandle of the target profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
* @return an icon that calling app can show user for the semantic of launching its own
* activity in specified user profile.
*
* @see #startMainActivity(ComponentName, UserHandle)
*/
public @NonNull Drawable getProfileSwitchingIconDrawable(@NonNull UserHandle userHandle) {
verifyCanAccessUser(userHandle);
final boolean isManagedProfile =
mUserManager.isManagedProfile(userHandle.getIdentifier());
if (isManagedProfile) {
return mResources.getDrawable(R.drawable.ic_corp_badge, null);
} else {
return UserIcons.getDefaultUserIcon(
mResources, UserHandle.USER_SYSTEM, true /* light */);
}
}
/**
* Returns whether the calling package can request to navigate the user to
* the relevant settings page to request user consent to interact across profiles.
*
* <p>If {@code true}, the navigation intent can be obtained via {@link
* #createRequestInteractAcrossProfilesIntent()}. The package can then listen to {@link
* #ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED} broadcasts.
*
* <p>Specifically, returns whether the following are all true:
* <ul>
* <li>{@code UserManager#getEnabledProfileIds(int)} ()} returns at least one other profile for
* the calling user.</li>
* <li>The calling app has requested
* {@code android.Manifest.permission.INTERACT_ACROSS_PROFILES} in its manifest.</li>
* </ul>
*
* <p>Note that in order for the user to be able to grant the consent, the requesting package
* must be whitelisted by the admin or the OEM and installed in the other profile. If this is
* not the case the user will be shown a message explaining why they can't grant the consent.
*
* <p>Note that user consent could already be granted if given a return value of {@code true}.
* The package's current ability to interact across profiles can be checked with {@link
* #canInteractAcrossProfiles()}.
*
* @return true if the calling package can request to interact across profiles.
*/
public boolean canRequestInteractAcrossProfiles() {
try {
return mService.canRequestInteractAcrossProfiles(mContext.getPackageName());
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Returns whether the calling package can interact across profiles.
* <p>Specifically, returns whether the following are all true:
* <ul>
* <li>{@link #getTargetUserProfiles()} returns a non-empty list for the calling user.</li>
* <li>The user has previously consented to cross-profile communication for the calling
* package.</li>
* <li>The calling package has either been whitelisted by default by the OEM or has been
* explicitly whitelisted by the admin via
* {@link android.app.admin.DevicePolicyManager#setCrossProfilePackages(ComponentName, Set)}.
* </li>
* </ul>
*
* <p>If {@code false}, the package's current ability to request user consent to interact across
* profiles can be checked with {@link #canRequestInteractAcrossProfiles()}. If {@code true},
* user consent can be obtained via {@link #createRequestInteractAcrossProfilesIntent()}. The
* package can then listen to {@link #ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED} broadcasts.
*
* @return true if the calling package can interact across profiles.
* @throws SecurityException if {@code mContext.getPackageName()} does not belong to the
* calling UID.
*/
public boolean canInteractAcrossProfiles() {
try {
return mService.canInteractAcrossProfiles(mContext.getPackageName());
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Returns an {@link Intent} to open the settings page that allows the user to decide whether
* the calling app can interact across profiles.
*
* <p>The method {@link #canRequestInteractAcrossProfiles()} must be returning {@code true}.
*
* <p>Note that the user may already have given consent and the app may already be able to
* interact across profiles, even if {@link #canRequestInteractAcrossProfiles()} is {@code
* true}. The current ability to interact across profiles is given by {@link
* #canInteractAcrossProfiles()}.
*
* @return an {@link Intent} to open the settings page that allows the user to decide whether
* the app can interact across profiles
*
* @throws SecurityException if {@code mContext.getPackageName()} does not belong to the
* calling UID, or {@link #canRequestInteractAcrossProfiles()} is {@code false}.
*/
public @NonNull Intent createRequestInteractAcrossProfilesIntent() {
if (!canRequestInteractAcrossProfiles()) {
throw new SecurityException(
"The calling package can not request to interact across profiles.");
}
final Intent settingsIntent = new Intent();
settingsIntent.setAction(Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS);
final Uri packageUri = Uri.parse("package:" + mContext.getPackageName());
settingsIntent.setData(packageUri);
return settingsIntent;
}
/**
* Sets the app-op for {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} that is
* configurable by users in Settings. This configures it for the profile group of the calling
* package.
*
* <p>Before calling, check {@link #canConfigureInteractAcrossProfiles(String)} and do not call
* if it is {@code false}. If presenting a user interface, do not allow the user to configure
* the app-op in that case.
*
* <p>The underlying app-op {@link android.app.AppOpsManager#OP_INTERACT_ACROSS_PROFILES} should
* never be set directly. This method ensures that the app-op is kept in sync for the app across
* each user in the profile group and that those apps are sent a broadcast when their ability to
* interact across profiles changes.
*
* <p>This method should be used directly whenever a user's action results in a change in an
* app's ability to interact across profiles, as defined by the return value of {@link
* #canInteractAcrossProfiles()}. This includes user consent changes in Settings or during
* provisioning.
*
* <p>If other changes could have affected the app's ability to interact across profiles, as
* defined by the return value of {@link #canInteractAcrossProfiles()}, such as changes to the
* admin or OEM consent whitelists, then {@link #resetInteractAcrossProfilesAppOps(Collection,
* Set)} should be used.
*
* <p>If the caller does not have the {@link android.Manifest.permission
* #CONFIGURE_INTERACT_ACROSS_PROFILES} permission, then they must have the permissions that
* would have been required to call {@link android.app.AppOpsManager#setMode(int, int, String,
* int)}, which includes {@link android.Manifest.permission#MANAGE_APP_OPS_MODES}.
*
* <p>Also requires either {@link android.Manifest.permission#INTERACT_ACROSS_USERS} or {@link
* android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
*
* @hide
*/
@RequiresPermission(
allOf={android.Manifest.permission.CONFIGURE_INTERACT_ACROSS_PROFILES,
android.Manifest.permission.INTERACT_ACROSS_USERS})
public void setInteractAcrossProfilesAppOp(@NonNull String packageName, @Mode int newMode) {
try {
mService.setInteractAcrossProfilesAppOp(packageName, newMode);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Returns whether the given package can have its ability to interact across profiles configured
* by the user. This means that every other condition to interact across profiles has been set.
*
* <p>This differs from {@link #canRequestInteractAcrossProfiles()} since it will not return
* {@code false} simply when the target profile is disabled.
*
* @hide
*/
public boolean canConfigureInteractAcrossProfiles(@NonNull String packageName) {
try {
return mService.canConfigureInteractAcrossProfiles(packageName);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Returns {@code true} if the given package has requested
* {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} and the user has at least one
* other profile in the same profile group.
*
* <p>This differs from {@link #canConfigureInteractAcrossProfiles(String)} since it will
* not return {@code false} if the app is not whitelisted or not installed in the other profile.
*
* <p>Note that platform-signed apps that are automatically granted the permission and are not
* whitelisted by the OEM will not be included in this list.
*
* @hide
*/
public boolean canUserAttemptToConfigureInteractAcrossProfiles(String packageName) {
try {
return mService.canUserAttemptToConfigureInteractAcrossProfiles(packageName);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* For each of the packages defined in {@code previousCrossProfilePackages} but not included in
* {@code newCrossProfilePackages}, resets the app-op for {@link android.Manifest.permission
* #INTERACT_ACROSS_PROFILES} back to its default value if it can no longer be configured by
* users in Settings, as defined by {@link #canConfigureInteractAcrossProfiles(String)}.
*
* <p>This method should be used whenever an app's ability to interact across profiles could
* have changed as a result of non-user actions, such as changes to admin or OEM consent
* whitelists.
*
* <p>If the caller does not have the {@link android.Manifest.permission
* #CONFIGURE_INTERACT_ACROSS_PROFILES} permission, then they must have the permissions that
* would have been required to call {@link android.app.AppOpsManager#setMode(int, int, String,
* int)}, which includes {@link android.Manifest.permission#MANAGE_APP_OPS_MODES}.
*
* <p>Also requires either {@link android.Manifest.permission#INTERACT_ACROSS_USERS} or {@link
* android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
*
* @hide
*/
@RequiresPermission(
allOf={android.Manifest.permission.CONFIGURE_INTERACT_ACROSS_PROFILES,
android.Manifest.permission.INTERACT_ACROSS_USERS})
public void resetInteractAcrossProfilesAppOps(
@NonNull Collection<String> previousCrossProfilePackages,
@NonNull Set<String> newCrossProfilePackages) {
if (previousCrossProfilePackages.isEmpty()) {
return;
}
final List<String> unsetCrossProfilePackages =
previousCrossProfilePackages.stream()
.filter(packageName -> !newCrossProfilePackages.contains(packageName))
.collect(Collectors.toList());
if (unsetCrossProfilePackages.isEmpty()) {
return;
}
try {
mService.resetInteractAcrossProfilesAppOps(unsetCrossProfilePackages);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Clears the app-op for {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} back to
* its default value for every package on the device.
*
* <p>This method can be used to ensure that app-op state is not left around on existing users
* for previously-configured profiles.
*
* <p>If the caller does not have the {@link android.Manifest.permission
* #CONFIGURE_INTERACT_ACROSS_PROFILES} permission, then they must have the permissions that
* would have been required to call {@link android.app.AppOpsManager#setMode(int, int, String,
* int)}, which includes {@link android.Manifest.permission#MANAGE_APP_OPS_MODES}.
*
* <p>Also requires either {@link android.Manifest.permission#INTERACT_ACROSS_USERS} or {@link
* android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
*
* @hide
*/
@RequiresPermission(
allOf={android.Manifest.permission.CONFIGURE_INTERACT_ACROSS_PROFILES,
android.Manifest.permission.INTERACT_ACROSS_USERS})
public void clearInteractAcrossProfilesAppOps() {
try {
mService.clearInteractAcrossProfilesAppOps();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
private void verifyCanAccessUser(UserHandle userHandle) {
if (!getTargetUserProfiles().contains(userHandle)) {
throw new SecurityException("Not allowed to access " + userHandle);
}
}
}