diff options
37 files changed, 3792 insertions, 1 deletions
diff --git a/Android.bp b/Android.bp index e30df05b2..be51143e4 100644 --- a/Android.bp +++ b/Android.bp @@ -26,6 +26,7 @@ apex_defaults { certificate: ":com.android.permission.certificate", java_libs: [ "framework-permission", + "framework-permission-s", "service-permission", ], apps: ["PermissionController"], @@ -41,3 +42,8 @@ android_app_certificate { name: "com.android.permission.certificate", certificate: "com.android.permission", } + +filegroup { + name: "permission-jarjar-rules", + srcs: ["jarjar-rules.txt"], +} diff --git a/apex_manifest.json b/apex_manifest.json index 7960598af..6350d54d6 100644 --- a/apex_manifest.json +++ b/apex_manifest.json @@ -1,4 +1,4 @@ { "name": "com.android.permission", - "version": 300000000 + "version": 309999999 } diff --git a/framework-s/Android.bp b/framework-s/Android.bp new file mode 100644 index 000000000..e71cc43f1 --- /dev/null +++ b/framework-s/Android.bp @@ -0,0 +1,72 @@ +// Copyright (C) 2021 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. + +filegroup { + name: "framework-permission-s-sources", + srcs: [ + "java/**/*.java", + "java/**/*.aidl", + ], + path: "java", + visibility: ["//frameworks/base"], +} + +java_library { + name: "framework-permission-s-shared", + srcs: [":framework-permission-s-shared-srcs"], + libs: [ + "framework-annotations-lib", + "unsupportedappusage", + ], + apex_available: [ + "com.android.permission", + "test_com.android.permission", + ], + installable: false, + min_sdk_version: "30", + sdk_version: "module_current", +} + +java_sdk_library { + name: "framework-permission-s", + defaults: ["framework-module-defaults"], + srcs: [ + ":framework-permission-s-sources", + ], + libs: [ + "framework-annotations-lib" + ], + static_libs: [ + "framework-permission-s-shared", + ], + apex_available: [ + "com.android.permission", + "test_com.android.permission", + ], + hostdex: true, + // Restrict access to implementation library. + impl_library_visibility: [ + "//frameworks/base/apex/permission:__subpackages__", + "//packages/modules/Permission:__subpackages__", + ], + installable: true, + jarjar_rules: ":permission-jarjar-rules", + min_sdk_version: "30", + permitted_packages: [ + "android.permission", + "android.app.role", + // For com.android.permission.jarjar. + "com.android.permission", + ], +} diff --git a/framework-s/api/current.txt b/framework-s/api/current.txt new file mode 100644 index 000000000..4ecc98980 --- /dev/null +++ b/framework-s/api/current.txt @@ -0,0 +1,19 @@ +// Signature format: 2.0 +package android.app.role { + + public final class RoleManager { + method @NonNull public android.content.Intent createRequestRoleIntent(@NonNull String); + method public boolean isRoleAvailable(@NonNull String); + method public boolean isRoleHeld(@NonNull String); + field public static final String ROLE_ASSISTANT = "android.app.role.ASSISTANT"; + field public static final String ROLE_BROWSER = "android.app.role.BROWSER"; + field public static final String ROLE_CALL_REDIRECTION = "android.app.role.CALL_REDIRECTION"; + field public static final String ROLE_CALL_SCREENING = "android.app.role.CALL_SCREENING"; + field public static final String ROLE_DIALER = "android.app.role.DIALER"; + field public static final String ROLE_EMERGENCY = "android.app.role.EMERGENCY"; + field public static final String ROLE_HOME = "android.app.role.HOME"; + field public static final String ROLE_SMS = "android.app.role.SMS"; + } + +} + diff --git a/framework-s/api/module-lib-current.txt b/framework-s/api/module-lib-current.txt new file mode 100644 index 000000000..d7c9a2395 --- /dev/null +++ b/framework-s/api/module-lib-current.txt @@ -0,0 +1,11 @@ +// Signature format: 2.0 +package android.app.role { + + public final class RoleManager { + method @Nullable public String getBrowserRoleHolder(int); + method @Nullable public String getSmsRoleHolder(int); + method @Nullable @RequiresPermission(android.Manifest.permission.SET_PREFERRED_APPLICATIONS) public boolean setBrowserRoleHolder(@Nullable String, int); + } + +} + diff --git a/framework-s/api/module-lib-removed.txt b/framework-s/api/module-lib-removed.txt new file mode 100644 index 000000000..d802177e2 --- /dev/null +++ b/framework-s/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/framework-s/api/removed.txt b/framework-s/api/removed.txt new file mode 100644 index 000000000..d802177e2 --- /dev/null +++ b/framework-s/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/framework-s/api/system-current.txt b/framework-s/api/system-current.txt new file mode 100644 index 000000000..6778d4826 --- /dev/null +++ b/framework-s/api/system-current.txt @@ -0,0 +1,43 @@ +// Signature format: 2.0 +package android.app.role { + + public interface OnRoleHoldersChangedListener { + method public void onRoleHoldersChanged(@NonNull String, @NonNull android.os.UserHandle); + } + + @Deprecated public abstract class RoleControllerService extends android.app.Service { + ctor @Deprecated public RoleControllerService(); + method @Deprecated @WorkerThread public abstract boolean onAddRoleHolder(@NonNull String, @NonNull String, int); + method @Deprecated @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); + method @Deprecated @WorkerThread public abstract boolean onClearRoleHolders(@NonNull String, int); + method @Deprecated @WorkerThread public abstract boolean onGrantDefaultRoles(); + method @Deprecated public abstract boolean onIsApplicationQualifiedForRole(@NonNull String, @NonNull String); + method @Deprecated public boolean onIsApplicationVisibleForRole(@NonNull String, @NonNull String); + method @Deprecated public abstract boolean onIsRoleVisible(@NonNull String); + method @Deprecated @WorkerThread public abstract boolean onRemoveRoleHolder(@NonNull String, @NonNull String, int); + field @Deprecated public static final String SERVICE_INTERFACE = "android.app.role.RoleControllerService"; + } + + public class RoleFrameworkInitializer { + method public static void registerServiceWrappers(); + } + + public final class RoleManager { + method @RequiresPermission(android.Manifest.permission.OBSERVE_ROLE_HOLDERS) public void addOnRoleHoldersChangedListenerAsUser(@NonNull java.util.concurrent.Executor, @NonNull android.app.role.OnRoleHoldersChangedListener, @NonNull android.os.UserHandle); + method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void addRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); + method @Deprecated @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean addRoleHolderFromController(@NonNull String, @NonNull String); + method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void clearRoleHoldersAsUser(@NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); + method @Deprecated @NonNull @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public java.util.List<java.lang.String> getHeldRolesFromController(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public java.util.List<java.lang.String> getRoleHolders(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public java.util.List<java.lang.String> getRoleHoldersAsUser(@NonNull String, @NonNull android.os.UserHandle); + method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void isApplicationVisibleForRole(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); + method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void isRoleVisible(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); + method @RequiresPermission(android.Manifest.permission.OBSERVE_ROLE_HOLDERS) public void removeOnRoleHoldersChangedListenerAsUser(@NonNull android.app.role.OnRoleHoldersChangedListener, @NonNull android.os.UserHandle); + method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void removeRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); + method @Deprecated @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean removeRoleHolderFromController(@NonNull String, @NonNull String); + method @Deprecated @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public void setRoleNamesFromController(@NonNull java.util.List<java.lang.String>); + field public static final int MANAGE_HOLDERS_FLAG_DONT_KILL_APP = 1; // 0x1 + } + +} + diff --git a/framework-s/api/system-removed.txt b/framework-s/api/system-removed.txt new file mode 100644 index 000000000..d802177e2 --- /dev/null +++ b/framework-s/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/framework-s/java/android/app/role/IOnRoleHoldersChangedListener.aidl b/framework-s/java/android/app/role/IOnRoleHoldersChangedListener.aidl new file mode 100644 index 000000000..6cf961fad --- /dev/null +++ b/framework-s/java/android/app/role/IOnRoleHoldersChangedListener.aidl @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * @hide + */ +oneway interface IOnRoleHoldersChangedListener { + + void onRoleHoldersChanged(String roleName, int userId); +} diff --git a/framework-s/java/android/app/role/IRoleController.aidl b/framework-s/java/android/app/role/IRoleController.aidl new file mode 100644 index 000000000..8a43d7fa9 --- /dev/null +++ b/framework-s/java/android/app/role/IRoleController.aidl @@ -0,0 +1,43 @@ +/* + * 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.os.RemoteCallback; + +/** + * @hide + */ +oneway interface IRoleController { + + void grantDefaultRoles(in RemoteCallback callback); + + void onAddRoleHolder(in String roleName, in String packageName, int flags, + in RemoteCallback callback); + + void onRemoveRoleHolder(in String roleName, in String packageName, int flags, + in RemoteCallback callback); + + void onClearRoleHolders(in String roleName, int flags, in RemoteCallback callback); + + void isApplicationQualifiedForRole(in String roleName, in String packageName, + in RemoteCallback callback); + + void isApplicationVisibleForRole(in String roleName, in String packageName, + in RemoteCallback callback); + + void isRoleVisible(in String roleName, in RemoteCallback callback); +} diff --git a/framework-s/java/android/app/role/IRoleManager.aidl b/framework-s/java/android/app/role/IRoleManager.aidl new file mode 100644 index 000000000..5fc25f042 --- /dev/null +++ b/framework-s/java/android/app/role/IRoleManager.aidl @@ -0,0 +1,61 @@ +/* + * 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.app.role.IOnRoleHoldersChangedListener; +import android.os.Bundle; +import android.os.RemoteCallback; + +/** + * @hide + */ +interface IRoleManager { + + boolean isRoleAvailable(in String roleName); + + boolean isRoleHeld(in String roleName, in String packageName); + + List<String> getRoleHoldersAsUser(in String roleName, int userId); + + void addRoleHolderAsUser(in String roleName, in String packageName, int flags, int userId, + in RemoteCallback callback); + + void removeRoleHolderAsUser(in String roleName, in String packageName, int flags, int userId, + in RemoteCallback callback); + + void clearRoleHoldersAsUser(in String roleName, int flags, int userId, + in RemoteCallback callback); + + void addOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener, int userId); + + void removeOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener, + int userId); + + void setRoleNamesFromController(in List<String> roleNames); + + boolean addRoleHolderFromController(in String roleName, in String packageName); + + boolean removeRoleHolderFromController(in String roleName, in String packageName); + + List<String> getHeldRolesFromController(in String packageName); + + String getBrowserRoleHolder(int userId); + + boolean setBrowserRoleHolder(String packageName, int userId); + + String getSmsRoleHolder(int userId); +} diff --git a/framework-s/java/android/app/role/OnRoleHoldersChangedListener.java b/framework-s/java/android/app/role/OnRoleHoldersChangedListener.java new file mode 100644 index 000000000..5958debc8 --- /dev/null +++ b/framework-s/java/android/app/role/OnRoleHoldersChangedListener.java @@ -0,0 +1,38 @@ +/* + * 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.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.UserHandle; + +/** + * Listener for role holder changes. + * + * @hide + */ +@SystemApi +public interface OnRoleHoldersChangedListener { + + /** + * Called when the holders of roles are changed. + * + * @param roleName the name of the role whose holders are changed + * @param user the user for this role holder change + */ + void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user); +} diff --git a/framework-s/java/android/app/role/RoleControllerManager.java b/framework-s/java/android/app/role/RoleControllerManager.java new file mode 100644 index 000000000..93a7ae0c8 --- /dev/null +++ b/framework-s/java/android/app/role/RoleControllerManager.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2019 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.NonNull; +import android.annotation.RequiresPermission; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteCallback; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.infra.ServiceConnector; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Interface for communicating with the role controller. + * + * @hide + */ +public class RoleControllerManager { + + private static final String LOG_TAG = RoleControllerManager.class.getSimpleName(); + + private static final long REQUEST_TIMEOUT_MILLIS = 15 * 1000; + + private static volatile ComponentName sRemoteServiceComponentName; + + private static final Object sRemoteServicesLock = new Object(); + + /** + * Global remote services (per user) used by all {@link RoleControllerManager managers}. + */ + @GuardedBy("sRemoteServicesLock") + private static final SparseArray<ServiceConnector<IRoleController>> sRemoteServices = + new SparseArray<>(); + + @NonNull + private final ServiceConnector<IRoleController> mRemoteService; + + /** + * Initialize the remote service component name once so that we can avoid acquiring the + * PackageManagerService lock in constructor. + * + * @see #createWithInitializedRemoteServiceComponentName(Handler, Context) + * + * @hide + */ + public static void initializeRemoteServiceComponentName(@NonNull Context context) { + sRemoteServiceComponentName = getRemoteServiceComponentName(context); + } + + /** + * Create a {@link RoleControllerManager} instance with the initialized remote service component + * name so that we can avoid acquiring the PackageManagerService lock in constructor. + * + * @see #initializeRemoteServiceComponentName(Context) + * + * @hide + */ + @NonNull + public static RoleControllerManager createWithInitializedRemoteServiceComponentName( + @NonNull Handler handler, @NonNull Context context) { + return new RoleControllerManager(sRemoteServiceComponentName, handler, context); + } + + private RoleControllerManager(@NonNull ComponentName remoteServiceComponentName, + @NonNull Handler handler, @NonNull Context context) { + synchronized (sRemoteServicesLock) { + int userId = context.getUser().getIdentifier(); + ServiceConnector<IRoleController> remoteService = sRemoteServices.get(userId); + if (remoteService == null) { + remoteService = new ServiceConnector.Impl<IRoleController>(context, + new Intent(RoleControllerService.SERVICE_INTERFACE) + .setComponent(remoteServiceComponentName), + 0 /* bindingFlags */, userId, IRoleController.Stub::asInterface) { + + @Override + protected Handler getJobHandler() { + return handler; + } + }; + sRemoteServices.put(userId, remoteService); + } + mRemoteService = remoteService; + } + } + + /** + * @hide + */ + public RoleControllerManager(@NonNull Context context) { + this(getRemoteServiceComponentName(context), new Handler(Looper.getMainLooper()), context); + } + + @NonNull + private static ComponentName getRemoteServiceComponentName(@NonNull Context context) { + Intent intent = new Intent(RoleControllerService.SERVICE_INTERFACE); + PackageManager packageManager = context.getPackageManager(); + intent.setPackage(packageManager.getPermissionControllerPackageName()); + ServiceInfo serviceInfo = packageManager.resolveService(intent, 0).serviceInfo; + return new ComponentName(serviceInfo.packageName, serviceInfo.name); + } + + /** + * @see RoleControllerService#onGrantDefaultRoles() + * + * @hide + */ + public void grantDefaultRoles(@NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<Boolean> callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.grantDefaultRoles(new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "grantDefaultRoles", executor, callback); + } + + /** + * @see RoleControllerService#onAddRoleHolder(String, String, int) + * + * @hide + */ + public void onAddRoleHolder(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.onAddRoleHolder(roleName, packageName, flags, + new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "onAddRoleHolder", callback); + } + + /** + * @see RoleControllerService#onRemoveRoleHolder(String, String, int) + * + * @hide + */ + public void onRemoveRoleHolder(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.onRemoveRoleHolder(roleName, packageName, flags, + new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "onRemoveRoleHolder", callback); + } + + /** + * @see RoleControllerService#onClearRoleHolders(String, int) + * + * @hide + */ + public void onClearRoleHolders(@NonNull String roleName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.onClearRoleHolders(roleName, flags, + new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "onClearRoleHolders", callback); + } + + /** + * @see RoleControllerService#onIsApplicationVisibleForRole(String, String) + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS) + public void isApplicationVisibleForRole(@NonNull String roleName, @NonNull String packageName, + @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.isApplicationVisibleForRole(roleName, packageName, + new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "isApplicationVisibleForRole", executor, callback); + } + + /** + * @see RoleControllerService#onIsRoleVisible(String) + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS) + public void isRoleVisible(@NonNull String roleName, + @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) { + AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> { + AndroidFuture<Bundle> future = new AndroidFuture<>(); + service.isRoleVisible(roleName, new RemoteCallback(future::complete)); + return future; + }); + propagateCallback(operation, "isRoleVisible", executor, callback); + } + + private void propagateCallback(AndroidFuture<Bundle> operation, String opName, + @CallbackExecutor @NonNull Executor executor, + Consumer<Boolean> destination) { + operation.orTimeout(REQUEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .whenComplete((res, err) -> executor.execute(() -> { + final long token = Binder.clearCallingIdentity(); + try { + if (err != null) { + Log.e(LOG_TAG, "Error calling " + opName + "()", err); + destination.accept(false); + } else { + destination.accept(res != null); + } + } finally { + Binder.restoreCallingIdentity(token); + } + })); + } + + private void propagateCallback(AndroidFuture<Bundle> operation, String opName, + RemoteCallback destination) { + operation.orTimeout(REQUEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .whenComplete((res, err) -> { + final long token = Binder.clearCallingIdentity(); + try { + if (err != null) { + Log.e(LOG_TAG, "Error calling " + opName + "()", err); + destination.sendResult(null); + } else { + destination.sendResult(res); + } + } finally { + Binder.restoreCallingIdentity(token); + } + }); + } +} diff --git a/framework-s/java/android/app/role/RoleControllerService.java b/framework-s/java/android/app/role/RoleControllerService.java new file mode 100644 index 000000000..cf7872913 --- /dev/null +++ b/framework-s/java/android/app/role/RoleControllerService.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2019 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.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.WorkerThread; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteCallback; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Abstract base class for the role controller service. + * <p> + * Subclass should implement the business logic for role management, including enforcing role + * requirements and granting or revoking relevant privileges of roles. This class can only be + * implemented by the permission controller app which is registered in {@code PackageManager}. + * + * @deprecated The role controller service is an internal implementation detail inside role, and it + * may be replaced by other mechanisms in the future and no longer be called. + * + * @hide + */ +@Deprecated +@SystemApi +public abstract class RoleControllerService extends Service { + + /** + * The {@link Intent} that must be declared as handled by the service. + */ + public static final String SERVICE_INTERFACE = "android.app.role.RoleControllerService"; + + private HandlerThread mWorkerThread; + private Handler mWorkerHandler; + + @Override + public void onCreate() { + super.onCreate(); + + mWorkerThread = new HandlerThread(RoleControllerService.class.getSimpleName()); + mWorkerThread.start(); + mWorkerHandler = new Handler(mWorkerThread.getLooper()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mWorkerThread.quitSafely(); + } + + @Nullable + @Override + public final IBinder onBind(@Nullable Intent intent) { + return new IRoleController.Stub() { + + @Override + public void grantDefaultRoles(RemoteCallback callback) { + enforceCallerSystemUid("grantDefaultRoles"); + + Objects.requireNonNull(callback, "callback cannot be null"); + + mWorkerHandler.post(() -> RoleControllerService.this.grantDefaultRoles(callback)); + } + + @Override + public void onAddRoleHolder(String roleName, String packageName, int flags, + RemoteCallback callback) { + enforceCallerSystemUid("onAddRoleHolder"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, + "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + mWorkerHandler.post(() -> RoleControllerService.this.onAddRoleHolder(roleName, + packageName, flags, callback)); + } + + @Override + public void onRemoveRoleHolder(String roleName, String packageName, int flags, + RemoteCallback callback) { + enforceCallerSystemUid("onRemoveRoleHolder"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, + "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + mWorkerHandler.post(() -> RoleControllerService.this.onRemoveRoleHolder(roleName, + packageName, flags, callback)); + } + + @Override + public void onClearRoleHolders(String roleName, int flags, RemoteCallback callback) { + enforceCallerSystemUid("onClearRoleHolders"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + mWorkerHandler.post(() -> RoleControllerService.this.onClearRoleHolders(roleName, + flags, callback)); + } + + private void enforceCallerSystemUid(@NonNull String methodName) { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Only the system process can call " + methodName + + "()"); + } + } + + @Override + public void isApplicationQualifiedForRole(String roleName, String packageName, + RemoteCallback callback) { + enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, + "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + boolean qualified = onIsApplicationQualifiedForRole(roleName, packageName); + callback.sendResult(qualified ? Bundle.EMPTY : null); + } + + @Override + public void isApplicationVisibleForRole(String roleName, String packageName, + RemoteCallback callback) { + enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, + "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + boolean visible = onIsApplicationVisibleForRole(roleName, packageName); + callback.sendResult(visible ? Bundle.EMPTY : null); + } + + @Override + public void isRoleVisible(String roleName, RemoteCallback callback) { + enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + boolean visible = onIsRoleVisible(roleName); + callback.sendResult(visible ? Bundle.EMPTY : null); + } + }; + } + + private void grantDefaultRoles(@NonNull RemoteCallback callback) { + boolean successful = onGrantDefaultRoles(); + callback.sendResult(successful ? Bundle.EMPTY : null); + } + + private void onAddRoleHolder(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + boolean successful = onAddRoleHolder(roleName, packageName, flags); + callback.sendResult(successful ? Bundle.EMPTY : null); + } + + private void onRemoveRoleHolder(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + boolean successful = onRemoveRoleHolder(roleName, packageName, flags); + callback.sendResult(successful ? Bundle.EMPTY : null); + } + + private void onClearRoleHolders(@NonNull String roleName, + @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) { + boolean successful = onClearRoleHolders(roleName, flags); + callback.sendResult(successful ? Bundle.EMPTY : null); + } + + /** + * Called by system to grant default permissions and roles. + * <p> + * This is typically when creating a new user or upgrading either system or + * permission controller package + * + * @return whether this call was successful + */ + @WorkerThread + public abstract boolean onGrantDefaultRoles(); + + /** + * Add a specific application to the holders of a role. If the role is exclusive, the previous + * holder will be replaced. + * <p> + * Implementation should enforce the role requirements and grant or revoke the relevant + * privileges of roles. + * + * @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 + * + * @return whether this call was successful + * + * @see RoleManager#addRoleHolderAsUser(String, String, int, UserHandle, Executor, + * RemoteCallback) + */ + @WorkerThread + public abstract boolean onAddRoleHolder(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags); + + /** + * Remove a specific application from the holders of a role. + * + * @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 + * + * @return whether this call was successful + * + * @see RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, Executor, + * RemoteCallback) + */ + @WorkerThread + public abstract boolean onRemoveRoleHolder(@NonNull String roleName, + @NonNull String packageName, @RoleManager.ManageHoldersFlags int flags); + + /** + * Remove all holders of a role. + * + * @param roleName the name of the role to remove role holders for + * @param flags optional behavior flags + * + * @return whether this call was successful + * + * @see RoleManager#clearRoleHoldersAsUser(String, int, UserHandle, Executor, RemoteCallback) + */ + @WorkerThread + public abstract boolean onClearRoleHolders(@NonNull String roleName, + @RoleManager.ManageHoldersFlags int flags); + + /** + * Check whether an application is qualified for a role. + * + * @param roleName name of the role to check for + * @param packageName package name of the application to check for + * + * @return whether the application is qualified for the role + * + * @deprecated Implement {@link #onIsApplicationVisibleForRole(String, String)} instead. + */ + @Deprecated + public abstract boolean onIsApplicationQualifiedForRole(@NonNull String roleName, + @NonNull String packageName); + + /** + * Check whether an application is visible for a role. + * + * While an application can be qualified for a role, it can still stay hidden from user (thus + * not visible). If an application is visible for a role, we may show things related to the role + * for it, e.g. showing an entry pointing to the role settings in its application info page. + * + * @param roleName name of the role to check for + * @param packageName package name of the application to check for + * + * @return whether the application is visible for the role + */ + public boolean onIsApplicationVisibleForRole(@NonNull String roleName, + @NonNull String packageName) { + return onIsApplicationQualifiedForRole(roleName, packageName); + } + + /** + * Check whether a role should be visible to user. + * + * @param roleName name of the role to check for + * + * @return whether the role should be visible to user + */ + public abstract boolean onIsRoleVisible(@NonNull String roleName); +} diff --git a/framework-s/java/android/app/role/RoleFrameworkInitializer.java b/framework-s/java/android/app/role/RoleFrameworkInitializer.java new file mode 100644 index 000000000..7a97770ec --- /dev/null +++ b/framework-s/java/android/app/role/RoleFrameworkInitializer.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 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.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class holding initialization code for role in the permission module. + * + * @hide + */ +@SystemApi +public class RoleFrameworkInitializer { + private RoleFrameworkInitializer() {} + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers + * {@link RoleManager} to {@link Context}, so that {@link Context#getSystemService} can return + * it. + * + * <p>If this is called from other places, it throws a {@link IllegalStateException). + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService(Context.ROLE_SERVICE, RoleManager.class, + (context, serviceBinder) -> new RoleManager(context, + IRoleManager.Stub.asInterface(serviceBinder))); + } +} diff --git a/framework-s/java/android/app/role/RoleManager.java b/framework-s/java/android/app/role/RoleManager.java new file mode 100644 index 000000000..ceccc4cfc --- /dev/null +++ b/framework-s/java/android/app/role/RoleManager.java @@ -0,0 +1,773 @@ +/* + * 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.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.UserHandle; +import android.util.ArrayMap; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +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 + 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(); + + @GuardedBy("mRoleControllerManagerLock") + @Nullable + private RoleControllerManager mRoleControllerManager; + private final Object mRoleControllerManagerLock = new Object(); + + /** + * Create a new instance of this class. + * + * @param context the {@link Context} + * @param service the {@link IRoleManager} service + * + * @hide + */ + public RoleManager(@NonNull Context context, @NonNull IRoleManager service) { + mContext = context; + mService = 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 + 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 + 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 + 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 + 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 + 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; + final 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 + 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 + 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 + * + * @deprecated This is only usable by the role controller service, which is an internal + * implementation detail inside role. + * + * @hide + */ + @Deprecated + @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) + @SystemApi + 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) + * + * @deprecated This is only usable by the role controller service, which is an internal + * implementation detail inside role. + * + * @hide + */ + @Deprecated + @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) + @SystemApi + 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) + * + * @deprecated This is only usable by the role controller service, which is an internal + * implementation detail inside role. + * + * @hide + */ + @Deprecated + @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) + @SystemApi + 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 + * + * @deprecated This is only usable by the role controller service, which is an internal + * implementation detail inside role. + * + * @hide + */ + @Deprecated + @NonNull + @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) + @SystemApi + 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(); + } + } + + /** + * Get the role holder of {@link #ROLE_BROWSER} without requiring + * {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as in + * {@link android.content.pm.PackageManager#getDefaultBrowserPackageNameAsUser(int)} + * + * @param userId the user ID + * @return the package name of the default browser, or {@code null} if none + * + * @hide + */ + @Nullable + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public String getBrowserRoleHolder(@UserIdInt int userId) { + try { + return mService.getBrowserRoleHolder(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the role holder of {@link #ROLE_BROWSER} requiring + * {@link Manifest.permission.SET_PREFERRED_APPLICATIONS} instead of + * {@link Manifest.permission#MANAGE_ROLE_HOLDERS}, as in + * {@link android.content.pm.PackageManager#setDefaultBrowserPackageNameAsUser(String, int)} + * + * @param packageName the package name of the default browser, or {@code null} if none + * @param userId the user ID + * @return whether the default browser was set successfully + * + * @hide + */ + @Nullable + @RequiresPermission(Manifest.permission.SET_PREFERRED_APPLICATIONS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public boolean setBrowserRoleHolder(@Nullable String packageName, @UserIdInt int userId) { + try { + return mService.setBrowserRoleHolder(packageName, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Allows getting the role holder for {@link #ROLE_SMS} without requiring + * {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as in + * {@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 none + * + * @hide + */ + @Nullable + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public String getSmsRoleHolder(@UserIdInt int userId) { + try { + return mService.getSmsRoleHolder(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Check whether a role should be visible to user. + * + * @param roleName name of the role to check for + * @param executor the executor to execute callback on + * @param callback the callback to receive whether the role should be visible to user + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS) + @SystemApi + public void isRoleVisible(@NonNull String roleName, + @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) { + getRoleControllerManager().isRoleVisible(roleName, executor, callback); + } + + /** + * Check whether an application is visible for a role. + * + * While an application can be qualified for a role, it can still stay hidden from user (thus + * not visible). If an application is visible for a role, we may show things related to the role + * for it, e.g. showing an entry pointing to the role settings in its application info page. + * + * @param roleName the name of the role to check for + * @param packageName the package name of the application to check for + * @param executor the executor to execute callback on + * @param callback the callback to receive whether the application is visible for the role + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS) + @SystemApi + public void isApplicationVisibleForRole(@NonNull String roleName, @NonNull String packageName, + @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) { + getRoleControllerManager().isApplicationVisibleForRole(roleName, packageName, executor, + callback); + } + + @NonNull + private RoleControllerManager getRoleControllerManager() { + synchronized (mRoleControllerManagerLock) { + if (mRoleControllerManager == null) { + mRoleControllerManager = new RoleControllerManager(mContext); + } + return mRoleControllerManager; + } + } + + 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) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> + mListener.onRoleHoldersChanged(roleName, UserHandle.of(userId))); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } +} diff --git a/framework-s/java/android/app/role/TEST_MAPPING b/framework-s/java/android/app/role/TEST_MAPPING new file mode 100644 index 000000000..f8f140dd7 --- /dev/null +++ b/framework-s/java/android/app/role/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "CtsRoleTestCases", + "options": [ + { + "include-filter": "android.app.role.cts.RoleManagerTest" + } + ] + } + ] +} diff --git a/framework/Android.bp b/framework/Android.bp index 1a2d5d541..52a61674a 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -19,6 +19,7 @@ filegroup { "java/**/*.aidl", ], path: "java", + visibility: ["//frameworks/base"], } java_sdk_library { @@ -39,6 +40,7 @@ java_sdk_library { "com.android.permission", "test_com.android.permission", ], + min_sdk_version: "30", permitted_packages: [ "android.permission", "android.app.role", diff --git a/jarjar-rules.txt b/jarjar-rules.txt new file mode 100644 index 000000000..4729ed13d --- /dev/null +++ b/jarjar-rules.txt @@ -0,0 +1,5 @@ +rule android.os.HandlerExecutor com.android.permission.jarjar.@0 +rule android.util.IndentingPrintWriter com.android.permission.jarjar.@0 +rule com.android.internal.** com.android.permission.jarjar.@0 +rule com.android.modules.** com.android.permission.jarjar.@0 +rule com.android.role.*Proto com.android.permission.jarjar.@0 diff --git a/service/Android.bp b/service/Android.bp index cc6f2019a..d0fc5b9d7 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -18,6 +18,57 @@ filegroup { "java/**/*.java", ], path: "java", + visibility: ["//frameworks/base/services"], +} + +filegroup { + name: "service-permission-protos", + srcs: [ + "proto/**/*.proto", + ], + visibility: ["//frameworks/base"], +} + +gensrcs { + name: "service-permission-javastream-protos", + depfile: true, + + tools: [ + "aprotoc", + "protoc-gen-javastream", + "soong_zip", + ], + + cmd: "mkdir -p $(genDir)/$(in) " + + "&& $(location aprotoc) " + + " --plugin=$(location protoc-gen-javastream) " + + " --dependency_out=$(depfile) " + + " --javastream_out=$(genDir)/$(in) " + + " -Iexternal/protobuf/src " + + " -I . " + + " $(in) " + + "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)", + + srcs: [ + ":service-permission-protos", + ], + output_extension: "srcjar", +} + +java_library { + name: "service-permission-shared", + srcs: [":service-permission-shared-srcs"], + libs: [ + "framework-annotations-lib", + "framework-permission-s-shared", + ], + apex_available: [ + "com.android.permission", + "test_com.android.permission", + ], + installable: false, + min_sdk_version: "30", + sdk_version: "system_server_current", } java_sdk_library { @@ -31,10 +82,20 @@ java_sdk_library { ], srcs: [ ":service-permission-sources", + ":service-permission-javastream-protos", ], libs: [ "framework-permission", + "framework-permission-s.impl", + "framework-permission-s-shared", + ], + static_libs: [ + "modules-utils-os", + "service-permission-shared", ], + jarjar_rules: ":permission-jarjar-rules", + min_sdk_version: "30", + sdk_version: "system_server_current", apex_available: [ "com.android.permission", "test_com.android.permission", diff --git a/service/api/system-server-current.txt b/service/api/system-server-current.txt index c76cc3275..b1869c2c7 100644 --- a/service/api/system-server-current.txt +++ b/service/api/system-server-current.txt @@ -26,6 +26,14 @@ package com.android.permission.persistence { } +package com.android.role { + + public interface RoleManagerLocal { + method @NonNull public java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getRolesAndHolders(int); + } + +} + package com.android.role.persistence { public interface RolesPersistence { diff --git a/service/java/com/android/permission/compat/UserHandleCompat.java b/service/java/com/android/permission/compat/UserHandleCompat.java new file mode 100644 index 000000000..7c711d301 --- /dev/null +++ b/service/java/com/android/permission/compat/UserHandleCompat.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 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.permission.compat; + +import android.annotation.UserIdInt; +import android.os.UserHandle; + +/** + * Helper for accessing features in {@link UserHandle}. + */ +public final class UserHandleCompat { + /** + * A user ID to indicate all users on the device. + */ + public static final int USER_ALL = UserHandle.ALL.getIdentifier(); + + /** + * A user ID to indicate the "system" user of the device. + */ + public static final int USER_SYSTEM = UserHandle.SYSTEM.getIdentifier(); + + private UserHandleCompat() {} + + /** + * Get the user ID of a given UID. + * + * @param uid the UID + * @return the user ID + */ + @UserIdInt + public static int getUserId(int uid) { + return UserHandle.getUserHandleForUid(uid).getIdentifier(); + } +} diff --git a/service/java/com/android/permission/compat/package-info.java b/service/java/com/android/permission/compat/package-info.java new file mode 100644 index 000000000..c89cc8eab --- /dev/null +++ b/service/java/com/android/permission/compat/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @hide + * TODO(b/146466118) remove this javadoc tag + */ +@android.annotation.Hide +package com.android.permission.compat; diff --git a/service/java/com/android/permission/util/ArrayUtils.java b/service/java/com/android/permission/util/ArrayUtils.java new file mode 100644 index 000000000..5d5cd7820 --- /dev/null +++ b/service/java/com/android/permission/util/ArrayUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 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.permission.util; + +import android.annotation.Nullable; + +import java.util.Objects; + +/** + * Array utilities. + */ +public final class ArrayUtils { + private ArrayUtils() {} + + /** + * @see java.util.List#contains(Object) + */ + public static <T> boolean contains(@Nullable T[] array, T value) { + return indexOf(array, value) != -1; + } + + /** + * Get the first element of an array, or {@code null} if none. + * + * @param array the array + * @param <T> the type of the elements of the array + * @return first element of an array, or {@code null} if none + */ + public static <T> T firstOrNull(@Nullable T[] array) { + return !isEmpty(array) ? array[0] : null; + } + + /** + * @see java.util.List#indexOf(Object) + */ + public static <T> int indexOf(@Nullable T[] array, T value) { + if (array == null) { + return -1; + } + final int length = array.length; + for (int i = 0; i < length; i++) { + final T element = array[i]; + if (Objects.equals(element, value)) { + return i; + } + } + return -1; + } + + /** + * @see java.util.List#isEmpty() + */ + public static <T> boolean isEmpty(@Nullable T[] array) { + return array == null || array.length == 0; + } +} diff --git a/service/java/com/android/permission/util/BackgroundThread.java b/service/java/com/android/permission/util/BackgroundThread.java new file mode 100644 index 000000000..7308eec98 --- /dev/null +++ b/service/java/com/android/permission/util/BackgroundThread.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 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.permission.util; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; + +import com.android.internal.annotations.GuardedBy; + +import java.util.concurrent.Executor; + +/** + * Shared singleton background thread. + */ +public class BackgroundThread extends HandlerThread { + private static final Object sLock = new Object(); + + @GuardedBy("sLock") + private static BackgroundThread sInstance; + @GuardedBy("sLock") + private static Handler sHandler; + @GuardedBy("sLock") + private static Executor sExecutor; + + private BackgroundThread() { + super(BackgroundThread.class.getName()); + } + + @GuardedBy("sLock") + private static void ensureInstanceLocked() { + if (sInstance == null) { + sInstance = new BackgroundThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + sExecutor = new HandlerExecutor(sHandler); + } + } + + /** + * Get the singleton instance of thi class. + * + * @return the singleton instance of thi class + */ + @NonNull + public static BackgroundThread get() { + synchronized (sLock) { + ensureInstanceLocked(); + return sInstance; + } + } + + /** + * Get the {@link Handler} for this thread. + * + * @return the {@link Handler} for this thread. + */ + @NonNull + public static Handler getHandler() { + synchronized (sLock) { + ensureInstanceLocked(); + return sHandler; + } + } + + /** + * Get the {@link Executor} for this thread. + * + * @return the {@link Executor} for this thread. + */ + @NonNull + public static Executor getExecutor() { + synchronized (sLock) { + ensureInstanceLocked(); + return sExecutor; + } + } +} diff --git a/service/java/com/android/permission/util/CollectionUtils.java b/service/java/com/android/permission/util/CollectionUtils.java new file mode 100644 index 000000000..ea4952404 --- /dev/null +++ b/service/java/com/android/permission/util/CollectionUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 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.permission.util; + +import android.annotation.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * {@link Collection} utilities. + */ +public class CollectionUtils { + private CollectionUtils() {} + + /** + * Get the first element of a {@link List}, or {@code null} if none. + * + * @param list the {@link List}, or {@code null} + * @param <E> the element type of the {@link List} + * @return the first element of the {@link List}, or {@code 0} if none + */ + @Nullable + public static <E> E firstOrNull(@Nullable List<E> list) { + return !isEmpty(list) ? list.get(0) : null; + } + + /** + * Check whether a {@link Collection} is empty or {@code null}. + * + * @param collection the {@link Collection}, or {@code null} + * @return whether the {@link Collection} is empty or {@code null} + */ + public static boolean isEmpty(@Nullable Collection<?> collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Get the size of a {@link Collection}, or {@code 0} if {@code null}. + * + * @param collection the {@link Collection}, or {@code null} + * @return the size of the {@link Collection}, or {@code 0} if {@code null} + */ + public static int size(@Nullable Collection<?> collection) { + return collection != null ? collection.size() : 0; + } + + /** + * Get the size of a {@link Map}, or {@code 0} if {@code null}. + * + * @param collection the {@link Map}, or {@code null} + * @return the size of the {@link Map}, or {@code 0} if {@code null} + */ + public static int size(@Nullable Map<?, ?> collection) { + return collection != null ? collection.size() : 0; + } +} diff --git a/service/java/com/android/permission/util/ForegroundThread.java b/service/java/com/android/permission/util/ForegroundThread.java new file mode 100644 index 000000000..cd6f60570 --- /dev/null +++ b/service/java/com/android/permission/util/ForegroundThread.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 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.permission.util; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; + +import com.android.internal.annotations.GuardedBy; + +import java.util.concurrent.Executor; + +/** + * Shared singleton foreground thread. + */ +public class ForegroundThread extends HandlerThread { + private static final Object sLock = new Object(); + + @GuardedBy("sLock") + private static ForegroundThread sInstance; + @GuardedBy("sLock") + private static Handler sHandler; + @GuardedBy("sLock") + private static Executor sExecutor; + + private ForegroundThread() { + super(ForegroundThread.class.getName()); + } + + @GuardedBy("sLock") + private static void ensureInstanceLocked() { + if (sInstance == null) { + sInstance = new ForegroundThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + sExecutor = new HandlerExecutor(sHandler); + } + } + + /** + * Get the singleton instance of thi class. + * + * @return the singleton instance of thi class + */ + @NonNull + public static ForegroundThread get() { + synchronized (sLock) { + ensureInstanceLocked(); + return sInstance; + } + } + + /** + * Get the {@link Handler} for this thread. + * + * @return the {@link Handler} for this thread. + */ + @NonNull + public static Handler getHandler() { + synchronized (sLock) { + ensureInstanceLocked(); + return sHandler; + } + } + + /** + * Get the {@link Executor} for this thread. + * + * @return the {@link Executor} for this thread. + */ + @NonNull + public static Executor getExecutor() { + synchronized (sLock) { + ensureInstanceLocked(); + return sExecutor; + } + } +} diff --git a/service/java/com/android/permission/util/ThrottledRunnable.java b/service/java/com/android/permission/util/ThrottledRunnable.java new file mode 100644 index 000000000..ba1c3939f --- /dev/null +++ b/service/java/com/android/permission/util/ThrottledRunnable.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 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.permission.util; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.SystemClock; + +import com.android.internal.annotations.GuardedBy; + +/** + * A throttled runnable that can wrap around a runnable and throttle calls to its run(). + * + * The throttling logic makes sure that the original runnable will be called only after the + * specified interval passes since the last actual call. The first call in a while (after the + * specified interval passes since the last actual call) will always result in the original runnable + * being called immediately, and then subsequent calls will start to be throttled. It is guaranteed + * that any call to this throttled runnable will always result in the original runnable being called + * afterwards, within the specified interval. + */ +public class ThrottledRunnable implements Runnable { + + @NonNull + private final Handler mHandler; + private final long mIntervalMillis; + @NonNull + private final Runnable mRunnable; + + @NonNull + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private long mScheduledUptimeMillis; + + public ThrottledRunnable(@NonNull Handler handler, long intervalMillis, + @NonNull Runnable runnable) { + mHandler = handler; + mIntervalMillis = intervalMillis; + mRunnable = runnable; + } + + @Override + public void run() { + synchronized (mLock) { + if (mHandler.hasCallbacks(mRunnable)) { + // We have a scheduled runnable. + return; + } + long currentUptimeMillis = SystemClock.uptimeMillis(); + if (mScheduledUptimeMillis == 0 + || currentUptimeMillis > mScheduledUptimeMillis + mIntervalMillis) { + // First time in a while, schedule immediately. + mScheduledUptimeMillis = currentUptimeMillis; + } else { + // We were scheduled not long ago, so schedule with delay for throttling. + mScheduledUptimeMillis = mScheduledUptimeMillis + mIntervalMillis; + } + mHandler.postAtTime(mRunnable, mScheduledUptimeMillis); + } + } +} diff --git a/service/java/com/android/permission/util/package-info.java b/service/java/com/android/permission/util/package-info.java new file mode 100644 index 000000000..18fada534 --- /dev/null +++ b/service/java/com/android/permission/util/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @hide + * TODO(b/146466118) remove this javadoc tag + */ +@android.annotation.Hide +package com.android.permission.util; diff --git a/service/java/com/android/role/RoleManagerLocal.java b/service/java/com/android/role/RoleManagerLocal.java new file mode 100644 index 000000000..e243e2e0d --- /dev/null +++ b/service/java/com/android/role/RoleManagerLocal.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.role; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.annotation.UserIdInt; + +import java.util.Map; +import java.util.Set; + +/** + * Internal calls into {@link RoleService}. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public interface RoleManagerLocal { + /** + * Get all roles and their holders. + * + * @param userId The user to query to roles for + * + * @return The roles and their holders + */ + @NonNull + Map<String, Set<String>> getRolesAndHolders(@UserIdInt int userId); +} diff --git a/service/java/com/android/role/RoleService.java b/service/java/com/android/role/RoleService.java new file mode 100644 index 000000000..3cbd9a8d4 --- /dev/null +++ b/service/java/com/android/role/RoleService.java @@ -0,0 +1,734 @@ +/* + * 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.role; + +import android.Manifest; +import android.annotation.AnyThread; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.annotation.WorkerThread; +import android.app.AppOpsManager; +import android.app.role.IOnRoleHoldersChangedListener; +import android.app.role.IRoleManager; +import android.app.role.RoleControllerManager; +import android.app.role.RoleManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteCallback; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.util.Preconditions; +import com.android.internal.util.dump.DualDumpOutputStream; +import com.android.permission.compat.UserHandleCompat; +import com.android.permission.util.ArrayUtils; +import com.android.permission.util.CollectionUtils; +import com.android.permission.util.ForegroundThread; +import com.android.permission.util.ThrottledRunnable; +import com.android.server.LocalManagerRegistry; +import com.android.server.SystemService; +import com.android.server.role.RoleServicePlatformHelper; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Service for role management. + * + * @see RoleManager + */ +public class RoleService extends SystemService implements RoleUserState.Callback { + private static final String LOG_TAG = RoleService.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final long GRANT_DEFAULT_ROLES_INTERVAL_MILLIS = 1000; + + @NonNull + private final AppOpsManager mAppOpsManager; + @NonNull + private final UserManager mUserManager; + + @NonNull + private final Object mLock = new Object(); + + @NonNull + private final RoleServicePlatformHelper mPlatformHelper; + + /** + * Maps user id to its state. + */ + @GuardedBy("mLock") + @NonNull + private final SparseArray<RoleUserState> mUserStates = new SparseArray<>(); + + /** + * Maps user id to its controller. + */ + @GuardedBy("mLock") + @NonNull + private final SparseArray<RoleControllerManager> mControllers = new SparseArray<>(); + + /** + * Maps user id to its list of listeners. + */ + @GuardedBy("mLock") + @NonNull + private final SparseArray<RemoteCallbackList<IOnRoleHoldersChangedListener>> mListeners = + new SparseArray<>(); + + @NonNull + private final Handler mListenerHandler = ForegroundThread.getHandler(); + + /** + * Maps user id to its throttled runnable for granting default roles. + */ + @GuardedBy("mLock") + @NonNull + private final SparseArray<ThrottledRunnable> mGrantDefaultRolesThrottledRunnables = + new SparseArray<>(); + + public RoleService(@NonNull Context context) { + super(context); + + mPlatformHelper = LocalManagerRegistry.getManager(RoleServicePlatformHelper.class); + + RoleControllerManager.initializeRemoteServiceComponentName(context); + + mAppOpsManager = context.getSystemService(AppOpsManager.class); + mUserManager = context.getSystemService(UserManager.class); + + LocalManagerRegistry.addManager(RoleManagerLocal.class, new Local()); + + registerUserRemovedReceiver(); + } + + private void registerUserRemovedReceiver() { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + getContext().registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + if (TextUtils.equals(intent.getAction(), Intent.ACTION_USER_REMOVED)) { + int userId = intent.<UserHandle>getParcelableExtra(Intent.EXTRA_USER) + .getIdentifier(); + onRemoveUser(userId); + } + } + }, intentFilter, null, null); + } + + @Override + public void onStart() { + publishBinderService(Context.ROLE_SERVICE, new Stub()); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + getContext().registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int userId = UserHandleCompat.getUserId(intent.getIntExtra(Intent.EXTRA_UID, -1)); + if (DEBUG) { + Log.i(LOG_TAG, "Packages changed - re-running initial grants for user " + + userId); + } + if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction()) + && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + // Package is being upgraded - we're about to get ACTION_PACKAGE_ADDED + return; + } + maybeGrantDefaultRolesAsync(userId); + } + }, intentFilter, null, null); + } + + @Override + public void onUserStarting(@NonNull TargetUser user) { + maybeGrantDefaultRolesSync(user.getUserHandle().getIdentifier()); + } + + @MainThread + private void maybeGrantDefaultRolesSync(@UserIdInt int userId) { + AndroidFuture<Void> future = maybeGrantDefaultRolesInternal(userId); + try { + future.get(30, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.e(LOG_TAG, "Failed to grant default roles for user " + userId, e); + } + } + + private void maybeGrantDefaultRolesAsync(@UserIdInt int userId) { + ThrottledRunnable runnable; + synchronized (mLock) { + runnable = mGrantDefaultRolesThrottledRunnables.get(userId); + if (runnable == null) { + runnable = new ThrottledRunnable(ForegroundThread.getHandler(), + GRANT_DEFAULT_ROLES_INTERVAL_MILLIS, + () -> maybeGrantDefaultRolesInternal(userId)); + mGrantDefaultRolesThrottledRunnables.put(userId, runnable); + } + } + runnable.run(); + } + + @AnyThread + @NonNull + private AndroidFuture<Void> maybeGrantDefaultRolesInternal(@UserIdInt int userId) { + RoleUserState userState = getOrCreateUserState(userId); + String oldPackagesHash = userState.getPackagesHash(); + String newPackagesHash = mPlatformHelper.computePackageStateHash(userId); + if (Objects.equals(oldPackagesHash, newPackagesHash)) { + if (DEBUG) { + Log.i(LOG_TAG, "Already granted default roles for packages hash " + + newPackagesHash); + } + return AndroidFuture.completedFuture(null); + } + + // Some package state has changed, so grant default roles again. + Log.i(LOG_TAG, "Granting default roles..."); + AndroidFuture<Void> future = new AndroidFuture<>(); + getOrCreateController(userId).grantDefaultRoles(ForegroundThread.getExecutor(), + successful -> { + if (successful) { + userState.setPackagesHash(newPackagesHash); + future.complete(null); + } else { + future.completeExceptionally(new RuntimeException()); + } + }); + return future; + } + + @NonNull + private RoleUserState getOrCreateUserState(@UserIdInt int userId) { + synchronized (mLock) { + RoleUserState userState = mUserStates.get(userId); + if (userState == null) { + userState = new RoleUserState(userId, mPlatformHelper, this); + mUserStates.put(userId, userState); + } + return userState; + } + } + + @NonNull + private RoleControllerManager getOrCreateController(@UserIdInt int userId) { + synchronized (mLock) { + RoleControllerManager controller = mControllers.get(userId); + if (controller == null) { + Context systemContext = getContext(); + Context context; + try { + context = systemContext.createPackageContextAsUser( + systemContext.getPackageName(), 0, UserHandle.of(userId)); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + controller = RoleControllerManager.createWithInitializedRemoteServiceComponentName( + ForegroundThread.getHandler(), context); + mControllers.put(userId, controller); + } + return controller; + } + } + + @Nullable + private RemoteCallbackList<IOnRoleHoldersChangedListener> getListeners(@UserIdInt int userId) { + synchronized (mLock) { + return mListeners.get(userId); + } + } + + @NonNull + private RemoteCallbackList<IOnRoleHoldersChangedListener> getOrCreateListeners( + @UserIdInt int userId) { + synchronized (mLock) { + RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = mListeners.get(userId); + if (listeners == null) { + listeners = new RemoteCallbackList<>(); + mListeners.put(userId, listeners); + } + return listeners; + } + } + + private void onRemoveUser(@UserIdInt int userId) { + RemoteCallbackList<IOnRoleHoldersChangedListener> listeners; + RoleUserState userState; + synchronized (mLock) { + mGrantDefaultRolesThrottledRunnables.remove(userId); + listeners = mListeners.get(userId); + mListeners.remove(userId); + mControllers.remove(userId); + userState = mUserStates.get(userId); + mUserStates.remove(userId); + } + if (listeners != null) { + listeners.kill(); + } + if (userState != null) { + userState.destroy(); + } + } + + @Override + public void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) { + mListenerHandler.post(() -> notifyRoleHoldersChanged(roleName, userId)); + } + + @WorkerThread + private void notifyRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) { + RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getListeners(userId); + if (listeners != null) { + notifyRoleHoldersChangedForListeners(listeners, roleName, userId); + } + + RemoteCallbackList<IOnRoleHoldersChangedListener> allUsersListeners = getListeners( + UserHandleCompat.USER_ALL); + if (allUsersListeners != null) { + notifyRoleHoldersChangedForListeners(allUsersListeners, roleName, userId); + } + } + + @WorkerThread + private void notifyRoleHoldersChangedForListeners( + @NonNull RemoteCallbackList<IOnRoleHoldersChangedListener> listeners, + @NonNull String roleName, @UserIdInt int userId) { + int broadcastCount = listeners.beginBroadcast(); + try { + for (int i = 0; i < broadcastCount; i++) { + IOnRoleHoldersChangedListener listener = listeners.getBroadcastItem(i); + try { + listener.onRoleHoldersChanged(roleName, userId); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error calling OnRoleHoldersChangedListener", e); + } + } + } finally { + listeners.finishBroadcast(); + } + } + + private class Stub extends IRoleManager.Stub { + + @Override + public boolean isRoleAvailable(@NonNull String roleName) { + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + + int userId = UserHandleCompat.getUserId(getCallingUid()); + return getOrCreateUserState(userId).isRoleAvailable(roleName); + } + + @Override + public boolean isRoleHeld(@NonNull String roleName, @NonNull String packageName) { + int callingUid = getCallingUid(); + mAppOpsManager.checkPackage(callingUid, packageName); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + + int userId = UserHandleCompat.getUserId(callingUid); + ArraySet<String> roleHolders = getOrCreateUserState(userId).getRoleHolders(roleName); + if (roleHolders == null) { + return false; + } + return roleHolders.contains(packageName); + } + + @NonNull + @Override + public List<String> getRoleHoldersAsUser(@NonNull String roleName, @UserIdInt int userId) { + if (!isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return Collections.emptyList(); + } + enforceCrossUserPermission(userId, false, "getRoleHoldersAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, + "getRoleHoldersAsUser"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + + ArraySet<String> roleHolders = getOrCreateUserState(userId).getRoleHolders(roleName); + if (roleHolders == null) { + return Collections.emptyList(); + } + return new ArrayList<>(roleHolders); + } + + @Override + public void addRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId, + @NonNull RemoteCallback callback) { + if (!isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return; + } + enforceCrossUserPermission(userId, false, "addRoleHolderAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, + "addRoleHolderAsUser"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + getOrCreateController(userId).onAddRoleHolder(roleName, packageName, flags, + callback); + } + + @Override + public void removeRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName, + @RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId, + @NonNull RemoteCallback callback) { + if (!isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return; + } + enforceCrossUserPermission(userId, false, "removeRoleHolderAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, + "removeRoleHolderAsUser"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + getOrCreateController(userId).onRemoveRoleHolder(roleName, packageName, flags, + callback); + } + + @Override + public void clearRoleHoldersAsUser(@NonNull String roleName, + @RoleManager.ManageHoldersFlags int flags, @UserIdInt int userId, + @NonNull RemoteCallback callback) { + if (!isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return; + } + enforceCrossUserPermission(userId, false, "clearRoleHoldersAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, + "clearRoleHoldersAsUser"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + getOrCreateController(userId).onClearRoleHolders(roleName, flags, callback); + } + + @Override + public void addOnRoleHoldersChangedListenerAsUser( + @NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) { + if (userId != UserHandleCompat.USER_ALL && !isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return; + } + enforceCrossUserPermission(userId, true, "addOnRoleHoldersChangedListenerAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS, + "addOnRoleHoldersChangedListenerAsUser"); + + Objects.requireNonNull(listener, "listener cannot be null"); + + RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getOrCreateListeners( + userId); + listeners.register(listener); + } + + @Override + public void removeOnRoleHoldersChangedListenerAsUser( + @NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) { + if (userId != UserHandleCompat.USER_ALL && !isUserExistent(userId)) { + Log.e(LOG_TAG, "user " + userId + " does not exist"); + return; + } + enforceCrossUserPermission(userId, true, "removeOnRoleHoldersChangedListenerAsUser"); + getContext().enforceCallingOrSelfPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS, + "removeOnRoleHoldersChangedListenerAsUser"); + + Objects.requireNonNull(listener, "listener cannot be null"); + + RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getListeners(userId); + if (listener == null) { + return; + } + listeners.unregister(listener); + } + + @Override + public void setRoleNamesFromController(@NonNull List<String> roleNames) { + getContext().enforceCallingOrSelfPermission( + RoleManager.PERMISSION_MANAGE_ROLES_FROM_CONTROLLER, + "setRoleNamesFromController"); + + Objects.requireNonNull(roleNames, "roleNames cannot be null"); + + int userId = UserHandleCompat.getUserId(Binder.getCallingUid()); + getOrCreateUserState(userId).setRoleNames(roleNames); + } + + @Override + public boolean addRoleHolderFromController(@NonNull String roleName, + @NonNull String packageName) { + getContext().enforceCallingOrSelfPermission( + RoleManager.PERMISSION_MANAGE_ROLES_FROM_CONTROLLER, + "addRoleHolderFromController"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + + int userId = UserHandleCompat.getUserId(Binder.getCallingUid()); + return getOrCreateUserState(userId).addRoleHolder(roleName, packageName); + } + + @Override + public boolean removeRoleHolderFromController(@NonNull String roleName, + @NonNull String packageName) { + getContext().enforceCallingOrSelfPermission( + RoleManager.PERMISSION_MANAGE_ROLES_FROM_CONTROLLER, + "removeRoleHolderFromController"); + + Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + + int userId = UserHandleCompat.getUserId(Binder.getCallingUid()); + return getOrCreateUserState(userId).removeRoleHolder(roleName, packageName); + } + + @Override + public List<String> getHeldRolesFromController(@NonNull String packageName) { + getContext().enforceCallingOrSelfPermission( + RoleManager.PERMISSION_MANAGE_ROLES_FROM_CONTROLLER, + "getRolesHeldFromController"); + + Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); + + int userId = UserHandleCompat.getUserId(Binder.getCallingUid()); + return getOrCreateUserState(userId).getHeldRoles(packageName); + } + + private boolean isUserExistent(@UserIdInt int userId) { + return mUserManager.getUserHandles(true).contains(UserHandle.of(userId)); + } + + private void enforceCrossUserPermission(@UserIdInt int userId, boolean allowAll, + @NonNull String message) { + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandleCompat.getUserId(callingUid); + if (userId == callingUserId) { + return; + } + Preconditions.checkArgument(userId >= UserHandleCompat.USER_SYSTEM + || (allowAll && userId == UserHandleCompat.USER_ALL), "Invalid user " + userId); + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, message); + if (callingUid == Process.SHELL_UID && userId >= UserHandleCompat.USER_SYSTEM) { + if (mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_DEBUGGING_FEATURES, + UserHandle.of(userId))) { + throw new SecurityException("Shell does not have permission to access user " + + userId); + } + } + } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return new RoleShellCommand(this).exec(this, in.getFileDescriptor(), + out.getFileDescriptor(), err.getFileDescriptor(), args); + } + + @Nullable + @Override + public String getBrowserRoleHolder(@UserIdInt int userId) { + final int callingUid = Binder.getCallingUid(); + if (UserHandleCompat.getUserId(callingUid) != userId) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); + } + if (isInstantApp(callingUid)) { + return null; + } + + final long identity = Binder.clearCallingIdentity(); + try { + return CollectionUtils.firstOrNull(getRoleHoldersAsUser(RoleManager.ROLE_BROWSER, + userId)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private boolean isInstantApp(int uid) { + final long identity = Binder.clearCallingIdentity(); + try { + final UserHandle user = UserHandle.getUserHandleForUid(uid); + final Context userContext = getContext().createContextAsUser(user, 0); + final PackageManager userPackageManager = userContext.getPackageManager(); + // Instant apps can not have shared UID, so it's safe to check only the first + // package name here. + final String packageName = ArrayUtils.firstOrNull( + userPackageManager.getPackagesForUid(uid)); + if (packageName == null) { + return false; + } + return userPackageManager.isInstantApp(packageName); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public boolean setBrowserRoleHolder(@Nullable String packageName, @UserIdInt int userId) { + final Context context = getContext(); + context.enforceCallingOrSelfPermission( + android.Manifest.permission.SET_PREFERRED_APPLICATIONS, null); + if (UserHandleCompat.getUserId(Binder.getCallingUid()) != userId) { + context.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); + } + + if (!isUserExistent(userId)) { + return false; + } + + final AndroidFuture<Void> future = new AndroidFuture<>(); + final RemoteCallback callback = new RemoteCallback(result -> { + boolean successful = result != null; + if (successful) { + future.complete(null); + } else { + future.completeExceptionally(new RuntimeException()); + } + }); + final long identity = Binder.clearCallingIdentity(); + try { + if (packageName != null) { + addRoleHolderAsUser(RoleManager.ROLE_BROWSER, packageName, 0, userId, callback); + } else { + clearRoleHoldersAsUser(RoleManager.ROLE_BROWSER, 0, userId, callback); + } + try { + future.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.e(LOG_TAG, "Exception while setting default browser: " + packageName, e); + return false; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + + return true; + } + + @Override + public String getSmsRoleHolder(int userId) { + final long identity = Binder.clearCallingIdentity(); + try { + return CollectionUtils.firstOrNull(getRoleHoldersAsUser(RoleManager.ROLE_SMS, + userId)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, + @Nullable String[] args) { + if (!checkDumpPermission("role", fout)) { + return; + } + + boolean dumpAsProto = args != null && ArrayUtils.contains(args, "--proto"); + DualDumpOutputStream dumpOutputStream; + if (dumpAsProto) { + dumpOutputStream = new DualDumpOutputStream(new ProtoOutputStream( + new FileOutputStream(fd))); + } else { + fout.println("ROLE STATE (dumpsys role):"); + dumpOutputStream = new DualDumpOutputStream(new IndentingPrintWriter(fout, " ")); + } + + synchronized (mLock) { + final int userStatesSize = mUserStates.size(); + for (int i = 0; i < userStatesSize; i++) { + final RoleUserState userState = mUserStates.valueAt(i); + + userState.dump(dumpOutputStream, "user_states", + RoleServiceDumpProto.USER_STATES); + } + } + + dumpOutputStream.flush(); + } + + private boolean checkDumpPermission(@NonNull String serviceName, + @NonNull PrintWriter writer) { + if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + writer.println("Permission Denial: can't dump " + serviceName + " from from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + + " due to missing " + android.Manifest.permission.DUMP + " permission"); + return false; + } else { + return true; + } + } + } + + private class Local implements RoleManagerLocal { + @NonNull + @Override + public Map<String, Set<String>> getRolesAndHolders(@UserIdInt int userId) { + // Convert ArrayMap<String, ArraySet<String>> to Map<String, Set<String>> for the API. + //noinspection unchecked + return (Map<String, Set<String>>) (Map<String, ?>) + getOrCreateUserState(userId).getRolesAndHolders(); + } + } +} diff --git a/service/java/com/android/role/RoleShellCommand.java b/service/java/com/android/role/RoleShellCommand.java new file mode 100644 index 000000000..03b7c76d2 --- /dev/null +++ b/service/java/com/android/role/RoleShellCommand.java @@ -0,0 +1,151 @@ +/* + * 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.role; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.role.IRoleManager; +import android.os.RemoteCallback; +import android.os.RemoteException; + +import com.android.modules.utils.BasicShellCommandHandler; +import com.android.permission.compat.UserHandleCompat; + +import java.io.PrintWriter; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +class RoleShellCommand extends BasicShellCommandHandler { + @NonNull + private final IRoleManager mRoleManager; + + RoleShellCommand(@NonNull IRoleManager roleManager) { + mRoleManager = roleManager; + } + + private class CallbackFuture extends CompletableFuture<Void> { + @NonNull + public RemoteCallback createCallback() { + return new RemoteCallback(result -> { + boolean successful = result != null; + if (successful) { + complete(null); + } else { + completeExceptionally(new RuntimeException("Failed")); + } + }); + } + + public int waitForResult() { + try { + get(5, TimeUnit.SECONDS); + return 0; + } catch (Exception e) { + getErrPrintWriter().println("Error: see logcat for details.\n" + e); + return -1; + } + } + } + + @Override + public int onCommand(@Nullable String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + PrintWriter pw = getOutPrintWriter(); + try { + switch (cmd) { + case "add-role-holder": + return runAddRoleHolder(); + case "remove-role-holder": + return runRemoveRoleHolder(); + case "clear-role-holders": + return runClearRoleHolders(); + default: + return handleDefaultCommands(cmd); + } + } catch (RemoteException e) { + pw.println("Remote exception: " + e); + } + return -1; + } + + private int getUserIdMaybe() { + int userId = UserHandleCompat.USER_SYSTEM; + String option = getNextOption(); + if (option != null && option.equals("--user")) { + userId = Integer.parseInt(getNextArgRequired()); + } + return userId; + } + + private int getFlagsMaybe() { + String flags = getNextArg(); + if (flags == null) { + return 0; + } + return Integer.parseInt(flags); + } + + private int runAddRoleHolder() throws RemoteException { + int userId = getUserIdMaybe(); + String roleName = getNextArgRequired(); + String packageName = getNextArgRequired(); + int flags = getFlagsMaybe(); + + CallbackFuture future = new CallbackFuture(); + mRoleManager.addRoleHolderAsUser(roleName, packageName, flags, userId, + future.createCallback()); + return future.waitForResult(); + } + + private int runRemoveRoleHolder() throws RemoteException { + int userId = getUserIdMaybe(); + String roleName = getNextArgRequired(); + String packageName = getNextArgRequired(); + int flags = getFlagsMaybe(); + + CallbackFuture future = new CallbackFuture(); + mRoleManager.removeRoleHolderAsUser(roleName, packageName, flags, userId, + future.createCallback()); + return future.waitForResult(); + } + + private int runClearRoleHolders() throws RemoteException { + int userId = getUserIdMaybe(); + String roleName = getNextArgRequired(); + int flags = getFlagsMaybe(); + + CallbackFuture future = new CallbackFuture(); + mRoleManager.clearRoleHoldersAsUser(roleName, flags, userId, future.createCallback()); + return future.waitForResult(); + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Role (role) commands:"); + pw.println(" help or -h"); + pw.println(" Print this help text."); + pw.println(); + pw.println(" add-role-holder [--user USER_ID] ROLE PACKAGE [FLAGS]"); + pw.println(" remove-role-holder [--user USER_ID] ROLE PACKAGE [FLAGS]"); + pw.println(" clear-role-holders [--user USER_ID] ROLE [FLAGS]"); + pw.println(); + } +} diff --git a/service/java/com/android/role/RoleUserState.java b/service/java/com/android/role/RoleUserState.java new file mode 100644 index 000000000..78d8d15bb --- /dev/null +++ b/service/java/com/android/role/RoleUserState.java @@ -0,0 +1,476 @@ +/* + * 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.role; + +import android.annotation.CheckResult; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.annotation.WorkerThread; +import android.os.Handler; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.dump.DualDumpOutputStream; +import com.android.permission.util.BackgroundThread; +import com.android.permission.util.CollectionUtils; +import com.android.role.persistence.RolesPersistence; +import com.android.role.persistence.RolesState; +import com.android.server.role.RoleServicePlatformHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Stores the state of roles for a user. + */ +class RoleUserState { + private static final String LOG_TAG = RoleUserState.class.getSimpleName(); + + public static final int VERSION_UNDEFINED = -1; + + private static final long WRITE_DELAY_MILLIS = 200; + + private final RolesPersistence mPersistence = RolesPersistence.createInstance(); + + @UserIdInt + private final int mUserId; + + @NonNull + private final RoleServicePlatformHelper mPlatformHelper; + + @NonNull + private final Callback mCallback; + + @NonNull + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private int mVersion = VERSION_UNDEFINED; + + @GuardedBy("mLock") + @Nullable + private String mPackagesHash; + + /** + * Maps role names to its holders' package names. The values should never be null. + */ + @GuardedBy("mLock") + @NonNull + private ArrayMap<String, ArraySet<String>> mRoles = new ArrayMap<>(); + + @GuardedBy("mLock") + private boolean mWriteScheduled; + + @GuardedBy("mLock") + private boolean mDestroyed; + + @NonNull + private final Handler mWriteHandler = new Handler(BackgroundThread.get().getLooper()); + + /** + * Create a new user state, and read its state from disk if previously persisted. + * + * @param userId the user id for this user state + * @param platformHelper the platform helper + * @param callback the callback for this user state + */ + public RoleUserState(@UserIdInt int userId, @NonNull RoleServicePlatformHelper platformHelper, + @NonNull Callback callback) { + mUserId = userId; + mPlatformHelper = platformHelper; + mCallback = callback; + + readFile(); + } + + /** + * Get the version of this user state. + */ + public int getVersion() { + synchronized (mLock) { + return mVersion; + } + } + + /** + * Set the version of this user state. + * + * @param version the version to set + */ + public void setVersion(int version) { + synchronized (mLock) { + if (mVersion == version) { + return; + } + mVersion = version; + scheduleWriteFileLocked(); + } + } + + /** + * Get the hash representing the state of packages during the last time initial grants was run. + * + * @return the hash representing the state of packages + */ + @Nullable + public String getPackagesHash() { + synchronized (mLock) { + return mPackagesHash; + } + } + + /** + * Set the hash representing the state of packages during the last time initial grants was run. + * + * @param packagesHash the hash representing the state of packages + */ + public void setPackagesHash(@Nullable String packagesHash) { + synchronized (mLock) { + if (Objects.equals(mPackagesHash, packagesHash)) { + return; + } + mPackagesHash = packagesHash; + scheduleWriteFileLocked(); + } + } + + /** + * Get whether the role is available. + * + * @param roleName the name of the role to get the holders for + * + * @return whether the role is available + */ + public boolean isRoleAvailable(@NonNull String roleName) { + synchronized (mLock) { + return mRoles.containsKey(roleName); + } + } + + /** + * Get the holders of a role. + * + * @param roleName the name of the role to query for + * + * @return the set of role holders, or {@code null} if and only if the role is not found + */ + @Nullable + public ArraySet<String> getRoleHolders(@NonNull String roleName) { + synchronized (mLock) { + ArraySet<String> packageNames = mRoles.get(roleName); + if (packageNames == null) { + return null; + } + return new ArraySet<>(packageNames); + } + } + + /** + * Adds the given role, effectively marking it as {@link #isRoleAvailable available} + * + * @param roleName the name of the role + * + * @return whether any changes were made + */ + public boolean addRoleName(@NonNull String roleName) { + synchronized (mLock) { + if (!mRoles.containsKey(roleName)) { + mRoles.put(roleName, new ArraySet<>()); + Log.i(LOG_TAG, "Added new role: " + roleName); + scheduleWriteFileLocked(); + return true; + } else { + return false; + } + } + } + + /** + * Set the names of all available roles. + * + * @param roleNames the names of all the available roles + */ + public void setRoleNames(@NonNull List<String> roleNames) { + synchronized (mLock) { + boolean changed = false; + + for (int i = mRoles.size() - 1; i >= 0; i--) { + String roleName = mRoles.keyAt(i); + + if (!roleNames.contains(roleName)) { + ArraySet<String> packageNames = mRoles.valueAt(i); + if (!packageNames.isEmpty()) { + Log.e(LOG_TAG, "Holders of a removed role should have been cleaned up," + + " role: " + roleName + ", holders: " + packageNames); + } + mRoles.removeAt(i); + changed = true; + } + } + + int roleNamesSize = roleNames.size(); + for (int i = 0; i < roleNamesSize; i++) { + changed |= addRoleName(roleNames.get(i)); + } + + if (changed) { + scheduleWriteFileLocked(); + } + } + } + + /** + * Add a holder to a role. + * + * @param roleName the name of the role to add the holder to + * @param packageName the package name of the new holder + * + * @return {@code false} if and only if the role is not found + */ + @CheckResult + public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) { + boolean changed; + + synchronized (mLock) { + ArraySet<String> roleHolders = mRoles.get(roleName); + if (roleHolders == null) { + Log.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName + + ", package: " + packageName); + return false; + } + changed = roleHolders.add(packageName); + if (changed) { + scheduleWriteFileLocked(); + } + } + + if (changed) { + mCallback.onRoleHoldersChanged(roleName, mUserId); + } + return true; + } + + /** + * Remove a holder from a role. + * + * @param roleName the name of the role to remove the holder from + * @param packageName the package name of the holder to remove + * + * @return {@code false} if and only if the role is not found + */ + @CheckResult + public boolean removeRoleHolder(@NonNull String roleName, @NonNull String packageName) { + boolean changed; + + synchronized (mLock) { + ArraySet<String> roleHolders = mRoles.get(roleName); + if (roleHolders == null) { + Log.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName + + ", package: " + packageName); + return false; + } + + changed = roleHolders.remove(packageName); + if (changed) { + scheduleWriteFileLocked(); + } + } + + if (changed) { + mCallback.onRoleHoldersChanged(roleName, mUserId); + } + return true; + } + + /** + * @see android.app.role.RoleManager#getHeldRolesFromController + */ + @NonNull + public List<String> getHeldRoles(@NonNull String packageName) { + synchronized (mLock) { + List<String> roleNames = new ArrayList<>(); + int size = mRoles.size(); + for (int i = 0; i < size; i++) { + if (mRoles.valueAt(i).contains(packageName)) { + roleNames.add(mRoles.keyAt(i)); + } + } + return roleNames; + } + } + + /** + * Schedule writing the state to file. + */ + @GuardedBy("mLock") + private void scheduleWriteFileLocked() { + if (mDestroyed) { + return; + } + + if (!mWriteScheduled) { + mWriteHandler.postDelayed(this::writeFile, WRITE_DELAY_MILLIS); + mWriteScheduled = true; + } + } + + @WorkerThread + private void writeFile() { + RolesState roles; + synchronized (mLock) { + if (mDestroyed) { + return; + } + + mWriteScheduled = false; + + roles = new RolesState(mVersion, mPackagesHash, + (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked()); + } + + mPersistence.writeForUser(roles, UserHandle.of(mUserId)); + } + + private void readFile() { + synchronized (mLock) { + RolesState roleState = mPersistence.readForUser(UserHandle.of(mUserId)); + + Map<String, Set<String>> roles; + if (roleState != null) { + mVersion = roleState.getVersion(); + mPackagesHash = roleState.getPackagesHash(); + roles = roleState.getRoles(); + } else { + roles = mPlatformHelper.getLegacyRoleState(mUserId); + } + mRoles.clear(); + for (Map.Entry<String, Set<String>> entry : roles.entrySet()) { + String roleName = entry.getKey(); + ArraySet<String> roleHolders = new ArraySet<>(entry.getValue()); + mRoles.put(roleName, roleHolders); + } + + if (roleState == null) { + scheduleWriteFileLocked(); + } + } + } + + /** + * Dump this user state. + * + * @param dumpOutputStream the output stream to dump to + */ + public void dump(@NonNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName, + long fieldId) { + int version; + String packagesHash; + ArrayMap<String, ArraySet<String>> roles; + synchronized (mLock) { + version = mVersion; + packagesHash = mPackagesHash; + roles = snapshotRolesLocked(); + } + + long fieldToken = dumpOutputStream.start(fieldName, fieldId); + dumpOutputStream.write("user_id", RoleUserStateProto.USER_ID, mUserId); + dumpOutputStream.write("version", RoleUserStateProto.VERSION, version); + dumpOutputStream.write("packages_hash", RoleUserStateProto.PACKAGES_HASH, packagesHash); + + int rolesSize = roles.size(); + for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) { + String roleName = roles.keyAt(rolesIndex); + ArraySet<String> roleHolders = roles.valueAt(rolesIndex); + + long rolesToken = dumpOutputStream.start("roles", RoleUserStateProto.ROLES); + dumpOutputStream.write("name", RoleProto.NAME, roleName); + + int roleHoldersSize = roleHolders.size(); + for (int roleHoldersIndex = 0; roleHoldersIndex < roleHoldersSize; roleHoldersIndex++) { + String roleHolder = roleHolders.valueAt(roleHoldersIndex); + + dumpOutputStream.write("holders", RoleProto.HOLDERS, roleHolder); + } + + dumpOutputStream.end(rolesToken); + } + + dumpOutputStream.end(fieldToken); + } + + /** + * Get the roles and their holders. + * + * @return A copy of the roles and their holders + */ + @NonNull + public ArrayMap<String, ArraySet<String>> getRolesAndHolders() { + synchronized (mLock) { + return snapshotRolesLocked(); + } + } + + @GuardedBy("mLock") + @NonNull + private ArrayMap<String, ArraySet<String>> snapshotRolesLocked() { + ArrayMap<String, ArraySet<String>> roles = new ArrayMap<>(); + for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) { + String roleName = mRoles.keyAt(i); + ArraySet<String> roleHolders = mRoles.valueAt(i); + + roleHolders = new ArraySet<>(roleHolders); + roles.put(roleName, roleHolders); + } + return roles; + } + + /** + * Destroy this user state and delete the corresponding file. Any pending writes to the file + * will be cancelled, and any future interaction with this state will throw an exception. + */ + public void destroy() { + synchronized (mLock) { + if (mDestroyed) { + throw new IllegalStateException("This RoleUserState has already been destroyed"); + } + mWriteHandler.removeCallbacksAndMessages(null); + mPersistence.deleteForUser(UserHandle.of(mUserId)); + mDestroyed = true; + } + } + + /** + * Callback for a user state. + */ + public interface Callback { + + /** + * Called when the holders of roles are changed. + * + * @param roleName the name of the role whose holders are changed + * @param userId the user id for this role holder change + */ + void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId); + } +} diff --git a/service/java/com/android/role/TEST_MAPPING b/service/java/com/android/role/TEST_MAPPING new file mode 100644 index 000000000..0d7bc1476 --- /dev/null +++ b/service/java/com/android/role/TEST_MAPPING @@ -0,0 +1,20 @@ +{ + "presubmit": [ + { + "name": "CtsStatsdHostTestCases", + "options": [ + { + "include-filter": "android.cts.statsd.atom.UidAtomTests#testRoleHolder" + } + ] + }, + { + "name": "CtsRoleTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/service/java/com/android/role/package-info.java b/service/java/com/android/role/package-info.java new file mode 100644 index 000000000..8b5b25161 --- /dev/null +++ b/service/java/com/android/role/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @hide + * TODO(b/146466118) remove this javadoc tag + */ +@android.annotation.Hide +package com.android.role; diff --git a/service/proto/com/android/role/roleservice.proto b/service/proto/com/android/role/roleservice.proto new file mode 100644 index 000000000..79c422992 --- /dev/null +++ b/service/proto/com/android/role/roleservice.proto @@ -0,0 +1,56 @@ +/* + * 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. + */ + +syntax = "proto2"; + +package com.android.role; + +option java_multiple_files = true; + +import "frameworks/base/core/proto/android/privacy.proto"; + +message RoleServiceDumpProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // List of per-user states for all users. + repeated RoleUserStateProto user_states = 1; +} + +message RoleUserStateProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The user id of this state. + optional int32 user_id = 1; + + // The version of this state. + optional int32 version = 2; + + // The hash of packages for this state. + optional string packages_hash = 3; + + // The set of roles in this state. + repeated RoleProto roles = 4; +} + +message RoleProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The name of this role, e.g. "android.app.role.DIALER". + optional string name = 1; + + // The package names of the holders of this role. + repeated string holders = 2; +} |