Slices permission model

 - Launcher/assistant get access to all slices
 - Apps with uri access to access a specific slice
 - Apps without access get a permission granting slice
 - If the user authorizes access to the slice for the app
   then the app will be granted access to the app's slices
   (this happens through a temp grant in the service, and a
   full uri grant from the app the next time it binds)
 - Add a hint that apps to add to allow them to return different
   slices depending on the caller, this allows custom permission
   checks.

Test: runtest --path frameworks/base/services/tests/uiservices
Bug: 68751119
Change-Id: I8f8cd0182cfcbfba3f307e2eaba5aae6f6fbe214
diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl
index 5f0e542..4461b16 100644
--- a/core/java/android/app/slice/ISliceManager.aidl
+++ b/core/java/android/app/slice/ISliceManager.aidl
@@ -29,4 +29,6 @@
     void unpinSlice(String pkg, in Uri uri);
     boolean hasSliceAccess(String pkg);
     SliceSpec[] getPinnedSpecs(in Uri uri, String pkg);
+    int checkSlicePermission(in Uri uri, String pkg, int pid, int uid);
+    void grantPermissionFromUser(in Uri uri, String pkg, String callingPkg, boolean allSlices);
 }
diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java
index 6093a4a..5bd3440 100644
--- a/core/java/android/app/slice/Slice.java
+++ b/core/java/android/app/slice/Slice.java
@@ -156,6 +156,13 @@
      */
     public static final String HINT_SEE_MORE = "see_more";
     /**
+     * A hint to tell the system that this slice cares about the return value of
+     * {@link SliceProvider#getBindingPackage} and should not cache the result
+     * for multiple apps.
+     * @hide
+     */
+    public static final String HINT_CALLER_NEEDED = "caller_needed";
+    /**
      * Key to retrieve an extra added to an intent when a control is changed.
      */
     public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index 74864cb..09c420c 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -53,12 +53,34 @@
 
     private static final String TAG = "SliceManager";
 
+    /**
+     * @hide
+     */
+    public static final String ACTION_REQUEST_SLICE_PERMISSION =
+            "android.intent.action.REQUEST_SLICE_PERMISSION";
+
     private final ISliceManager mService;
     private final Context mContext;
     private final ArrayMap<Pair<Uri, SliceCallback>, ISliceListener> mListenerLookup =
             new ArrayMap<>();
 
     /**
+     * Permission denied.
+     * @hide
+     */
+    public static final int PERMISSION_DENIED = -1;
+    /**
+     * Permission granted.
+     * @hide
+     */
+    public static final int PERMISSION_GRANTED = 0;
+    /**
+     * Permission just granted by the user, and should be granted uri permission as well.
+     * @hide
+     */
+    public static final int PERMISSION_USER_GRANTED = 1;
+
+    /**
      * @hide
      */
     public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
@@ -284,7 +306,7 @@
             extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
             extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
                     new ArrayList<>(supportedSpecs));
-            final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE,
+            final Bundle res = provider.call(mContext.getPackageName(), SliceProvider.METHOD_SLICE,
                     null, extras);
             Bundle.setDefusable(res, true);
             if (res == null) {
@@ -342,7 +364,7 @@
             extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
             extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
                     new ArrayList<>(supportedSpecs));
-            final Bundle res = provider.call(resolver.getPackageName(),
+            final Bundle res = provider.call(mContext.getPackageName(),
                     SliceProvider.METHOD_MAP_INTENT, null, extras);
             if (res == null) {
                 return null;
@@ -358,6 +380,45 @@
     }
 
     /**
+     * Does the permission check to see if a caller has access to a specific slice.
+     * @hide
+     */
+    public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) {
+        try {
+            if (pkg == null) {
+                throw new SecurityException("No pkg specified");
+            }
+            int result = mService.checkSlicePermission(uri, pkg, pid, uid);
+            if (result == PERMISSION_DENIED) {
+                throw new SecurityException("User " + uid + " does not have slice permission for "
+                        + uri + ".");
+            }
+            if (result == PERMISSION_USER_GRANTED) {
+                // We just had a user grant of this permission and need to grant this to the app
+                // permanently.
+                mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
+                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+                                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Called by SystemUI to grant a slice permission after a dialog is shown.
+     * @hide
+     */
+    public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
+        try {
+            mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Class that listens to changes in {@link Slice}s.
      */
     public interface SliceCallback {
diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java
index aa41f14..8ffacf5 100644
--- a/core/java/android/app/slice/SliceProvider.java
+++ b/core/java/android/app/slice/SliceProvider.java
@@ -15,13 +15,19 @@
  */
 package android.app.slice;
 
-import android.Manifest.permission;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
@@ -129,9 +135,41 @@
      * @hide
      */
     public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+    /**
+     * @hide
+     */
+    public static final String EXTRA_PKG = "pkg";
+    /**
+     * @hide
+     */
+    public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+    /**
+     * @hide
+     */
+    public static final String EXTRA_OVERRIDE_PKG = "override_pkg";
 
     private static final boolean DEBUG = false;
 
+    private String mBindingPkg;
+    private SliceManager mSliceManager;
+
+    /**
+     * Return the package name of the caller that initiated the binding request
+     * currently happening. The returned package will have been
+     * verified to belong to the calling UID. Returns {@code null} if not
+     * currently performing an {@link #onBindSlice(Uri, List)}.
+     * @hide
+     */
+    public final @Nullable String getBindingPackage() {
+        return mBindingPkg;
+    }
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        super.attachInfo(context, info);
+        mSliceManager = context.getSystemService(SliceManager.class);
+    }
+
     /**
      * Implemented to create a slice. Will be called on the main thread.
      * <p>
@@ -262,28 +300,27 @@
     public Bundle call(String method, String arg, Bundle extras) {
         if (method.equals(METHOD_SLICE)) {
             Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
-                getContext().enforceUriPermission(uri, permission.BIND_SLICE,
-                        permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires the permission BIND_SLICE");
-            }
             List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
 
-            Slice s = handleBindSlice(uri, supportedSpecs);
+            String callingPackage = getCallingPackage();
+            if (extras.containsKey(EXTRA_OVERRIDE_PKG)) {
+                if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                    throw new SecurityException("Only the system can override calling pkg");
+                }
+                callingPackage = extras.getString(EXTRA_OVERRIDE_PKG);
+            }
+            Slice s = handleBindSlice(uri, supportedSpecs, callingPackage);
             Bundle b = new Bundle();
             b.putParcelable(EXTRA_SLICE, s);
             return b;
         } else if (method.equals(METHOD_MAP_INTENT)) {
-            getContext().enforceCallingPermission(permission.BIND_SLICE,
-                    "Slice binding requires the permission BIND_SLICE");
             Intent intent = extras.getParcelable(EXTRA_INTENT);
             if (intent == null) return null;
             Uri uri = onMapIntentToUri(intent);
             List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
             Bundle b = new Bundle();
             if (uri != null) {
-                Slice s = handleBindSlice(uri, supportedSpecs);
+                Slice s = handleBindSlice(uri, supportedSpecs, getCallingPackage());
                 b.putParcelable(EXTRA_SLICE, s);
             } else {
                 b.putParcelable(EXTRA_SLICE, null);
@@ -291,20 +328,14 @@
             return b;
         } else if (method.equals(METHOD_PIN)) {
             Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
-                getContext().enforceUriPermission(uri, permission.BIND_SLICE,
-                        permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires the permission BIND_SLICE");
+            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                throw new SecurityException("Only the system can pin/unpin slices");
             }
             handlePinSlice(uri);
         } else if (method.equals(METHOD_UNPIN)) {
             Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
-                getContext().enforceUriPermission(uri, permission.BIND_SLICE,
-                        permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires the permission BIND_SLICE");
+            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                throw new SecurityException("Only the system can pin/unpin slices");
             }
             handleUnpinSlice(uri);
         } else if (method.equals(METHOD_GET_DESCENDANTS)) {
@@ -370,14 +401,27 @@
         }
     }
 
-    private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+    private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs,
+            String callingPkg) {
+        // This can be removed once Slice#bindSlice is removed and everyone is using
+        // SliceManager#bindSlice.
+        String pkg = callingPkg != null ? callingPkg
+                : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+        if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+            try {
+                mSliceManager.enforceSlicePermission(sliceUri, pkg,
+                        Binder.getCallingPid(), Binder.getCallingUid());
+            } catch (SecurityException e) {
+                return createPermissionSlice(getContext(), sliceUri, pkg);
+            }
+        }
         if (Looper.myLooper() == Looper.getMainLooper()) {
-            return onBindSliceStrict(sliceUri, supportedSpecs);
+            return onBindSliceStrict(sliceUri, supportedSpecs, pkg);
         } else {
             CountDownLatch latch = new CountDownLatch(1);
             Slice[] output = new Slice[1];
             Handler.getMain().post(() -> {
-                output[0] = onBindSliceStrict(sliceUri, supportedSpecs);
+                output[0] = onBindSliceStrict(sliceUri, supportedSpecs, pkg);
                 latch.countDown();
             });
             try {
@@ -389,15 +433,66 @@
         }
     }
 
-    private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+    /**
+     * @hide
+     */
+    public static Slice createPermissionSlice(Context context, Uri sliceUri,
+            String callingPackage) {
+        return new Slice.Builder(sliceUri)
+                .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+                        new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+                                .addText(getPermissionString(context, callingPackage), null)
+                                .build())
+                .addHints(Slice.HINT_LIST_ITEM)
+                .build();
+    }
+
+    /**
+     * @hide
+     */
+    public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+            String callingPackage) {
+        Intent intent = new Intent(SliceManager.ACTION_REQUEST_SLICE_PERMISSION);
+        intent.setComponent(new ComponentName("com.android.systemui",
+                "com.android.systemui.SlicePermissionActivity"));
+        intent.putExtra(EXTRA_BIND_URI, sliceUri);
+        intent.putExtra(EXTRA_PKG, callingPackage);
+        intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+        // Unique pending intent.
+        intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+                .build());
+
+        return PendingIntent.getActivity(context, 0, intent, 0);
+    }
+
+    /**
+     * @hide
+     */
+    public static CharSequence getPermissionString(Context context, String callingPackage) {
+        PackageManager pm = context.getPackageManager();
+        try {
+            return context.getString(
+                    com.android.internal.R.string.slices_permission_request,
+                    pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+                    context.getApplicationInfo().loadLabel(pm));
+        } catch (NameNotFoundException e) {
+            // This shouldn't be possible since the caller is verified.
+            throw new RuntimeException("Unknown calling app", e);
+        }
+    }
+
+    private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs,
+            String callingPackage) {
         ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
         try {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectAll()
                     .penaltyDeath()
                     .build());
+            mBindingPkg = callingPackage;
             return onBindSlice(sliceUri, supportedSpecs);
         } finally {
+            mBindingPkg = null;
             StrictMode.setThreadPolicy(oldPolicy);
         }
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e4e46f6..990c574 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3192,10 +3192,14 @@
     <permission android:name="android.permission.BIND_APPWIDGET"
         android:protectionLevel="signature|privileged" />
 
+    <!-- @hide Allows sysui to manage user grants of slice permissions. -->
+    <permission android:name="android.permission.MANAGE_SLICE_PERMISSIONS"
+        android:protectionLevel="signature" />
+
     <!-- Allows an application to bind app's slices and get their
          content. This content will be surfaced to the
          user and not to leave the device.
-         <p>Not for use by third-party applications. -->
+         <p>Not for use by third-party applications.-->
     <permission android:name="android.permission.BIND_SLICE"
         android:protectionLevel="signature|privileged|development" />
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 2cfe919..0c844c9 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4817,4 +4817,8 @@
     <string name="harmful_app_warning_launch_anyway">Launch anyway</string>
     <!-- Title for the harmful app warning dialog. -->
     <string name="harmful_app_warning_title">Uninstall harmful app?</string>
+
+    <!-- Text describing a permission request for one app to show another app's
+         slices [CHAR LIMIT=NONE] -->
+    <string name="slices_permission_request"><xliff:g id="app" example="Example App">%1$s</xliff:g> wants to show <xliff:g id="app_2" example="Other Example App">%2$s</xliff:g> slices</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5309115..03a800d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3227,4 +3227,5 @@
   <java-symbol type="string" name="config_defaultAssistantAccessPackage" />
 
   <java-symbol type="bool" name="config_supportBluetoothPersistedState" />
+  <java-symbol type="string" name="slices_permission_request" />
 </resources>