diff options
-rw-r--r-- | core/java/android/app/slice/ISliceManager.aidl | 2 | ||||
-rw-r--r-- | core/java/android/app/slice/Slice.java | 7 | ||||
-rw-r--r-- | core/java/android/app/slice/SliceManager.java | 65 | ||||
-rw-r--r-- | core/java/android/app/slice/SliceProvider.java | 145 | ||||
-rw-r--r-- | core/res/AndroidManifest.xml | 6 | ||||
-rw-r--r-- | core/res/res/values/strings.xml | 4 | ||||
-rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
-rw-r--r-- | packages/SystemUI/AndroidManifest.xml | 13 | ||||
-rw-r--r-- | packages/SystemUI/res/layout/slice_permission_request.xml | 50 | ||||
-rw-r--r-- | packages/SystemUI/res/values/strings.xml | 16 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java | 88 | ||||
-rw-r--r-- | services/core/java/com/android/server/slice/PinnedSliceState.java | 52 | ||||
-rw-r--r-- | services/core/java/com/android/server/slice/SliceManagerService.java | 129 | ||||
-rw-r--r-- | services/tests/uiservicestests/AndroidManifest.xml | 1 | ||||
-rw-r--r-- | services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java | 10 |
15 files changed, 527 insertions, 62 deletions
diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl index 5f0e542f4b12..4461b16fe15c 100644 --- a/core/java/android/app/slice/ISliceManager.aidl +++ b/core/java/android/app/slice/ISliceManager.aidl @@ -29,4 +29,6 @@ interface ISliceManager { 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 6093a4a02fbe..5bd3440d09f8 100644 --- a/core/java/android/app/slice/Slice.java +++ b/core/java/android/app/slice/Slice.java @@ -156,6 +156,13 @@ public final class Slice implements Parcelable { */ 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 74864cb1a371..09c420c3e66a 100644 --- a/core/java/android/app/slice/SliceManager.java +++ b/core/java/android/app/slice/SliceManager.java @@ -53,12 +53,34 @@ public class SliceManager { 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 @@ public class SliceManager { 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 @@ public class SliceManager { 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 @@ public class SliceManager { } /** + * 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 aa41f14d8cb5..8ffacf5280c1 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 @@ public abstract class SliceProvider extends ContentProvider { * @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 abstract class SliceProvider extends ContentProvider { 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 @@ public abstract class SliceProvider extends ContentProvider { 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 @@ public abstract class SliceProvider extends ContentProvider { } } - 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 @@ public abstract class SliceProvider extends ContentProvider { } } - 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 e4e46f6d0a8f..990c574f5f6c 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 2cfe919fb8f5..0c844c9deb9d 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 5309115f37bb..03a800d8bd6f 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> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index aa2cdbb7730f..80ac82576d13 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -121,7 +121,7 @@ <uses-permission android:name="android.permission.TRUST_LISTENER" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.RESET_FINGERPRINT_LOCKOUT" /> - <uses-permission android:name="android.permission.BIND_SLICE" /> + <uses-permission android:name="android.permission.MANAGE_SLICE_PERMISSIONS" /> <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked --> <uses-permission android:name="android.permission.SET_WALLPAPER"/> @@ -435,6 +435,16 @@ android:launchMode="singleTop" androidprv:alwaysFocusable="true" /> + <!-- started from SliceProvider --> + <activity android:name=".SlicePermissionActivity" + android:theme="@style/Theme.SystemUI.Dialog.Alert" + android:finishOnCloseSystemDialogs="true" + android:excludeFromRecents="true"> + <intent-filter> + <action android:name="android.intent.action.REQUEST_SLICE_PERMISSION" /> + </intent-filter> + </activity> + <!-- platform logo easter egg activity --> <activity android:name=".DessertCase" @@ -572,6 +582,7 @@ <provider android:name=".keyguard.KeyguardSliceProvider" android:authorities="com.android.systemui.keyguard" + android:grantUriPermissions="true" android:exported="true"> </provider> diff --git a/packages/SystemUI/res/layout/slice_permission_request.xml b/packages/SystemUI/res/layout/slice_permission_request.xml new file mode 100644 index 000000000000..cdb2a91a73d2 --- /dev/null +++ b/packages/SystemUI/res/layout/slice_permission_request.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> +<!-- Extends LinearLayout --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="vertical"> + + <TextView + android:id="@+id/text2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingStart="8dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@string/slice_permission_text_1" /> + + <TextView + android:id="@+id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingBottom="16dp" + android:text="@string/slice_permission_text_2" /> + + <CheckBox + android:id="@+id/slice_permission_checkbox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/slice_permission_checkbox" /> + +</LinearLayout> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 99ba36965231..199ccfcaeaa2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2067,5 +2067,21 @@ <string name="touch_filtered_warning">Because an app is obscuring a permission request, Settings can’t verify your response.</string> + <!-- Title of prompt requesting access to display slices [CHAR LIMIT=NONE] --> + <string name="slice_permission_title">Allow <xliff:g id="app" example="Example App">%1$s</xliff:g> to show <xliff:g id="app_2" example="Other Example App">%2$s</xliff:g> slices?</string> + + <!-- Description of what kind of access is given to a slice host [CHAR LIMIT=NONE] --> + <string name="slice_permission_text_1"> - It can read information from <xliff:g id="app" example="Example App">%1$s</xliff:g></string> + <!-- Description of what kind of access is given to a slice host [CHAR LIMIT=NONE] --> + <string name="slice_permission_text_2"> - It can take actions inside <xliff:g id="app" example="Example App">%1$s</xliff:g></string> + + <!-- Text on checkbox allowing the app to show slices from all apps [CHAR LIMIT=NONE] --> + <string name="slice_permission_checkbox">Allow <xliff:g id="app" example="Example App">%1$s</xliff:g> to show slices from any app</string> + + <!-- Option to grant the slice permission request on the screen [CHAR LIMIT=15] --> + <string name="slice_permission_allow">Allow</string> + + <!-- Option to grant the slice permission request on the screen [CHAR LIMIT=15] --> + <string name="slice_permission_deny">Deny</string> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java new file mode 100644 index 000000000000..302face14c1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java @@ -0,0 +1,88 @@ +/* + * 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.systemui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.slice.SliceManager; +import android.app.slice.SliceProvider; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.TextView; + +public class SlicePermissionActivity extends Activity implements OnClickListener, + OnDismissListener { + + private static final String TAG = "SlicePermissionActivity"; + + private CheckBox mAllCheckbox; + + private Uri mUri; + private String mCallingPkg; + private String mProviderPkg; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mUri = getIntent().getParcelableExtra(SliceProvider.EXTRA_BIND_URI); + mCallingPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PKG); + mProviderPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PROVIDER_PKG); + + try { + PackageManager pm = getPackageManager(); + CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm); + CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm); + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(getString(R.string.slice_permission_title, app1, app2)) + .setView(R.layout.slice_permission_request) + .setNegativeButton(R.string.slice_permission_deny, this) + .setPositiveButton(R.string.slice_permission_allow, this) + .setOnDismissListener(this) + .show(); + TextView t1 = dialog.getWindow().getDecorView().findViewById(R.id.text1); + t1.setText(getString(R.string.slice_permission_text_1, app2)); + TextView t2 = dialog.getWindow().getDecorView().findViewById(R.id.text2); + t2.setText(getString(R.string.slice_permission_text_2, app2)); + mAllCheckbox = dialog.getWindow().getDecorView().findViewById( + R.id.slice_permission_checkbox); + mAllCheckbox.setText(getString(R.string.slice_permission_checkbox, app1)); + } catch (NameNotFoundException e) { + Log.e(TAG, "Couldn't find package", e); + finish(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + getSystemService(SliceManager.class).grantPermissionFromUser(mUri, mCallingPkg, + mAllCheckbox.isChecked()); + } + finish(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } +} diff --git a/services/core/java/com/android/server/slice/PinnedSliceState.java b/services/core/java/com/android/server/slice/PinnedSliceState.java index cf930f5cc6b7..09f6da92939c 100644 --- a/services/core/java/com/android/server/slice/PinnedSliceState.java +++ b/services/core/java/com/android/server/slice/PinnedSliceState.java @@ -22,6 +22,7 @@ import android.content.ContentProviderClient; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -51,6 +52,8 @@ public class PinnedSliceState { private final ArraySet<ISliceListener> mListeners = new ArraySet<>(); @GuardedBy("mLock") private SliceSpec[] mSupportedSpecs = null; + @GuardedBy("mLock") + private final ArrayMap<ISliceListener, String> mPkgMap = new ArrayMap<>(); public PinnedSliceState(SliceManagerService service, Uri uri) { mService = service; @@ -102,17 +105,19 @@ public class PinnedSliceState { mService.getHandler().post(this::handleBind); } - public void addSliceListener(ISliceListener listener, SliceSpec[] specs) { + public void addSliceListener(ISliceListener listener, String pkg, SliceSpec[] specs) { synchronized (mLock) { if (mListeners.add(listener) && mListeners.size() == 1) { mService.listen(mUri); } + mPkgMap.put(listener, pkg); mergeSpecs(specs); } } public boolean removeSliceListener(ISliceListener listener) { synchronized (mLock) { + mPkgMap.remove(listener); if (mListeners.remove(listener) && mListeners.size() == 0) { mService.unlisten(mUri); } @@ -155,25 +160,16 @@ public class PinnedSliceState { } private void handleBind() { - Slice s; - try (ContentProviderClient client = getClient()) { - Bundle extras = new Bundle(); - extras.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri); - extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, - new ArrayList<>(Arrays.asList(mSupportedSpecs))); - final Bundle res; - try { - res = client.call(SliceProvider.METHOD_SLICE, null, extras); - } catch (RemoteException e) { - Log.e(TAG, "Unable to bind slice " + mUri, e); - return; - } - if (res == null) return; - Bundle.setDefusable(res, true); - s = res.getParcelable(SliceProvider.EXTRA_SLICE); - } + Slice cachedSlice = doBind(null); synchronized (mLock) { mListeners.removeIf(l -> { + Slice s = cachedSlice; + if (s == null || s.hasHint(Slice.HINT_CALLER_NEEDED)) { + s = doBind(mPkgMap.get(l)); + } + if (s == null) { + return true; + } try { l.onSliceUpdated(s); return false; @@ -189,6 +185,26 @@ public class PinnedSliceState { } } + private Slice doBind(String overridePkg) { + try (ContentProviderClient client = getClient()) { + Bundle extras = new Bundle(); + extras.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri); + extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, + new ArrayList<>(Arrays.asList(mSupportedSpecs))); + extras.putString(SliceProvider.EXTRA_OVERRIDE_PKG, overridePkg); + final Bundle res; + try { + res = client.call(SliceProvider.METHOD_SLICE, null, extras); + } catch (RemoteException e) { + Log.e(TAG, "Unable to bind slice " + mUri, e); + return null; + } + if (res == null) return null; + Bundle.setDefusable(res, true); + return res.getParcelable(SliceProvider.EXTRA_SLICE); + } + } + private void handleSendPinned() { try (ContentProviderClient client = getClient()) { Bundle b = new Bundle(); diff --git a/services/core/java/com/android/server/slice/SliceManagerService.java b/services/core/java/com/android/server/slice/SliceManagerService.java index 2d9e772a6b0c..ca7632c354d1 100644 --- a/services/core/java/com/android/server/slice/SliceManagerService.java +++ b/services/core/java/com/android/server/slice/SliceManagerService.java @@ -16,27 +16,35 @@ package com.android.server.slice; +import static android.content.ContentProvider.getUriWithoutUserId; import static android.content.ContentProvider.getUserIdFromUri; import static android.content.ContentProvider.maybeAddUserId; import android.Manifest.permission; +import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.ContentProviderHolder; +import android.app.IActivityManager; import android.app.slice.ISliceListener; import android.app.slice.ISliceManager; +import android.app.slice.SliceManager; import android.app.slice.SliceSpec; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.database.ContentObserver; import android.net.Uri; import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Process; import android.os.RemoteException; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -63,6 +71,8 @@ public class SliceManagerService extends ISliceManager.Stub { @GuardedBy("mLock") private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>(); + @GuardedBy("mLock") + private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>(); private final Handler mHandler; private final ContentObserver mObserver; @@ -111,7 +121,7 @@ public class SliceManagerService extends ISliceManager.Stub { verifyCaller(pkg); uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); enforceAccess(pkg, uri); - getOrCreatePinnedSlice(uri).addSliceListener(listener, specs); + getOrCreatePinnedSlice(uri).addSliceListener(listener, pkg, specs); } @Override @@ -156,6 +166,43 @@ public class SliceManagerService extends ISliceManager.Stub { return getPinnedSlice(uri).getSpecs(); } + @Override + public int checkSlicePermission(Uri uri, String pkg, int pid, int uid) throws RemoteException { + if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + return SliceManager.PERMISSION_GRANTED; + } + if (hasFullSliceAccess(pkg, uid)) { + return SliceManager.PERMISSION_GRANTED; + } + synchronized (mLock) { + if (mUserGrants.contains(new SliceGrant(uri, pkg))) { + return SliceManager.PERMISSION_USER_GRANTED; + } + } + return SliceManager.PERMISSION_DENIED; + } + + @Override + public void grantPermissionFromUser(Uri uri, String pkg, String callingPkg, boolean allSlices) { + verifyCaller(callingPkg); + getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS, + "Slice granting requires MANAGE_SLICE_PERMISSIONS"); + if (allSlices) { + // TODO: Manage full access grants. + } else { + synchronized (mLock) { + mUserGrants.add(new SliceGrant(uri, pkg)); + } + long ident = Binder.clearCallingIdentity(); + try { + mContext.getContentResolver().notifyChange(uri, null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + /// ----- internal code ----- void removePinnedSlice(Uri uri) { synchronized (mLock) { @@ -202,12 +249,45 @@ public class SliceManagerService extends ISliceManager.Stub { return mHandler; } - private void enforceAccess(String pkg, Uri uri) { - 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"); + private void enforceAccess(String pkg, Uri uri) throws RemoteException { int user = Binder.getCallingUserHandle().getIdentifier(); + // Check for default launcher/assistant. + if (!hasFullSliceAccess(pkg, Binder.getCallingUid())) { + try { + // Also allow things with uri access. + getContext().enforceUriPermission(uri, Binder.getCallingPid(), + Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires permission to the Uri"); + } catch (SecurityException e) { + // Last fallback (if the calling app owns the authority, then it can have access). + long ident = Binder.clearCallingIdentity(); + try { + IBinder token = new Binder(); + IActivityManager activityManager = ActivityManager.getService(); + ContentProviderHolder holder = null; + String providerName = getUriWithoutUserId(uri).getAuthority(); + try { + holder = activityManager.getContentProviderExternal( + providerName, getUserIdFromUri(uri, user), token); + if (holder == null || holder.info == null + || !Objects.equals(holder.info.packageName, pkg)) { + // No more fallbacks, no access. + throw e; + } + } finally { + if (holder != null && holder.provider != null) { + activityManager.removeContentProviderExternal(providerName, token); + } + } + } finally { + // I know, the double finally seems ugly, but seems safest for the identity. + Binder.restoreCallingIdentity(ident); + } + } + } + // Lastly check for any multi-userness. Any return statements above here will break this + // important check. if (getUserIdFromUri(uri, user) != user) { getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); @@ -230,8 +310,14 @@ public class SliceManagerService extends ISliceManager.Stub { } private boolean hasFullSliceAccess(String pkg, int userId) { - return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId) - || isGrantedFullAccess(pkg, userId); + long ident = Binder.clearCallingIdentity(); + try { + boolean ret = isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId) + || isGrantedFullAccess(pkg, userId); + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } } private boolean isAssistant(String pkg, int userId) { @@ -259,7 +345,8 @@ public class SliceManagerService extends ISliceManager.Stub { private boolean isDefaultHomeApp(String pkg, int userId) { String defaultHome = getDefaultHome(userId); - return Objects.equals(pkg, defaultHome); + + return pkg != null && Objects.equals(pkg, defaultHome); } // Based on getDefaultHome in ShortcutService. @@ -301,7 +388,7 @@ public class SliceManagerService extends ISliceManager.Stub { lastPriority = ri.priority; } } - return detected.getPackageName(); + return detected != null ? detected.getPackageName() : null; } finally { Binder.restoreCallingIdentity(token); } @@ -349,4 +436,26 @@ public class SliceManagerService extends ISliceManager.Stub { mService.onStopUser(userHandle); } } + + private class SliceGrant { + private final Uri mUri; + private final String mPkg; + + public SliceGrant(Uri uri, String pkg) { + mUri = uri; + mPkg = pkg; + } + + @Override + public int hashCode() { + return mUri.hashCode() + mPkg.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SliceGrant)) return false; + SliceGrant other = (SliceGrant) obj; + return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg); + } + } } diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml index f022dcf376a6..347557291427 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -25,6 +25,7 @@ <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" /> <application> <uses-library android:name="android.test.runner" /> diff --git a/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java index ce328c29f01c..aada68273af0 100644 --- a/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java +++ b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java @@ -149,7 +149,7 @@ public class PinnedSliceStateTest extends UiServiceTestCase { ISliceListener listener = mock(ISliceListener.class); assertFalse(mPinnedSliceManager.isPinned()); - mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + mPinnedSliceManager.addSliceListener(listener, mContext.getPackageName(), FIRST_SPECS); assertTrue(mPinnedSliceManager.isPinned()); assertTrue(mPinnedSliceManager.removeSliceListener(listener)); @@ -162,9 +162,9 @@ public class PinnedSliceStateTest extends UiServiceTestCase { ISliceListener listener2 = mock(ISliceListener.class); assertFalse(mPinnedSliceManager.isPinned()); - mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + mPinnedSliceManager.addSliceListener(listener, mContext.getPackageName(), FIRST_SPECS); assertTrue(mPinnedSliceManager.isPinned()); - mPinnedSliceManager.addSliceListener(listener2, FIRST_SPECS); + mPinnedSliceManager.addSliceListener(listener2, mContext.getPackageName(), FIRST_SPECS); assertFalse(mPinnedSliceManager.removeSliceListener(listener)); assertTrue(mPinnedSliceManager.removeSliceListener(listener2)); @@ -176,7 +176,7 @@ public class PinnedSliceStateTest extends UiServiceTestCase { ISliceListener listener = mock(ISliceListener.class); assertFalse(mPinnedSliceManager.isPinned()); - mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + mPinnedSliceManager.addSliceListener(listener, mContext.getPackageName(), FIRST_SPECS); assertTrue(mPinnedSliceManager.isPinned()); mPinnedSliceManager.pin("pkg", FIRST_SPECS); @@ -199,7 +199,7 @@ public class PinnedSliceStateTest extends UiServiceTestCase { assertFalse(mPinnedSliceManager.isPinned()); - mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + mPinnedSliceManager.addSliceListener(listener, mContext.getPackageName(), FIRST_SPECS); mPinnedSliceManager.onChange(); TestableLooper.get(this).processAllMessages(); |