Allow adopting a subset of shell permissions

Add an API to allow adopting a subset of shell UID permissions.

Test: added - android.app.uiautomation.cts.UiAutomationTest#testAdoptSomeShellPermissions
      passed - atest atest android.app.uiautomation.cts.UiAutomationTest

bug:80415658

Change-Id: I5cd8beeed7e586b2ac4e0cb16686cef738641e23
diff --git a/api/current.txt b/api/current.txt
index cc38630..63bb349 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -6180,6 +6180,7 @@
 
   public final class UiAutomation {
     method public void adoptShellPermissionIdentity();
+    method public void adoptShellPermissionIdentity(java.lang.String...);
     method public void clearWindowAnimationFrameStats();
     method public boolean clearWindowContentFrameStats(int);
     method public void dropShellPermissionIdentity();
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 519a274..80f512c 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -486,7 +486,7 @@
      * instrumentation at a time. An active instrumentation is one running and
      * started from the shell.
      */
-    void startDelegateShellPermissionIdentity(int uid);
+    void startDelegateShellPermissionIdentity(int uid, in String[] permissions);
 
     /**
      * Method for the shell UID to stop deletating its permission identity to an
diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl
index ac4bf7d..96da72a 100644
--- a/core/java/android/app/IUiAutomationConnection.aidl
+++ b/core/java/android/app/IUiAutomationConnection.aidl
@@ -47,7 +47,7 @@
             in ParcelFileDescriptor source);
     void grantRuntimePermission(String packageName, String permission, int userId);
     void revokeRuntimePermission(String packageName, String permission, int userId);
-    void adoptShellPermissionIdentity(int uid);
+    void adoptShellPermissionIdentity(int uid, in String[] permissions);
     void dropShellPermissionIdentity();
     // Called from the system process.
     oneway void shutdown();
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 5a25f5a..3f9627e 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -354,12 +354,17 @@
     }
 
     /**
-     * Adopt the permission identity of the shell UID. This allows you to call APIs protected
-     * permissions which normal apps cannot hold but are granted to the shell UID. If you
-     * already adopted the shell permission identity this method would be a no-op.
-     * Note that your permission state becomes that of the shell UID and it is not a
-     * combination of your and the shell UID permissions.
+     * Adopt the permission identity of the shell UID for all permissions. This allows
+     * you to call APIs protected permissions which normal apps cannot hold but are
+     * granted to the shell UID. If you already adopted all shell permissions by calling
+     * this method or {@link #adoptShellPermissionIdentity(String...)} a subsequent call
+     * would be a no-op. Note that your permission state becomes that of the shell UID
+     * and it is not a combination of your and the shell UID permissions.
+     * <p>
+     * <strong>Note:<strong/> Calling this method adopts all shell permissions and overrides
+     * any subset of adopted permissions via {@link #adoptShellPermissionIdentity(String...)}.
      *
+     * @see #adoptShellPermissionIdentity(String...)
      * @see #dropShellPermissionIdentity()
      */
     public void adoptShellPermissionIdentity() {
@@ -368,7 +373,33 @@
         }
         try {
             // Calling out without a lock held.
-            mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid());
+            mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), null);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error executing adopting shell permission identity!", re);
+        }
+    }
+
+    /**
+     * Adopt the permission identity of the shell UID only for the provided permissions.
+     * This allows you to call APIs protected permissions which normal apps cannot hold
+     * but are granted to the shell UID. If you already adopted the specified shell
+     * permissions by calling this method or {@link #adoptShellPermissionIdentity()} a
+     * subsequent call would be a no-op. Note that your permission state becomes that of the
+     * shell UID and it is not a combination of your and the shell UID permissions.
+     * <p>
+     * <strong>Note:<strong/> Calling this method adopts only the specified shell permissions
+     * and overrides all adopted permissions via {@link #adoptShellPermissionIdentity()}.
+     *
+     * @see #adoptShellPermissionIdentity()
+     * @see #dropShellPermissionIdentity()
+     */
+    public void adoptShellPermissionIdentity(String... permissions) {
+        synchronized (mLock) {
+            throwIfNotConnectedLocked();
+        }
+        try {
+            // Calling out without a lock held.
+            mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), permissions);
         } catch (RemoteException re) {
             Log.e(LOG_TAG, "Error executing adopting shell permission identity!", re);
         }
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index b406d9e..dc2f983 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -18,7 +18,7 @@
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
-import android.annotation.UnsupportedAppUsage;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.IPackageManager;
 import android.graphics.Bitmap;
@@ -279,7 +279,8 @@
     }
 
     @Override
-    public void adoptShellPermissionIdentity(int uid) throws RemoteException {
+    public void adoptShellPermissionIdentity(int uid, @Nullable String[] permissions)
+            throws RemoteException {
         synchronized (mLock) {
             throwIfCalledByNotTrustedUidLocked();
             throwIfShutdownLocked();
@@ -287,7 +288,7 @@
         }
         final long identity = Binder.clearCallingIdentity();
         try {
-            mActivityManager.startDelegateShellPermissionIdentity(uid);
+            mActivityManager.startDelegateShellPermissionIdentity(uid, permissions);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e8c6365..5316f4e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19386,7 +19386,8 @@
     }
 
     @Override
-    public void startDelegateShellPermissionIdentity(int delegateUid) {
+    public void startDelegateShellPermissionIdentity(int delegateUid,
+            @Nullable String[] permissions) {
         if (UserHandle.getCallingAppId() != Process.SHELL_UID
                 && UserHandle.getCallingAppId() != Process.ROOT_UID) {
             throw new SecurityException("Only the shell can delegate its permissions");
@@ -19405,11 +19406,13 @@
                 if (!(mAppOpsService.getAppOpsServiceDelegate() instanceof ShellDelegate)) {
                     throw new IllegalStateException("Bad shell delegate state");
                 }
-                if (((ShellDelegate) mAppOpsService.getAppOpsServiceDelegate())
-                        .getDelegateUid() != delegateUid) {
+                final ShellDelegate delegate = (ShellDelegate) mAppOpsService
+                        .getAppOpsServiceDelegate();
+                if (delegate.getDelegateUid() != delegateUid) {
                     throw new SecurityException("Shell can delegate permissions only "
                             + "to one instrumentation at a time");
                 }
+                delegate.setPermissions(permissions);
                 return;
             }
 
@@ -19427,7 +19430,7 @@
 
                 // Hook them up...
                 final ShellDelegate shellDelegate = new ShellDelegate(
-                        instr.mTargetInfo.packageName, delegateUid);
+                        instr.mTargetInfo.packageName, delegateUid, permissions);
                 mAppOpsService.setAppOpsServiceDelegate(shellDelegate);
                 getPackageManagerInternalLocked().setCheckPermissionDelegate(shellDelegate);
                 return;
@@ -19450,20 +19453,26 @@
     private class ShellDelegate implements CheckOpsDelegate, CheckPermissionDelegate {
         private final String mTargetPackageName;
         private final int mTargetUid;
+        private @Nullable String[] mPermissions;
 
-        ShellDelegate(String targetPacakgeName, int targetUid) {
+        ShellDelegate(String targetPacakgeName, int targetUid, @Nullable String[] permissions) {
             mTargetPackageName = targetPacakgeName;
             mTargetUid = targetUid;
+            mPermissions = permissions;
         }
 
         int getDelegateUid() {
             return mTargetUid;
         }
 
+        void setPermissions(@Nullable String[] permissions) {
+            mPermissions = permissions;
+        }
+
         @Override
         public int checkOperation(int code, int uid, String packageName,
                 TriFunction<Integer, Integer, String, Integer> superImpl) {
-            if (uid == mTargetUid) {
+            if (uid == mTargetUid && isTargetOp(code)) {
                 final long identity = Binder.clearCallingIdentity();
                 try {
                     return superImpl.apply(code, Process.SHELL_UID,
@@ -19478,7 +19487,7 @@
         @Override
         public int checkAudioOperation(int code, int usage, int uid, String packageName,
                 QuadFunction<Integer, Integer, Integer, String, Integer> superImpl) {
-            if (uid == mTargetUid) {
+            if (uid == mTargetUid && isTargetOp(code)) {
                 final long identity = Binder.clearCallingIdentity();
                 try {
                     return superImpl.apply(code, usage, Process.SHELL_UID,
@@ -19493,7 +19502,7 @@
         @Override
         public int noteOperation(int code, int uid, String packageName,
                 TriFunction<Integer, Integer, String, Integer> superImpl) {
-            if (uid == mTargetUid) {
+            if (uid == mTargetUid && isTargetOp(code)) {
                 final long identity = Binder.clearCallingIdentity();
                 try {
                     return mAppOpsService.noteProxyOperation(code, Process.SHELL_UID,
@@ -19508,7 +19517,7 @@
         @Override
         public int checkPermission(String permName, String pkgName, int userId,
                 TriFunction<String, String, Integer, Integer> superImpl) {
-            if (mTargetPackageName.equals(pkgName)) {
+            if (mTargetPackageName.equals(pkgName) && isTargetPermission(permName)) {
                 return superImpl.apply(permName, "com.android.shell", userId);
             }
             return superImpl.apply(permName, pkgName, userId);
@@ -19517,11 +19526,29 @@
         @Override
         public int checkUidPermission(String permName, int uid,
                 BiFunction<String, Integer, Integer> superImpl) {
-            if (uid == mTargetUid) {
+            if (uid == mTargetUid  && isTargetPermission(permName)) {
                 return superImpl.apply(permName, Process.SHELL_UID);
             }
             return superImpl.apply(permName, uid);
         }
+
+        private boolean isTargetOp(int code) {
+            // null permissions means all ops are targeted
+            if (mPermissions == null) {
+                return true;
+            }
+            // no permission for the op means the op is targeted
+            final String permission = AppOpsManager.opToPermission(code);
+            if (permission == null) {
+                return true;
+            }
+            return isTargetPermission(permission);
+        }
+
+        private boolean isTargetPermission(@NonNull String permission) {
+            // null permissions means all permissions are targeted
+            return (mPermissions == null || ArrayUtils.contains(mPermissions, permission));
+        }
     }
 
     /**