blob: 253c73796caf2de975181b3032d96836199036cc [file] [log] [blame]
/*
* 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 android.app.role;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.internal.util.function.pooled.PooledLambda;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* This class provides information about and manages roles.
* <p>
* A role is a unique name within the system associated with certain privileges. The list of
* available roles might change with a system app update, so apps should not make assumption about
* the availability of roles. Instead, they should always query if the role is available using
* {@link #isRoleAvailable(String)} before trying to do anything with it. Some predefined role names
* are available as constants in this class, and a list of possibly available roles can be found in
* the <a href="{@docRoot}reference/androidx/core/role/package-summary.html">AndroidX Role
* library</a>.
* <p>
* 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. These requirements can be found in the AndroidX
* Libraries. Then the application will need user consent to become a role holder, which can be
* requested using {@link android.app.Activity#startActivityForResult(Intent, int)} with the
* {@code Intent} obtained from {@link #createRequestRoleIntent(String)}.
* <p>
* Upon becoming a role holder, the application may be granted certain privileges that are role
* specific. When the application loses its role, these privileges will also be revoked.
*/
@SystemService(Context.ROLE_SERVICE)
public final class RoleManager {
private static final String LOG_TAG = RoleManager.class.getSimpleName();
/**
* The name of the assistant app role.
*
* @see android.service.voice.VoiceInteractionService
*/
public static final String ROLE_ASSISTANT = "android.app.role.ASSISTANT";
/**
* The name of the browser role.
*
* @see Intent#CATEGORY_APP_BROWSER
*/
public static final String ROLE_BROWSER = "android.app.role.BROWSER";
/**
* The name of the dialer role.
*
* @see Intent#ACTION_DIAL
* @see android.telecom.InCallService
*/
public static final String ROLE_DIALER = "android.app.role.DIALER";
/**
* The name of the SMS role.
*
* @see Intent#CATEGORY_APP_MESSAGING
*/
public static final String ROLE_SMS = "android.app.role.SMS";
/**
* The name of the emergency role
*/
public static final String ROLE_EMERGENCY = "android.app.role.EMERGENCY";
/**
* The name of the home role.
*
* @see Intent#CATEGORY_HOME
*/
public static final String ROLE_HOME = "android.app.role.HOME";
/**
* The name of the call redirection role.
* <p>
* A call redirection app provides a means to re-write the phone number for an outgoing call to
* place the call through a call redirection service.
*
* @see android.telecom.CallRedirectionService
*/
public static final String ROLE_CALL_REDIRECTION = "android.app.role.CALL_REDIRECTION";
/**
* The name of the call screening and caller id role.
*
* @see android.telecom.CallScreeningService
*/
public static final String ROLE_CALL_SCREENING = "android.app.role.CALL_SCREENING";
/**
* @hide
*/
@IntDef(flag = true, value = { MANAGE_HOLDERS_FLAG_DONT_KILL_APP })
public @interface ManageHoldersFlags {}
/**
* Flag parameter for {@link #addRoleHolderAsUser}, {@link #removeRoleHolderAsUser} and
* {@link #clearRoleHoldersAsUser} to indicate that apps should not be killed when changing
* their role holder status.
*
* @hide
*/
@SystemApi
@TestApi
public static final int MANAGE_HOLDERS_FLAG_DONT_KILL_APP = 1;
/**
* The action used to request user approval of a role for an application.
*
* @hide
*/
public static final String ACTION_REQUEST_ROLE = "android.app.role.action.REQUEST_ROLE";
/**
* The permission required to manage records of role holders in {@link RoleManager} directly.
*
* @hide
*/
public static final String PERMISSION_MANAGE_ROLES_FROM_CONTROLLER =
"com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER";
@NonNull
private final Context mContext;
@NonNull
private final IRoleManager mService;
@GuardedBy("mListenersLock")
@NonNull
private final SparseArray<ArrayMap<OnRoleHoldersChangedListener,
OnRoleHoldersChangedListenerDelegate>> mListeners = new SparseArray<>();
@NonNull
private final Object mListenersLock = new Object();
/**
* @hide
*/
public RoleManager(@NonNull Context context) throws ServiceManager.ServiceNotFoundException {
mContext = context;
mService = IRoleManager.Stub.asInterface(ServiceManager.getServiceOrThrow(
Context.ROLE_SERVICE));
}
/**
* Returns an {@code Intent} suitable for passing to
* {@link android.app.Activity#startActivityForResult(Intent, int)} which prompts the user to
* grant a role to this application.
* <p>
* If the role is granted, the {@code resultCode} will be
* {@link android.app.Activity#RESULT_OK}, otherwise it will be
* {@link android.app.Activity#RESULT_CANCELED}.
*
* @param roleName the name of requested role
*
* @return the {@code Intent} to prompt user to grant the role
*/
@NonNull
public Intent createRequestRoleIntent(@NonNull String roleName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Intent intent = new Intent(ACTION_REQUEST_ROLE);
intent.setPackage(mContext.getPackageManager().getPermissionControllerPackageName());
intent.putExtra(Intent.EXTRA_ROLE_NAME, roleName);
return intent;
}
/**
* Check whether a role is available in the system.
*
* @param roleName the name of role to checking for
*
* @return whether the role is available in the system
*/
public boolean isRoleAvailable(@NonNull String roleName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
try {
return mService.isRoleAvailable(roleName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Check whether the calling application is holding a particular role.
*
* @param roleName the name of the role to check for
*
* @return whether the calling application is holding the role
*/
public boolean isRoleHeld(@NonNull String roleName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
try {
return mService.isRoleHeld(roleName, mContext.getPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Get package names of the applications holding the role.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.MANAGE_ROLE_HOLDERS}.
*
* @param roleName the name of the role to get the role holder for
*
* @return a list of package names of the role holders, or an empty list if none.
*
* @see #getRoleHoldersAsUser(String, UserHandle)
*
* @hide
*/
@NonNull
@RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
@SystemApi
@TestApi
public List<String> getRoleHolders(@NonNull String roleName) {
return getRoleHoldersAsUser(roleName, Process.myUserHandle());
}
/**
* Get package names of the applications holding the role.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param roleName the name of the role to get the role holder for
* @param user the user to get the role holder for
*
* @return a list of package names of the role holders, or an empty list if none.
*
* @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
* @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
* @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
*
* @hide
*/
@NonNull
@RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
@SystemApi
@TestApi
public List<String> getRoleHoldersAsUser(@NonNull String roleName, @NonNull UserHandle user) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Objects.requireNonNull(user, "user cannot be null");
try {
return mService.getRoleHoldersAsUser(roleName, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Add a specific application to the holders of a role. If the role is exclusive, the previous
* holder will be replaced.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param roleName the name of the role to add the role holder for
* @param packageName the package name of the application to add to the role holders
* @param flags optional behavior flags
* @param user the user to add the role holder for
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
* @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
*
* @hide
*/
@RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
@SystemApi
@TestApi
public void addRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
@ManageHoldersFlags int flags, @NonNull UserHandle user,
@CallbackExecutor @NonNull Executor executor, @NonNull Consumer<Boolean> callback) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
Objects.requireNonNull(user, "user cannot be null");
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
try {
mService.addRoleHolderAsUser(roleName, packageName, flags, user.getIdentifier(),
createRemoteCallback(executor, callback));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Remove a specific application from the holders of a role.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param roleName the name of the role to remove the role holder for
* @param packageName the package name of the application to remove from the role holders
* @param flags optional behavior flags
* @param user the user to remove the role holder for
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
* @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
*
* @hide
*/
@RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
@SystemApi
@TestApi
public void removeRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
@ManageHoldersFlags int flags, @NonNull UserHandle user,
@CallbackExecutor @NonNull Executor executor, @NonNull Consumer<Boolean> callback) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
Objects.requireNonNull(user, "user cannot be null");
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
try {
mService.removeRoleHolderAsUser(roleName, packageName, flags, user.getIdentifier(),
createRemoteCallback(executor, callback));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Remove all holders of a role.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param roleName the name of the role to remove role holders for
* @param flags optional behavior flags
* @param user the user to remove role holders for
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
* @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
*
* @hide
*/
@RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
@SystemApi
@TestApi
public void clearRoleHoldersAsUser(@NonNull String roleName, @ManageHoldersFlags int flags,
@NonNull UserHandle user, @CallbackExecutor @NonNull Executor executor,
@NonNull Consumer<Boolean> callback) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Objects.requireNonNull(user, "user cannot be null");
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
try {
mService.clearRoleHoldersAsUser(roleName, flags, user.getIdentifier(),
createRemoteCallback(executor, callback));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@NonNull
private static RemoteCallback createRemoteCallback(@NonNull Executor executor,
@NonNull Consumer<Boolean> callback) {
return new RemoteCallback(result -> executor.execute(() -> {
boolean successful = result != null;
long token = Binder.clearCallingIdentity();
try {
callback.accept(successful);
} finally {
Binder.restoreCallingIdentity(token);
}
}));
}
/**
* Add a listener to observe role holder changes
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param executor the {@code Executor} to call the listener on.
* @param listener the listener to be added
* @param user the user to add the listener for
*
* @see #removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener, UserHandle)
*
* @hide
*/
@RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
@SystemApi
@TestApi
public void addOnRoleHoldersChangedListenerAsUser(@CallbackExecutor @NonNull Executor executor,
@NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(listener, "listener cannot be null");
Objects.requireNonNull(user, "user cannot be null");
int userId = user.getIdentifier();
synchronized (mListenersLock) {
ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
mListeners.get(userId);
if (listeners == null) {
listeners = new ArrayMap<>();
mListeners.put(userId, listeners);
} else {
if (listeners.containsKey(listener)) {
return;
}
}
OnRoleHoldersChangedListenerDelegate listenerDelegate =
new OnRoleHoldersChangedListenerDelegate(executor, listener);
try {
mService.addOnRoleHoldersChangedListenerAsUser(listenerDelegate, userId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
listeners.put(listener, listenerDelegate);
}
}
/**
* Remove a listener observing role holder changes
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
* {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
*
* @param listener the listener to be removed
* @param user the user to remove the listener for
*
* @see #addOnRoleHoldersChangedListenerAsUser(Executor, OnRoleHoldersChangedListener,
* UserHandle)
*
* @hide
*/
@RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
@SystemApi
@TestApi
public void removeOnRoleHoldersChangedListenerAsUser(
@NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
Objects.requireNonNull(listener, "listener cannot be null");
Objects.requireNonNull(user, "user cannot be null");
int userId = user.getIdentifier();
synchronized (mListenersLock) {
ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
mListeners.get(userId);
if (listeners == null) {
return;
}
OnRoleHoldersChangedListenerDelegate listenerDelegate = listeners.get(listener);
if (listenerDelegate == null) {
return;
}
try {
mService.removeOnRoleHoldersChangedListenerAsUser(listenerDelegate,
user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
listeners.remove(listener);
if (listeners.isEmpty()) {
mListeners.remove(userId);
}
}
}
/**
* Set the names of all the available roles. Should only be called from
* {@link android.app.role.RoleControllerService}.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
*
* @param roleNames the names of all the available roles
*
* @hide
*/
@RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
@SystemApi
@TestApi
public void setRoleNamesFromController(@NonNull List<String> roleNames) {
Objects.requireNonNull(roleNames, "roleNames cannot be null");
try {
mService.setRoleNamesFromController(roleNames);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Add a specific application to the holders of a role, only modifying records inside
* {@link RoleManager}. Should only be called from
* {@link android.app.role.RoleControllerService}.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
*
* @param roleName the name of the role to add the role holder for
* @param packageName the package name of the application to add to the role holders
*
* @return whether the operation was successful, and will also be {@code true} if a matching
* role holder is already found.
*
* @see #getRoleHolders(String)
* @see #removeRoleHolderFromController(String, String)
*
* @hide
*/
@RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
@SystemApi
@TestApi
public boolean addRoleHolderFromController(@NonNull String roleName,
@NonNull String packageName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
try {
return mService.addRoleHolderFromController(roleName, packageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Remove a specific application from the holders of a role, only modifying records inside
* {@link RoleManager}. Should only be called from
* {@link android.app.role.RoleControllerService}.
* <p>
* <strong>Note:</strong> Using this API requires holding
* {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
*
* @param roleName the name of the role to remove the role holder for
* @param packageName the package name of the application to remove from the role holders
*
* @return whether the operation was successful, and will also be {@code true} if no matching
* role holder was found to remove.
*
* @see #getRoleHolders(String)
* @see #addRoleHolderFromController(String, String)
*
* @hide
*/
@RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
@SystemApi
@TestApi
public boolean removeRoleHolderFromController(@NonNull String roleName,
@NonNull String packageName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
try {
return mService.removeRoleHolderFromController(roleName, packageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of all roles that the given package is currently holding
*
* @param packageName the package name
* @return the list of role names
*
* @hide
*/
@NonNull
@RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
@SystemApi
@TestApi
public List<String> getHeldRolesFromController(@NonNull String packageName) {
Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
try {
return mService.getHeldRolesFromController(packageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Allows getting the role holder for {@link #ROLE_SMS} without
* {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as required by
* {@link android.provider.Telephony.Sms#getDefaultSmsPackage(Context)}
*
* @param userId The user ID to get the default SMS package for.
* @return the package name of the default SMS app, or {@code null} if not configured.
* @hide
*/
@Nullable
public String getDefaultSmsPackage(@UserIdInt int userId) {
try {
return mService.getDefaultSmsPackage(userId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static class OnRoleHoldersChangedListenerDelegate
extends IOnRoleHoldersChangedListener.Stub {
@NonNull
private final Executor mExecutor;
@NonNull
private final OnRoleHoldersChangedListener mListener;
OnRoleHoldersChangedListenerDelegate(@NonNull Executor executor,
@NonNull OnRoleHoldersChangedListener listener) {
mExecutor = executor;
mListener = listener;
}
@Override
public void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(PooledLambda.obtainRunnable(
OnRoleHoldersChangedListener::onRoleHoldersChanged, mListener, roleName,
UserHandle.of(userId)));
} finally {
Binder.restoreCallingIdentity(token);
}
}
}
}