/*
* Copyright (C) 2018 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 com.android.packageinstaller.role.model;
import android.app.ActivityManager;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.Preference;
import com.android.packageinstaller.Constants;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.packageinstaller.role.ui.TwoTargetPreference;
import com.android.packageinstaller.role.utils.PackageUtils;
import com.android.packageinstaller.role.utils.UserUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Specifies a role and its properties.
*
* A role is a unique name within the system associated with certain privileges. There can be
* multiple applications qualifying for a role, but only a subset of them can become role holders.
* To qualify for a role, an application must meet certain requirements, including defining certain
* components in its manifest. Then the application will need user consent to become the role
* holder.
*
* Upon becoming a role holder, the application may be granted certain permissions, have certain
* app ops set to certain modes and certain {@code Activity} components configured as preferred for
* certain {@code Intent} actions. When an application loses its role, these privileges will also be
* revoked.
*
* @see android.app.role.RoleManager
*/
public class Role {
private static final String LOG_TAG = Role.class.getSimpleName();
private static final boolean DEBUG = false;
private static final String PACKAGE_NAME_ANDROID_SYSTEM = "android";
/**
* The name of this role. Must be unique.
*/
@NonNull
private final String mName;
/**
* The behavior of this role.
*/
@Nullable
private final RoleBehavior mBehavior;
/**
* The string resource for the description of this role.
*/
@StringRes
private final int mDescriptionResource;
/**
* Whether this role is exclusive, i.e. allows at most one holder.
*/
private final boolean mExclusive;
/**
* The string resource for the label of this role.
*/
@StringRes
private final int mLabelResource;
/**
* The string resource for the request description of this role, shown below the selected app in
* the request role dialog.
*/
@StringRes
private final int mRequestDescriptionResource;
/**
* The string resource for the request title of this role, shown as the title of the request
* role dialog.
*/
@StringRes
private final int mRequestTitleResource;
/**
* Whether this role is requestable by applications with
* {@link android.app.role.RoleManager#createRequestRoleIntent(String)}.
*/
private final boolean mRequestable;
/**
* The string resource for the short label of this role, currently used when in a list of roles.
*/
@StringRes
private final int mShortLabelResource;
/**
* Whether the UI for this role will show the "None" item. Only valid if this role is
* {@link #mExclusive exclusive}, and {@link #getFallbackHolder(Context)} should also return
* empty to allow actually selecting "None".
*/
private final boolean mShowNone;
/**
* Whether this role only accepts system apps as its holders.
*/
private final boolean mSystemOnly;
/**
* The required components for an application to qualify for this role.
*/
@NonNull
private final List mRequiredComponents;
/**
* The permissions to be granted by this role.
*/
@NonNull
private final List mPermissions;
/**
* The app ops to be set to allowed by this role.
*/
@NonNull
private final List mAppOps;
/**
* The set of preferred {@code Activity} configurations to be configured by this role.
*/
@NonNull
private final List mPreferredActivities;
public Role(@NonNull String name, @Nullable RoleBehavior behavior,
@StringRes int descriptionResource, boolean exclusive, @StringRes int labelResource,
@StringRes int requestDescriptionResource, @StringRes int requestTitleResource,
boolean requestable, @StringRes int shortLabelResource, boolean showNone,
boolean systemOnly, @NonNull List requiredComponents,
@NonNull List permissions, @NonNull List appOps,
@NonNull List preferredActivities) {
mName = name;
mBehavior = behavior;
mDescriptionResource = descriptionResource;
mExclusive = exclusive;
mLabelResource = labelResource;
mRequestDescriptionResource = requestDescriptionResource;
mRequestTitleResource = requestTitleResource;
mRequestable = requestable;
mShortLabelResource = shortLabelResource;
mShowNone = showNone;
mSystemOnly = systemOnly;
mRequiredComponents = requiredComponents;
mPermissions = permissions;
mAppOps = appOps;
mPreferredActivities = preferredActivities;
}
@NonNull
public String getName() {
return mName;
}
@Nullable
public RoleBehavior getBehavior() {
return mBehavior;
}
@StringRes
public int getDescriptionResource() {
return mDescriptionResource;
}
public boolean isExclusive() {
return mExclusive;
}
@StringRes
public int getLabelResource() {
return mLabelResource;
}
@StringRes
public int getRequestDescriptionResource() {
return mRequestDescriptionResource;
}
@StringRes
public int getRequestTitleResource() {
return mRequestTitleResource;
}
public boolean isRequestable() {
return mRequestable;
}
@StringRes
public int getShortLabelResource() {
return mShortLabelResource;
}
/**
* @see #mShowNone
*/
public boolean shouldShowNone() {
return mShowNone;
}
@NonNull
public List getRequiredComponents() {
return mRequiredComponents;
}
@NonNull
public List getPermissions() {
return mPermissions;
}
@NonNull
public List getAppOps() {
return mAppOps;
}
@NonNull
public List getPreferredActivities() {
return mPreferredActivities;
}
/**
* Callback when this role is added to the system for the first time.
*
* @param context the {@code Context} to retrieve system services
*/
public void onRoleAdded(@NonNull Context context) {
if (mBehavior != null) {
mBehavior.onRoleAdded(this, context);
}
}
/**
* Check whether this role is available.
*
* @param user the user to check for
* @param context the {@code Context} to retrieve system services
*
* @return whether this role is available.
*/
public boolean isAvailableAsUser(@NonNull UserHandle user, @NonNull Context context) {
if (mBehavior != null) {
return mBehavior.isAvailableAsUser(this, user, context);
}
return true;
}
/**
* Check whether this role is available, for current user.
*
* @param context the {@code Context} to retrieve system services
*
* @return whether this role is available.
*/
public boolean isAvailable(@NonNull Context context) {
return isAvailableAsUser(Process.myUserHandle(), context);
}
/**
* Get the default holders of this role, which will be added when the role is added for the
* first time.
*
* @param context the {@code Context} to retrieve system services
*
* @return the list of package names of the default holders
*/
@NonNull
public List getDefaultHolders(@NonNull Context context) {
if (mBehavior != null) {
return mBehavior.getDefaultHolders(this, context);
}
return Collections.emptyList();
}
/**
* Get the fallback holder of this role, which will be added whenever there are no role holders.
*
* Should return {@code null} if this role {@link #mShowNone shows a "None" item}.
*
* @param context the {@code Context} to retrieve system services
*
* @return the package name of the fallback holder, or {@code null} if none
*/
@Nullable
public String getFallbackHolder(@NonNull Context context) {
if (mBehavior != null && !isNoneHolderSelected(context)) {
return mBehavior.getFallbackHolder(this, context);
}
return null;
}
/**
* Check whether this role should be visible to user.
*
* @param user the user to check for
* @param context the {@code Context} to retrieve system services
*
* @return whether this role should be visible to user
*/
public boolean isVisibleAsUser(@NonNull UserHandle user, @NonNull Context context) {
if (mBehavior != null) {
return mBehavior.isVisibleAsUser(this, user, context);
}
return true;
}
/**
* Check whether this role should be visible to user, for current user.
*
* @param context the {@code Context} to retrieve system services
*
* @return whether this role should be visible to user.
*/
public boolean isVisible(@NonNull Context context) {
return isVisibleAsUser(Process.myUserHandle(), context);
}
/**
* Get the {@link Intent} to manage this role, or {@code null} to use the default UI.
*
* @param user the user to manage this role for
* @param context the {@code Context} to retrieve system services
*
* @return the {@link Intent} to manage this role, or {@code null} to use the default UI.
*/
@Nullable
public Intent getManageIntentAsUser(@NonNull UserHandle user, @NonNull Context context) {
if (mBehavior != null) {
return mBehavior.getManageIntentAsUser(this, user, context);
}
return null;
}
/**
* Prepare a {@link Preference} for this role.
*
* @param preference the {@link Preference} for this role
* @param user the user for this role
* @param context the {@code Context} to retrieve system services
*/
public void preparePreferenceAsUser(@NonNull TwoTargetPreference preference,
@NonNull UserHandle user, @NonNull Context context) {
if (mBehavior != null) {
mBehavior.preparePreferenceAsUser(this, preference, user, context);
}
}
/**
* Check whether a qualifying application should be visible to user.
*
* @param applicationInfo the {@link ApplicationInfo} for the application
* @param user the user for the application
* @param context the {@code Context} to retrieve system services
*
* @return whether the qualifying application should be visible to user
*/
public boolean isApplicationVisibleAsUser(@NonNull ApplicationInfo applicationInfo,
@NonNull UserHandle user, @NonNull Context context) {
if (mBehavior != null) {
return mBehavior.isApplicationVisibleAsUser(this, applicationInfo, user, context);
}
return true;
}
/**
* Prepare a {@link Preference} for an application.
*
* @param preference the {@link Preference} for the application
* @param applicationInfo the {@link ApplicationInfo} for the application
* @param user the user for the application
* @param context the {@code Context} to retrieve system services
*/
public void prepareApplicationPreferenceAsUser(@NonNull Preference preference,
@NonNull ApplicationInfo applicationInfo, @NonNull UserHandle user,
@NonNull Context context) {
if (mBehavior != null) {
mBehavior.prepareApplicationPreferenceAsUser(this, preference, applicationInfo, user,
context);
}
}
/**
* Get the confirmation message for adding an application as a holder of this role.
*
* @param packageName the package name of the application to get confirmation message for
* @param context the {@code Context} to retrieve system services
*
* @return the confirmation message, or {@code null} if no confirmation is needed
*/
@Nullable
public CharSequence getConfirmationMessage(@NonNull String packageName,
@NonNull Context context) {
if (mBehavior != null) {
return mBehavior.getConfirmationMessage(this, packageName, context);
}
return null;
}
/**
* Check whether a package is qualified for this role, i.e. whether it contains all the required
* components (plus meeting some other general restrictions).
*
* @param packageName the package name to check for
* @param context the {@code Context} to retrieve system services
*
* @return whether the package is qualified for a role
*/
public boolean isPackageQualified(@NonNull String packageName, @NonNull Context context) {
if (!isPackageMinimallyQualifiedAsUser(packageName, Process.myUserHandle(), context)) {
return false;
}
if (mBehavior != null) {
Boolean isPackageQualified = mBehavior.isPackageQualified(this, packageName, context);
if (isPackageQualified != null) {
return isPackageQualified;
}
}
int requiredComponentsSize = mRequiredComponents.size();
for (int i = 0; i < requiredComponentsSize; i++) {
RequiredComponent requiredComponent = mRequiredComponents.get(i);
if (requiredComponent.getQualifyingComponentForPackage(packageName, context) == null) {
Log.w(LOG_TAG, packageName + " not qualified for " + mName
+ " due to missing " + requiredComponent);
return false;
}
}
return true;
}
/**
* Get the list of packages that are qualified for this role, i.e. packages containing all the
* required components (plus meeting some other general restrictions).
*
* @param user the user to get the qualifying packages.
* @param context the {@code Context} to retrieve system services
*
* @return the list of packages that are qualified for this role
*/
@NonNull
public List getQualifyingPackagesAsUser(@NonNull UserHandle user,
@NonNull Context context) {
List qualifyingPackages = null;
if (mBehavior != null) {
qualifyingPackages = mBehavior.getQualifyingPackagesAsUser(this, user, context);
}
if (qualifyingPackages == null) {
ArrayMap packageComponentCountMap = new ArrayMap<>();
int requiredComponentsSize = mRequiredComponents.size();
for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize;
requiredComponentsIndex++) {
RequiredComponent requiredComponent = mRequiredComponents.get(
requiredComponentsIndex);
// This returns at most one component per package.
List qualifyingComponents =
requiredComponent.getQualifyingComponentsAsUser(user, context);
int qualifyingComponentsSize = qualifyingComponents.size();
for (int qualifyingComponentsIndex = 0;
qualifyingComponentsIndex < qualifyingComponentsSize;
++qualifyingComponentsIndex) {
ComponentName componentName = qualifyingComponents.get(
qualifyingComponentsIndex);
String packageName = componentName.getPackageName();
Integer componentCount = packageComponentCountMap.get(packageName);
packageComponentCountMap.put(packageName, componentCount == null ? 1
: componentCount + 1);
}
}
qualifyingPackages = new ArrayList<>();
int packageComponentCountMapSize = packageComponentCountMap.size();
for (int i = 0; i < packageComponentCountMapSize; i++) {
int componentCount = packageComponentCountMap.valueAt(i);
if (componentCount != requiredComponentsSize) {
continue;
}
String packageName = packageComponentCountMap.keyAt(i);
qualifyingPackages.add(packageName);
}
}
int qualifyingPackagesSize = qualifyingPackages.size();
for (int i = 0; i < qualifyingPackagesSize; ) {
String packageName = qualifyingPackages.get(i);
if (!isPackageMinimallyQualifiedAsUser(packageName, user, context)) {
qualifyingPackages.remove(i);
qualifyingPackagesSize--;
} else {
i++;
}
}
return qualifyingPackages;
}
private boolean isPackageMinimallyQualifiedAsUser(
@NonNull String packageName, @NonNull UserHandle user, @NonNull Context context) {
if (Objects.equals(packageName, PACKAGE_NAME_ANDROID_SYSTEM)) {
return false;
}
ApplicationInfo applicationInfo = PackageUtils.getApplicationInfoAsUser(packageName, user,
context);
if (applicationInfo == null) {
Log.w(LOG_TAG, "Cannot get ApplicationInfo for package: " + packageName + ", user: "
+ user.getIdentifier());
return false;
}
if (mSystemOnly && (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return false;
}
if (!applicationInfo.enabled) {
return false;
}
if (applicationInfo.isInstantApp()) {
return false;
}
PackageManager userPackageManager = UserUtils.getUserContext(context, user)
.getPackageManager();
if (!userPackageManager.getDeclaredSharedLibraries(packageName, 0).isEmpty()) {
return false;
}
return true;
}
/**
* Grant this role to an application.
*
* @param packageName the package name of the application to be granted this role to
* @param dontKillApp whether this application should not be killed despite changes
* @param overrideUserSetAndFixedPermissions whether to override user set and fixed flags on
* permissions
* @param context the {@code Context} to retrieve system services
*/
public void grant(@NonNull String packageName, boolean dontKillApp,
boolean overrideUserSetAndFixedPermissions, @NonNull Context context) {
boolean permissionOrAppOpChanged = Permissions.grant(packageName, mPermissions, true,
overrideUserSetAndFixedPermissions, true, false, false, context);
int appOpsSize = mAppOps.size();
for (int i = 0; i < appOpsSize; i++) {
AppOp appOp = mAppOps.get(i);
appOp.grant(packageName, context);
}
int preferredActivitiesSize = mPreferredActivities.size();
for (int i = 0; i < preferredActivitiesSize; i++) {
PreferredActivity preferredActivity = mPreferredActivities.get(i);
preferredActivity.configure(packageName, context);
}
if (mBehavior != null) {
mBehavior.grant(this, packageName, context);
}
if (!dontKillApp && permissionOrAppOpChanged && !Permissions.isRuntimePermissionsSupported(
packageName, context)) {
killApp(packageName, context);
}
}
/**
* Revoke this role from an application.
*
* @param packageName the package name of the application to be granted this role to
* @param dontKillApp whether this application should not be killed despite changes
* @param overrideSystemFixedPermissions whether system-fixed permissions can be revoked
* @param context the {@code Context} to retrieve system services
*/
public void revoke(@NonNull String packageName, boolean dontKillApp,
boolean overrideSystemFixedPermissions, @NonNull Context context) {
RoleManager roleManager = context.getSystemService(RoleManager.class);
List otherRoleNames = roleManager.getHeldRolesFromController(packageName);
otherRoleNames.remove(mName);
List permissionsToRevoke = new ArrayList<>(mPermissions);
ArrayMap roles = Roles.get(context);
int otherRoleNamesSize = otherRoleNames.size();
for (int i = 0; i < otherRoleNamesSize; i++) {
String roleName = otherRoleNames.get(i);
Role role = roles.get(roleName);
permissionsToRevoke.removeAll(role.getPermissions());
}
boolean permissionOrAppOpChanged = Permissions.revoke(packageName, permissionsToRevoke,
true, false, overrideSystemFixedPermissions, context);
List appOpsToRevoke = new ArrayList<>(mAppOps);
for (int i = 0; i < otherRoleNamesSize; i++) {
String roleName = otherRoleNames.get(i);
Role role = roles.get(roleName);
appOpsToRevoke.removeAll(role.getAppOps());
}
int appOpsSize = appOpsToRevoke.size();
for (int i = 0; i < appOpsSize; i++) {
AppOp appOp = appOpsToRevoke.get(i);
appOp.revoke(packageName, context);
}
// TODO: Revoke preferred activities? But this is unnecessary for most roles using it as
// they have fallback holders. Moreover, clearing the preferred activity might result in
// other system components listening to preferred activity change get notified for the
// wrong thing when we are removing a exclusive role holder for adding another.
if (mBehavior != null) {
mBehavior.revoke(this, packageName, context);
}
if (!dontKillApp && permissionOrAppOpChanged) {
killApp(packageName, context);
}
}
private void killApp(@NonNull String packageName, @NonNull Context context) {
if (DEBUG) {
Log.i(LOG_TAG, "Killing " + packageName + " due to "
+ Thread.currentThread().getStackTrace()[3].getMethodName()
+ "(" + mName + ")");
}
ApplicationInfo applicationInfo = PackageUtils.getApplicationInfo(packageName, context);
if (applicationInfo == null) {
Log.w(LOG_TAG, "Cannot get ApplicationInfo for package: " + packageName);
return;
}
ActivityManager activityManager = context.getSystemService(ActivityManager.class);
activityManager.killUid(applicationInfo.uid, "Permission or app op changed");
}
/**
* Check whether the "none" role holder is selected.
*
* @param context the {@code Context} to retrieve system services
*
* @return whether the "none" role holder is selected
*/
private boolean isNoneHolderSelected(@NonNull Context context) {
return Utils.getDeviceProtectedSharedPreferences(context).getBoolean(
Constants.IS_NONE_ROLE_HOLDER_SELECTED_KEY + mName, false);
}
/**
* Callback when a role holder (other than "none") was added.
*
* @param packageName the package name of the role holder
* @param user the user for the role
* @param context the {@code Context} to retrieve system services
*/
public void onHolderAddedAsUser(@NonNull String packageName, @NonNull UserHandle user,
@NonNull Context context) {
Utils.getDeviceProtectedSharedPreferences(UserUtils.getUserContext(context, user)).edit()
.remove(Constants.IS_NONE_ROLE_HOLDER_SELECTED_KEY + mName)
.apply();
}
/**
* Callback when a role holder (other than "none") was selected in the UI and added
* successfully.
*
* @param packageName the package name of the role holder
* @param user the user for the role
* @param context the {@code Context} to retrieve system services
*/
public void onHolderSelectedAsUser(@NonNull String packageName, @NonNull UserHandle user,
@NonNull Context context) {
if (mBehavior != null) {
mBehavior.onHolderSelectedAsUser(this, packageName, user, context);
}
}
/**
* Callback when a role holder changed.
*
* @param user the user for the role
* @param context the {@code Context} to retrieve system services
*/
public void onHolderChangedAsUser(@NonNull UserHandle user,
@NonNull Context context) {
if (mBehavior != null) {
mBehavior.onHolderChangedAsUser(this, user, context);
}
}
/**
* Callback when the "none" role holder was selected in the UI.
*
* @param user the user for the role
* @param context the {@code Context} to retrieve system services
*/
public void onNoneHolderSelectedAsUser(@NonNull UserHandle user, @NonNull Context context) {
Utils.getDeviceProtectedSharedPreferences(UserUtils.getUserContext(context, user)).edit()
.putBoolean(Constants.IS_NONE_ROLE_HOLDER_SELECTED_KEY + mName, true)
.apply();
}
@Override
public String toString() {
return "Role{"
+ "mName='" + mName + '\''
+ ", mBehavior=" + mBehavior
+ ", mExclusive=" + mExclusive
+ ", mLabelResource=" + mLabelResource
+ ", mShowNone=" + mShowNone
+ ", mSystemOnly=" + mSystemOnly
+ ", mRequiredComponents=" + mRequiredComponents
+ ", mPermissions=" + mPermissions
+ ", mAppOps=" + mAppOps
+ ", mPreferredActivities=" + mPreferredActivities
+ '}';
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
Role that = (Role) object;
return mExclusive == that.mExclusive
&& mLabelResource == that.mLabelResource
&& mShowNone == that.mShowNone
&& mSystemOnly == that.mSystemOnly
&& mName.equals(that.mName)
&& Objects.equals(mBehavior, that.mBehavior)
&& mRequiredComponents.equals(that.mRequiredComponents)
&& mPermissions.equals(that.mPermissions)
&& mAppOps.equals(that.mAppOps)
&& mPreferredActivities.equals(that.mPreferredActivities);
}
@Override
public int hashCode() {
return Objects.hash(mName, mBehavior, mExclusive, mLabelResource, mShowNone, mSystemOnly,
mRequiredComponents, mPermissions, mAppOps, mPreferredActivities);
}
}