diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl index 5f0e542f4b123..4461b16fe15c7 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 6093a4a02fbe7..5bd3440d09f81 100644 --- a/core/java/android/app/slice/Slice.java +++ b/core/java/android/app/slice/Slice.java @@ -155,6 +155,13 @@ public final class Slice implements Parcelable { * content associated with this slice. */ 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. */ diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java index 74864cb1a3717..09c420c3e66a9 100644 --- a/core/java/android/app/slice/SliceManager.java +++ b/core/java/android/app/slice/SliceManager.java @@ -53,11 +53,33 @@ 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, 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 */ @@ -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; @@ -357,6 +379,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. */ diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java index aa41f14d8cb50..8ffacf5280c14 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. *

@@ -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 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 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 supportedSpecs) { + private Slice handleBindSlice(Uri sliceUri, List 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 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 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 e4e46f6d0a8f5..990c574f5f6c6 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3192,10 +3192,14 @@ + + + +

Not for use by third-party applications.--> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 2cfe919fb8f58..0c844c9deb9d1 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4817,4 +4817,8 @@ Launch anyway Uninstall harmful app? + + + %1$s wants to show %2$s slices diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5309115f37bbb..03a800d8bd6f4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3227,4 +3227,5 @@ + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index aa2cdbb7730f0..80ac82576d130 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -121,7 +121,7 @@ - + @@ -435,6 +435,16 @@ android:launchMode="singleTop" androidprv:alwaysFocusable="true" /> + + + + + + + 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 0000000000000..cdb2a91a73d2e --- /dev/null +++ b/packages/SystemUI/res/layout/slice_permission_request.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 99ba369652314..199ccfcaeaa2f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2067,5 +2067,21 @@ Because an app is obscuring a permission request, Settings can’t verify your response. + + Allow %1$s to show %2$s slices? + + + - It can read information from %1$s + + - It can take actions inside %1$s + + + Allow %1$s to show slices from any app + + + Allow + + + Deny 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 0000000000000..302face14c1a7 --- /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 cf930f5cc6b79..09f6da92939cf 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 mListeners = new ArraySet<>(); @GuardedBy("mLock") private SliceSpec[] mSupportedSpecs = null; + @GuardedBy("mLock") + private final ArrayMap 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 2d9e772a6b0c6..ca7632c354d12 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 mPinnedSlicesByUri = new ArrayMap<>(); + @GuardedBy("mLock") + private final ArraySet 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 f022dcf376a66..3475572914277 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -25,6 +25,7 @@ + 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 ce328c29f01c6..aada68273af07 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();