Slice permissions++
New permissions system for slices. Its up a little disk space at the
optimization of CPU/Mem usage, and most importantly doesn't have a
limit on the number that can exist.
Test: runtest --path frameworks/base/services/tests/uiservicestests
Test: runtest --path cts/tests/tests/slices
Test: manual grant through SliceBrowser
Bug: 77323760
Change-Id: Ie601bca042a7b28113a511383ca7e3a35c7c8c71
diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl
index a2aaf12..69852f3 100644
--- a/core/java/android/app/slice/ISliceManager.aidl
+++ b/core/java/android/app/slice/ISliceManager.aidl
@@ -25,11 +25,15 @@
void unpinSlice(String pkg, in Uri uri, in IBinder token);
boolean hasSliceAccess(String pkg);
SliceSpec[] getPinnedSpecs(in Uri uri, String pkg);
- int checkSlicePermission(in Uri uri, String pkg, int pid, int uid,
- in String[] autoGrantPermissions);
- void grantPermissionFromUser(in Uri uri, String pkg, String callingPkg, boolean allSlices);
Uri[] getPinnedSlices(String pkg);
byte[] getBackupPayload(int user);
void applyRestore(in byte[] payload, int user);
+
+ // Perms.
+ void grantSlicePermission(String callingPkg, String toPkg, in Uri uri);
+ void revokeSlicePermission(String callingPkg, String toPkg, in Uri uri);
+ int checkSlicePermission(in Uri uri, String pkg, int pid, int uid,
+ in String[] autoGrantPermissions);
+ void grantPermissionFromUser(in Uri uri, String pkg, String callingPkg, boolean allSlices);
}
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index 0285e9f..3b73174 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -16,6 +16,8 @@
package android.app.slice;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
@@ -101,22 +103,6 @@
private final IBinder mToken = new Binder();
/**
- * 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 {
@@ -417,9 +403,11 @@
* @see #grantSlicePermission(String, Uri)
*/
public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
- // TODO: Switch off Uri permissions.
- return mContext.checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ try {
+ return mService.checkSlicePermission(uri, null, pid, uid, null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -431,11 +419,11 @@
* @see #revokeSlicePermission
*/
public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.grantUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ try {
+ mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -453,11 +441,11 @@
* @see #grantSlicePermission
*/
public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.revokeUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ try {
+ mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -478,16 +466,6 @@
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);
- // Notify a change has happened because we just granted a permission.
- mContext.getContentResolver().notifyChange(uri, null);
- }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/services/core/java/com/android/server/slice/DirtyTracker.java b/services/core/java/com/android/server/slice/DirtyTracker.java
new file mode 100644
index 0000000..4288edc
--- /dev/null
+++ b/services/core/java/com/android/server/slice/DirtyTracker.java
@@ -0,0 +1,35 @@
+/*
+ * 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.server.slice;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * A parent object that cares when a Persistable changes and will schedule a serialization
+ * in response to the onPersistableDirty callback.
+ */
+public interface DirtyTracker {
+ void onPersistableDirty(Persistable obj);
+
+ /**
+ * An object that can be written to XML.
+ */
+ interface Persistable {
+ String getFileName();
+ void writeTo(XmlSerializer out) throws IOException;
+ }
+}
diff --git a/services/core/java/com/android/server/slice/SliceClientPermissions.java b/services/core/java/com/android/server/slice/SliceClientPermissions.java
new file mode 100644
index 0000000..e461e0d
--- /dev/null
+++ b/services/core/java/com/android/server/slice/SliceClientPermissions.java
@@ -0,0 +1,354 @@
+/*
+ * 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.server.slice;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SliceClientPermissions implements DirtyTracker, Persistable {
+
+ private static final String TAG = "SliceClientPermissions";
+
+ static final String TAG_CLIENT = "client";
+ private static final String TAG_AUTHORITY = "authority";
+ private static final String TAG_PATH = "path";
+ private static final String NAMESPACE = null;
+
+ private static final String ATTR_PKG = "pkg";
+ private static final String ATTR_AUTHORITY = "authority";
+ private static final String ATTR_FULL_ACCESS = "fullAccess";
+
+ private final PkgUser mPkg;
+ // Keyed off (authority, userId) rather than the standard (pkg, userId)
+ private final ArrayMap<PkgUser, SliceAuthority> mAuths = new ArrayMap<>();
+ private final DirtyTracker mTracker;
+ private boolean mHasFullAccess;
+
+ public SliceClientPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ public synchronized Collection<SliceAuthority> getAuthorities() {
+ return new ArrayList<>(mAuths.values());
+ }
+
+ public synchronized SliceAuthority getOrCreateAuthority(PkgUser authority, PkgUser provider) {
+ SliceAuthority ret = mAuths.get(authority);
+ if (ret == null) {
+ ret = new SliceAuthority(authority.getPkg(), provider, this);
+ mAuths.put(authority, ret);
+ onPersistableDirty(ret);
+ }
+ return ret;
+ }
+
+ public synchronized SliceAuthority getAuthority(PkgUser authority) {
+ return mAuths.get(authority);
+ }
+
+ public boolean hasFullAccess() {
+ return mHasFullAccess;
+ }
+
+ public void setHasFullAccess(boolean hasFullAccess) {
+ if (mHasFullAccess == hasFullAccess) return;
+ mHasFullAccess = hasFullAccess;
+ mTracker.onPersistableDirty(this);
+ }
+
+ public void removeAuthority(String authority, int userId) {
+ if (mAuths.remove(new PkgUser(authority, userId)) != null) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized boolean hasPermission(Uri uri, int userId) {
+ if (!Objects.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme())) return false;
+ SliceAuthority authority = getAuthority(new PkgUser(uri.getAuthority(), userId));
+ return authority != null && authority.hasPermission(uri.getPathSegments());
+ }
+
+ public void grantUri(Uri uri, PkgUser providerPkg) {
+ SliceAuthority authority = getOrCreateAuthority(
+ new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+ providerPkg);
+ authority.addPath(uri.getPathSegments());
+ }
+
+ public void revokeUri(Uri uri, PkgUser providerPkg) {
+ SliceAuthority authority = getOrCreateAuthority(
+ new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+ providerPkg);
+ authority.removePath(uri.getPathSegments());
+ }
+
+ public void clear() {
+ if (!mHasFullAccess && mAuths.isEmpty()) return;
+ mHasFullAccess = false;
+ mAuths.clear();
+ onPersistableDirty(this);
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mTracker.onPersistableDirty(this);
+ }
+
+ @Override
+ public String getFileName() {
+ return getFileName(mPkg);
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ out.startTag(NAMESPACE, TAG_CLIENT);
+ out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+ out.attribute(NAMESPACE, ATTR_FULL_ACCESS, mHasFullAccess ? "1" : "0");
+
+ final int N = mAuths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_AUTHORITY);
+ out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+ out.attribute(NAMESPACE, ATTR_PKG, mAuths.valueAt(i).mPkg.toString());
+
+ mAuths.valueAt(i).writeTo(out);
+
+ out.endTag(NAMESPACE, TAG_AUTHORITY);
+ }
+
+ out.endTag(NAMESPACE, TAG_CLIENT);
+ }
+
+ public static SliceClientPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+ throws XmlPullParserException, IOException {
+ // Get to the beginning of the provider.
+ while (parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_CLIENT.equals(parser.getName())) {
+ parser.next();
+ }
+ int depth = parser.getDepth();
+ PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceClientPermissions provider = new SliceClientPermissions(pkgUser, tracker);
+ String fullAccess = parser.getAttributeValue(NAMESPACE, ATTR_FULL_ACCESS);
+ if (fullAccess == null) {
+ fullAccess = "0";
+ }
+ provider.mHasFullAccess = Integer.parseInt(fullAccess) != 0;
+ parser.next();
+
+ while (parser.getDepth() > depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_AUTHORITY.equals(parser.getName())) {
+ try {
+ PkgUser pkg = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceAuthority authority = new SliceAuthority(
+ parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), pkg, provider);
+ authority.readFrom(parser);
+ provider.mAuths.put(new PkgUser(authority.getAuthority(), pkg.getUserId()),
+ authority);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Couldn't read PkgUser", e);
+ }
+ }
+
+ parser.next();
+ }
+ return provider;
+ }
+
+ public static String getFileName(PkgUser pkg) {
+ return String.format("client_%s", pkg.toString());
+ }
+
+ public static class SliceAuthority implements Persistable {
+ public static final String DELIMITER = "/";
+ private final String mAuthority;
+ private final DirtyTracker mTracker;
+ private final PkgUser mPkg;
+ private final ArraySet<String[]> mPaths = new ArraySet<>();
+
+ public SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker) {
+ mAuthority = authority;
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public String getAuthority() {
+ return mAuthority;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ void addPath(List<String> path) {
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(existing, pathSegs)) {
+ // Nothing to add here.
+ return;
+ }
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ mPaths.removeAt(i);
+ }
+ }
+ mPaths.add(pathSegs);
+ mTracker.onPersistableDirty(this);
+ }
+
+ void removePath(List<String> path) {
+ boolean changed = false;
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ changed = true;
+ mPaths.removeAt(i);
+ }
+ }
+ if (changed) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized Collection<String[]> getPaths() {
+ return new ArraySet<>(mPaths);
+ }
+
+ public boolean hasPermission(List<String> path) {
+ for (String[] p : mPaths) {
+ if (isPathPrefixMatch(p, path.toArray(new String[path.size()]))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+ final int prefixSize = prefix.length;
+ if (path.length < prefixSize) return false;
+
+ for (int i = 0; i < prefixSize; i++) {
+ if (!Objects.equals(path[i], prefix[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public String getFileName() {
+ return null;
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ final int N = mPaths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_PATH);
+ out.text(encodeSegments(mPaths.valueAt(i)));
+ out.endTag(NAMESPACE, TAG_PATH);
+ }
+ }
+
+ public synchronized void readFrom(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ parser.next();
+ int depth = parser.getDepth();
+ while (parser.getDepth() >= depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_PATH.equals(parser.getName())) {
+ mPaths.add(decodeSegments(parser.nextText()));
+ }
+ parser.next();
+ }
+ }
+
+ private String encodeSegments(String[] s) {
+ String[] out = new String[s.length];
+ for (int i = 0; i < s.length; i++) {
+ out[i] = Uri.encode(s[i]);
+ }
+ return TextUtils.join(DELIMITER, out);
+ }
+
+ private String[] decodeSegments(String s) {
+ String[] sets = s.split(DELIMITER, -1);
+ for (int i = 0; i < sets.length; i++) {
+ sets[i] = Uri.decode(sets[i]);
+ }
+ return sets;
+ }
+
+ /**
+ * Only for testing, no deep equality of these are done normally.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ SliceAuthority other = (SliceAuthority) obj;
+ if (mPaths.size() != other.mPaths.size()) return false;
+ ArrayList<String[]> p1 = new ArrayList<>(mPaths);
+ ArrayList<String[]> p2 = new ArrayList<>(other.mPaths);
+ p1.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+ p2.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+ for (int i = 0; i < p1.size(); i++) {
+ String[] a1 = p1.get(i);
+ String[] a2 = p2.get(i);
+ if (a1.length != a2.length) return false;
+ for (int j = 0; j < a1.length; j++) {
+ if (!Objects.equals(a1[j], a2[j])) return false;
+ }
+ }
+ return Objects.equals(mAuthority, other.mAuthority)
+ && Objects.equals(mPkg, other.mPkg);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths));
+ }
+
+ private String pathToString(ArraySet<String[]> paths) {
+ return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s))
+ .collect(Collectors.toList()));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/slice/SliceManagerService.java b/services/core/java/com/android/server/slice/SliceManagerService.java
index fd0b6f1..b7b9612 100644
--- a/services/core/java/com/android/server/slice/SliceManagerService.java
+++ b/services/core/java/com/android/server/slice/SliceManagerService.java
@@ -31,14 +31,15 @@
import android.app.ContentProviderHolder;
import android.app.IActivityManager;
import android.app.slice.ISliceManager;
-import android.app.slice.SliceManager;
import android.app.slice.SliceSpec;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.net.Uri;
@@ -51,7 +52,6 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
-import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml.Encoding;
@@ -72,7 +72,6 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -91,13 +90,9 @@
@GuardedBy("mLock")
private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
- @GuardedBy("mLock")
- private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>();
private final Handler mHandler;
- @GuardedBy("mSliceAccessFile")
- private final AtomicFile mSliceAccessFile;
- @GuardedBy("mAccessList")
- private final SliceFullAccessList mAccessList;
+
+ private final SlicePermissionManager mPermissions;
private final UsageStatsManagerInternal mAppUsageStats;
public SliceManagerService(Context context) {
@@ -113,24 +108,9 @@
mAssistUtils = new AssistUtils(context);
mHandler = new Handler(looper);
- final File systemDir = new File(Environment.getDataDirectory(), "system");
- mSliceAccessFile = new AtomicFile(new File(systemDir, "slice_access.xml"));
- mAccessList = new SliceFullAccessList(mContext);
mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
- synchronized (mSliceAccessFile) {
- if (!mSliceAccessFile.exists()) return;
- try {
- InputStream input = mSliceAccessFile.openRead();
- XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
- parser.setInput(input, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.readXml(parser);
- }
- } catch (IOException | XmlPullParserException e) {
- Slog.d(TAG, "Can't read slice access file", e);
- }
- }
+ mPermissions = new SlicePermissionManager(mContext, looper);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
@@ -211,26 +191,58 @@
}
@Override
+ public void grantSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+ verifyCaller(pkg);
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ enforceOwner(pkg, uri, user);
+ mPermissions.grantSliceAccess(toPkg, user, pkg, user, uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+ verifyCaller(pkg);
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ enforceOwner(pkg, uri, user);
+ mPermissions.revokeSliceAccess(toPkg, user, pkg, user, uri);
+ }
+
+ @Override
public int checkSlicePermission(Uri uri, String pkg, int pid, int uid,
- String[] autoGrantPermissions) throws RemoteException {
+ String[] autoGrantPermissions) {
+ int userId = UserHandle.getUserId(uid);
+ if (pkg == null) {
+ for (String p : mContext.getPackageManager().getPackagesForUid(uid)) {
+ if (checkSlicePermission(uri, p, pid, uid, autoGrantPermissions)
+ == PERMISSION_GRANTED) {
+ return PERMISSION_GRANTED;
+ }
+ }
+ return PERMISSION_DENIED;
+ }
+ if (hasFullSliceAccess(pkg, userId)) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ if (mPermissions.hasPermission(pkg, userId, uri)) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ if (autoGrantPermissions != null) {
+ // Need to own the Uri to call in with permissions to grant.
+ enforceOwner(pkg, uri, userId);
+ for (String perm : autoGrantPermissions) {
+ if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
+ int providerUser = ContentProvider.getUserIdFromUri(uri, userId);
+ String providerPkg = getProviderPkg(uri, providerUser);
+ mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, uri);
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ }
+ // Fallback to allowing uri permissions through.
if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
== PERMISSION_GRANTED) {
- return SliceManager.PERMISSION_GRANTED;
+ return PackageManager.PERMISSION_GRANTED;
}
- if (hasFullSliceAccess(pkg, UserHandle.getUserId(uid))) {
- return SliceManager.PERMISSION_GRANTED;
- }
- for (String perm : autoGrantPermissions) {
- if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
- return SliceManager.PERMISSION_USER_GRANTED;
- }
- }
- synchronized (mLock) {
- if (mUserGrants.contains(new SliceGrant(uri, pkg, UserHandle.getUserId(uid)))) {
- return SliceManager.PERMISSION_USER_GRANTED;
- }
- }
- return SliceManager.PERMISSION_DENIED;
+ return PackageManager.PERMISSION_DENIED;
}
@Override
@@ -238,16 +250,17 @@
verifyCaller(callingPkg);
getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
"Slice granting requires MANAGE_SLICE_PERMISSIONS");
+ int userId = Binder.getCallingUserHandle().getIdentifier();
if (allSlices) {
- synchronized (mAccessList) {
- mAccessList.grantFullAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
- }
- mHandler.post(mSaveAccessList);
+ mPermissions.grantFullAccess(pkg, userId);
} else {
- synchronized (mLock) {
- mUserGrants.add(new SliceGrant(uri, pkg,
- Binder.getCallingUserHandle().getIdentifier()));
- }
+ // When granting, grant to all slices in the provider.
+ Uri grantUri = uri.buildUpon()
+ .path("")
+ .build();
+ int providerUser = ContentProvider.getUserIdFromUri(grantUri, userId);
+ String providerPkg = getProviderPkg(grantUri, providerUser);
+ mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, grantUri);
}
long ident = Binder.clearCallingIdentity();
try {
@@ -268,19 +281,17 @@
Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
return null;
}
- synchronized(mSliceAccessFile) {
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
- out.setOutput(baos, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.writeXml(out, user);
- }
- out.flush();
- return baos.toByteArray();
- } catch (IOException | XmlPullParserException e) {
- Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
- }
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(baos, Encoding.UTF_8.name());
+
+ mPermissions.writeBackup(out);
+
+ out.flush();
+ return baos.toByteArray();
+ } catch (IOException | XmlPullParserException e) {
+ Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
}
return null;
}
@@ -299,27 +310,21 @@
Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
return;
}
- synchronized(mSliceAccessFile) {
- final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
- try {
- XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
- parser.setInput(bais, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.readXml(parser);
- }
- mHandler.post(mSaveAccessList);
- } catch (NumberFormatException | XmlPullParserException | IOException e) {
- Slog.w(TAG, "applyRestore: error reading payload", e);
- }
+ final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
+ try {
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(bais, Encoding.UTF_8.name());
+ mPermissions.readRestore(parser);
+ } catch (NumberFormatException | XmlPullParserException | IOException e) {
+ Slog.w(TAG, "applyRestore: error reading payload", e);
}
}
/// ----- internal code -----
- private void removeFullAccess(String pkg, int userId) {
- synchronized (mAccessList) {
- mAccessList.removeGrant(pkg, userId);
+ private void enforceOwner(String pkg, Uri uri, int user) {
+ if (!Objects.equals(getProviderPkg(uri, user), pkg) || pkg == null) {
+ throw new SecurityException("Caller must own " + uri);
}
- mHandler.post(mSaveAccessList);
}
protected void removePinnedSlice(Uri uri) {
@@ -368,19 +373,7 @@
}
protected int checkAccess(String pkg, Uri uri, int uid, int pid) {
- int user = UserHandle.getUserId(uid);
- // Check for default launcher/assistant.
- if (!hasFullSliceAccess(pkg, user)) {
- // Also allow things with uri access.
- if (getContext().checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PERMISSION_GRANTED) {
- // Last fallback (if the calling app owns the authority, then it can have access).
- if (!Objects.equals(getProviderPkg(uri, user), pkg)) {
- return PERMISSION_DENIED;
- }
- }
- }
- return PERMISSION_GRANTED;
+ return checkSlicePermission(uri, pkg, uid, pid, null);
}
private String getProviderPkg(Uri uri, int user) {
@@ -425,15 +418,11 @@
private void enforceAccess(String pkg, Uri uri) throws RemoteException {
if (checkAccess(pkg, uri, Binder.getCallingUid(), Binder.getCallingPid())
!= PERMISSION_GRANTED) {
- throw new SecurityException("Access to slice " + uri + " is required");
- }
- enforceCrossUser(pkg, uri);
- }
-
- private void enforceFullAccess(String pkg, String name, Uri uri) {
- int user = Binder.getCallingUserHandle().getIdentifier();
- if (!hasFullSliceAccess(pkg, user)) {
- throw new SecurityException(String.format("Call %s requires full slice access", name));
+ int userId = ContentProvider.getUserIdFromUri(uri,
+ Binder.getCallingUserHandle().getIdentifier());
+ if (!Objects.equals(pkg, getProviderPkg(uri, userId))) {
+ throw new SecurityException("Access to slice " + uri + " is required");
+ }
}
enforceCrossUser(pkg, uri);
}
@@ -513,9 +502,7 @@
}
private boolean isGrantedFullAccess(String pkg, int userId) {
- synchronized (mAccessList) {
- return mAccessList.hasFullAccess(pkg, userId);
- }
+ return mPermissions.hasFullAccess(pkg, userId);
}
private static ServiceThread createHandler() {
@@ -525,34 +512,6 @@
return handlerThread;
}
- private final Runnable mSaveAccessList = new Runnable() {
- @Override
- public void run() {
- synchronized (mSliceAccessFile) {
- final FileOutputStream stream;
- try {
- stream = mSliceAccessFile.startWrite();
- } catch (IOException e) {
- Slog.w(TAG, "Failed to save access file", e);
- return;
- }
-
- try {
- XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
- out.setOutput(stream, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.writeXml(out, UserHandle.USER_ALL);
- }
- out.flush();
- mSliceAccessFile.finishWrite(stream);
- } catch (IOException | XmlPullParserException e) {
- Slog.w(TAG, "Failed to save access file, restoring backup", e);
- mSliceAccessFile.failWrite(stream);
- }
- }
- }
- };
-
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -572,11 +531,11 @@
final boolean replacing =
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
if (!replacing) {
- removeFullAccess(pkg, userId);
+ mPermissions.removePkg(pkg, userId);
}
break;
case Intent.ACTION_PACKAGE_DATA_CLEARED:
- removeFullAccess(pkg, userId);
+ mPermissions.removePkg(pkg, userId);
break;
}
}
diff --git a/services/core/java/com/android/server/slice/SlicePermissionManager.java b/services/core/java/com/android/server/slice/SlicePermissionManager.java
new file mode 100644
index 0000000..d25ec89
--- /dev/null
+++ b/services/core/java/com/android/server/slice/SlicePermissionManager.java
@@ -0,0 +1,432 @@
+/*
+ * 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.server.slice;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml.Encoding;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.XmlUtils;
+import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+public class SlicePermissionManager implements DirtyTracker {
+
+ private static final String TAG = "SlicePermissionManager";
+
+ /**
+ * The amount of time we'll cache a SliceProviderPermissions or SliceClientPermissions
+ * in case they are used again.
+ */
+ private static final long PERMISSION_CACHE_PERIOD = 5 * DateUtils.MINUTE_IN_MILLIS;
+
+ /**
+ * The amount of time we delay flushing out permission changes to disk because they usually
+ * come in short bursts.
+ */
+ private static final long WRITE_GRACE_PERIOD = 500;
+
+ private static final String SLICE_DIR = "slice";
+
+ // If/when this bumps again we'll need to write it out in the disk somewhere.
+ // Currently we don't have a central file for this in version 2 and there is no
+ // reason to add one until we actually have incompatible version bumps.
+ // This does however block us from reading backups from P-DP1 which may contain
+ // a very different XML format for perms.
+ static final int DB_VERSION = 2;
+
+ private static final String TAG_LIST = "slice-access-list";
+ private final String ATT_VERSION = "version";
+
+ private final File mSliceDir;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>();
+ private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>();
+ private final ArraySet<Persistable> mDirty = new ArraySet<>();
+
+ @VisibleForTesting
+ SlicePermissionManager(Context context, Looper looper, File sliceDir) {
+ mContext = context;
+ mHandler = new H(looper);
+ mSliceDir = sliceDir;
+ }
+
+ public SlicePermissionManager(Context context, Looper looper) {
+ this(context, looper, new File(Environment.getDataDirectory(), "system/" + SLICE_DIR));
+ }
+
+ public void grantFullAccess(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceClientPermissions client = getClient(pkgUser);
+ client.setHasFullAccess(true);
+ }
+
+ public void grantSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+ Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+ SliceClientPermissions client = getClient(pkgUser);
+ client.grantUri(uri, providerPkgUser);
+
+ SliceProviderPermissions provider = getProvider(providerPkgUser);
+ provider.getOrCreateAuthority(ContentProvider.getUriWithoutUserId(uri).getAuthority())
+ .addPkg(pkgUser);
+ }
+
+ public void revokeSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+ Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+ SliceClientPermissions client = getClient(pkgUser);
+ client.revokeUri(uri, providerPkgUser);
+ }
+
+ public void removePkg(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceProviderPermissions provider = getProvider(pkgUser);
+
+ for (SliceAuthority authority : provider.getAuthorities()) {
+ for (PkgUser p : authority.getPkgs()) {
+ getClient(p).removeAuthority(authority.getAuthority(), userId);
+ }
+ }
+ SliceClientPermissions client = getClient(pkgUser);
+ client.clear();
+ mHandler.obtainMessage(H.MSG_REMOVE, pkgUser);
+ }
+
+ public boolean hasFullAccess(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ return getClient(pkgUser).hasFullAccess();
+ }
+
+ public boolean hasPermission(String pkg, int userId, Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceClientPermissions client = getClient(pkgUser);
+ int providerUserId = ContentProvider.getUserIdFromUri(uri, userId);
+ return client.hasFullAccess()
+ || client.hasPermission(ContentProvider.getUriWithoutUserId(uri), providerUserId);
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mHandler.removeMessages(H.MSG_PERSIST);
+ mHandler.obtainMessage(H.MSG_ADD_DIRTY, obj).sendToTarget();
+ mHandler.sendEmptyMessageDelayed(H.MSG_PERSIST, WRITE_GRACE_PERIOD);
+ }
+
+ public void writeBackup(XmlSerializer out) throws IOException, XmlPullParserException {
+ synchronized (this) {
+ out.startTag(null, TAG_LIST);
+ out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
+
+ // Don't do anything with changes from the backup, because there shouldn't be any.
+ DirtyTracker tracker = obj -> { };
+ if (mHandler.hasMessages(H.MSG_PERSIST)) {
+ mHandler.removeMessages(H.MSG_PERSIST);
+ handlePersist();
+ }
+ for (String file : new File(mSliceDir.getAbsolutePath()).list()) {
+ if (file.isEmpty()) continue;
+ try (ParserHolder parser = getParser(file)) {
+ Persistable p;
+ while (parser.parser.getEventType() != XmlPullParser.START_TAG) {
+ parser.parser.next();
+ }
+ if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) {
+ p = SliceClientPermissions.createFrom(parser.parser, tracker);
+ } else {
+ p = SliceProviderPermissions.createFrom(parser.parser, tracker);
+ }
+ p.writeTo(out);
+ }
+ }
+
+ out.endTag(null, TAG_LIST);
+ }
+ }
+
+ public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException {
+ synchronized (this) {
+ while ((parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_LIST.equals(parser.getName()))
+ && parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ parser.next();
+ }
+ int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
+ if (xmlVersion < DB_VERSION) {
+ // No conversion support right now.
+ return;
+ }
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) {
+ SliceClientPermissions client = SliceClientPermissions.createFrom(parser,
+ this);
+ synchronized (mCachedClients) {
+ mCachedClients.put(client.getPkg(), client);
+ }
+ onPersistableDirty(client);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()),
+ PERMISSION_CACHE_PERIOD);
+ } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) {
+ SliceProviderPermissions provider = SliceProviderPermissions.createFrom(
+ parser, this);
+ synchronized (mCachedProviders) {
+ mCachedProviders.put(provider.getPkg(), provider);
+ }
+ onPersistableDirty(provider);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()),
+ PERMISSION_CACHE_PERIOD);
+ } else {
+ parser.next();
+ }
+ } else {
+ parser.next();
+ }
+ }
+ }
+ }
+
+ private SliceClientPermissions getClient(PkgUser pkgUser) {
+ SliceClientPermissions client;
+ synchronized (mCachedClients) {
+ client = mCachedClients.get(pkgUser);
+ }
+ if (client == null) {
+ try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) {
+ client = SliceClientPermissions.createFrom(parser.parser, this);
+ synchronized (mCachedClients) {
+ mCachedClients.put(pkgUser, client);
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser),
+ PERMISSION_CACHE_PERIOD);
+ return client;
+ } catch (FileNotFoundException e) {
+ // No client exists yet.
+ } catch (IOException e) {
+ Log.e(TAG, "Can't read client", e);
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Can't read client", e);
+ }
+ // Can't read or no permissions exist, create a clean object.
+ client = new SliceClientPermissions(pkgUser, this);
+ }
+ return client;
+ }
+
+ private SliceProviderPermissions getProvider(PkgUser pkgUser) {
+ SliceProviderPermissions provider;
+ synchronized (mCachedProviders) {
+ provider = mCachedProviders.get(pkgUser);
+ }
+ if (provider == null) {
+ try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) {
+ provider = SliceProviderPermissions.createFrom(parser.parser, this);
+ synchronized (mCachedProviders) {
+ mCachedProviders.put(pkgUser, provider);
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser),
+ PERMISSION_CACHE_PERIOD);
+ return provider;
+ } catch (FileNotFoundException e) {
+ // No provider exists yet.
+ } catch (IOException e) {
+ Log.e(TAG, "Can't read provider", e);
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Can't read provider", e);
+ }
+ // Can't read or no permissions exist, create a clean object.
+ provider = new SliceProviderPermissions(pkgUser, this);
+ }
+ return provider;
+ }
+
+ private ParserHolder getParser(String fileName)
+ throws FileNotFoundException, XmlPullParserException {
+ AtomicFile file = getFile(fileName);
+ ParserHolder holder = new ParserHolder();
+ holder.input = file.openRead();
+ holder.parser = XmlPullParserFactory.newInstance().newPullParser();
+ holder.parser.setInput(holder.input, Encoding.UTF_8.name());
+ return holder;
+ }
+
+ private AtomicFile getFile(String fileName) {
+ if (!mSliceDir.exists()) {
+ mSliceDir.mkdir();
+ }
+ return new AtomicFile(new File(mSliceDir, fileName));
+ }
+
+ private void handlePersist() {
+ synchronized (this) {
+ for (Persistable persistable : mDirty) {
+ AtomicFile file = getFile(persistable.getFileName());
+ final FileOutputStream stream;
+ try {
+ stream = file.startWrite();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save access file", e);
+ return;
+ }
+
+ try {
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(stream, Encoding.UTF_8.name());
+
+ persistable.writeTo(out);
+
+ out.flush();
+ file.finishWrite(stream);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.w(TAG, "Failed to save access file, restoring backup", e);
+ file.failWrite(stream);
+ }
+ }
+ mDirty.clear();
+ }
+ }
+
+ private void handleRemove(PkgUser pkgUser) {
+ getFile(SliceClientPermissions.getFileName(pkgUser)).delete();
+ getFile(SliceProviderPermissions.getFileName(pkgUser)).delete();
+ mDirty.remove(mCachedClients.remove(pkgUser));
+ mDirty.remove(mCachedProviders.remove(pkgUser));
+ }
+
+ private final class H extends Handler {
+ private static final int MSG_ADD_DIRTY = 1;
+ private static final int MSG_PERSIST = 2;
+ private static final int MSG_REMOVE = 3;
+ private static final int MSG_CLEAR_CLIENT = 4;
+ private static final int MSG_CLEAR_PROVIDER = 5;
+
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ADD_DIRTY:
+ mDirty.add((Persistable) msg.obj);
+ break;
+ case MSG_PERSIST:
+ handlePersist();
+ break;
+ case MSG_REMOVE:
+ handleRemove((PkgUser) msg.obj);
+ break;
+ case MSG_CLEAR_CLIENT:
+ synchronized (mCachedClients) {
+ mCachedClients.remove(msg.obj);
+ }
+ break;
+ case MSG_CLEAR_PROVIDER:
+ synchronized (mCachedProviders) {
+ mCachedProviders.remove(msg.obj);
+ }
+ break;
+ }
+ }
+ }
+
+ public static class PkgUser {
+ private static final String SEPARATOR = "@";
+ private static final String FORMAT = "%s" + SEPARATOR + "%d";
+ private final String mPkg;
+ private final int mUserId;
+
+ public PkgUser(String pkg, int userId) {
+ mPkg = pkg;
+ mUserId = userId;
+ }
+
+ public PkgUser(String pkgUserStr) throws IllegalArgumentException {
+ try {
+ String[] vals = pkgUserStr.split(SEPARATOR, 2);
+ mPkg = vals[0];
+ mUserId = Integer.parseInt(vals[1]);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public String getPkg() {
+ return mPkg;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPkg.hashCode() + mUserId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ PkgUser other = (PkgUser) obj;
+ return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(FORMAT, mPkg, mUserId);
+ }
+ }
+
+ private class ParserHolder implements AutoCloseable {
+
+ private InputStream input;
+ private XmlPullParser parser;
+
+ @Override
+ public void close() throws IOException {
+ input.close();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/slice/SliceProviderPermissions.java b/services/core/java/com/android/server/slice/SliceProviderPermissions.java
new file mode 100644
index 0000000..6e602d5
--- /dev/null
+++ b/services/core/java/com/android/server/slice/SliceProviderPermissions.java
@@ -0,0 +1,204 @@
+/*
+ * 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.server.slice;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+
+public class SliceProviderPermissions implements DirtyTracker, Persistable {
+
+ private static final String TAG = "SliceProviderPermissions";
+
+ static final String TAG_PROVIDER = "provider";
+ private static final String TAG_AUTHORITY = "authority";
+ private static final String TAG_PKG = "pkg";
+ private static final String NAMESPACE = null;
+
+ private static final String ATTR_PKG = "pkg";
+ private static final String ATTR_AUTHORITY = "authority";
+
+ private final PkgUser mPkg;
+ private final ArrayMap<String, SliceAuthority> mAuths = new ArrayMap<>();
+ private final DirtyTracker mTracker;
+
+ public SliceProviderPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ public synchronized Collection<SliceAuthority> getAuthorities() {
+ return new ArrayList<>(mAuths.values());
+ }
+
+ public synchronized SliceAuthority getOrCreateAuthority(String authority) {
+ SliceAuthority ret = mAuths.get(authority);
+ if (ret == null) {
+ ret = new SliceAuthority(authority, this);
+ mAuths.put(authority, ret);
+ onPersistableDirty(ret);
+ }
+ return ret;
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mTracker.onPersistableDirty(this);
+ }
+
+ @Override
+ public String getFileName() {
+ return getFileName(mPkg);
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ out.startTag(NAMESPACE, TAG_PROVIDER);
+ out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+
+ final int N = mAuths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_AUTHORITY);
+ out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+
+ mAuths.valueAt(i).writeTo(out);
+
+ out.endTag(NAMESPACE, TAG_AUTHORITY);
+ }
+
+ out.endTag(NAMESPACE, TAG_PROVIDER);
+ }
+
+ public static SliceProviderPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+ throws XmlPullParserException, IOException {
+ // Get to the beginning of the provider.
+ while (parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_PROVIDER.equals(parser.getName())) {
+ parser.next();
+ }
+ int depth = parser.getDepth();
+ PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceProviderPermissions provider = new SliceProviderPermissions(pkgUser, tracker);
+ parser.next();
+
+ while (parser.getDepth() > depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_AUTHORITY.equals(parser.getName())) {
+ try {
+ SliceAuthority authority = new SliceAuthority(
+ parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), provider);
+ authority.readFrom(parser);
+ provider.mAuths.put(authority.getAuthority(), authority);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Couldn't read PkgUser", e);
+ }
+ }
+
+ parser.next();
+ }
+ return provider;
+ }
+
+ public static String getFileName(PkgUser pkg) {
+ return String.format("provider_%s", pkg.toString());
+ }
+
+ public static class SliceAuthority implements Persistable {
+ private final String mAuthority;
+ private final DirtyTracker mTracker;
+ private final ArraySet<PkgUser> mPkgs = new ArraySet<>();
+
+ public SliceAuthority(String authority, DirtyTracker tracker) {
+ mAuthority = authority;
+ mTracker = tracker;
+ }
+
+ public String getAuthority() {
+ return mAuthority;
+ }
+
+ public synchronized void addPkg(PkgUser pkg) {
+ if (mPkgs.add(pkg)) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized void removePkg(PkgUser pkg) {
+ if (mPkgs.remove(pkg)) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized Collection<PkgUser> getPkgs() {
+ return new ArraySet<>(mPkgs);
+ }
+
+ @Override
+ public String getFileName() {
+ return null;
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ final int N = mPkgs.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_PKG);
+ out.text(mPkgs.valueAt(i).toString());
+ out.endTag(NAMESPACE, TAG_PKG);
+ }
+ }
+
+ public synchronized void readFrom(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ parser.next();
+ int depth = parser.getDepth();
+ while (parser.getDepth() >= depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_PKG.equals(parser.getName())) {
+ mPkgs.add(new PkgUser(parser.nextText()));
+ }
+ parser.next();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ SliceAuthority other = (SliceAuthority) obj;
+ return Objects.equals(mAuthority, other.mAuthority)
+ && Objects.equals(mPkgs, other.mPkgs);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s: %s)", mAuthority, mPkgs.toString());
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceClientPermissionsTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceClientPermissionsTest.java
new file mode 100644
index 0000000..1efa415
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceClientPermissionsTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.server.slice;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+import android.util.Xml.Encoding;
+
+import com.android.server.UiServiceTestCase;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+import com.android.server.slice.SliceClientPermissions.SliceAuthority;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class SliceClientPermissionsTest extends UiServiceTestCase {
+
+ @Test
+ public void testRemoveBasic() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+ Uri base = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.pkg.slices").build();
+
+ PkgUser testPkg = new PkgUser("other", 2);
+
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .build(), testPkg);
+ client.revokeUri(base.buildUpon()
+ .appendPath("first")
+ .build(), testPkg);
+
+ assertFalse(client.hasPermission(base.buildUpon()
+ .appendPath("first")
+ .appendPath("third")
+ .build(), testPkg.getUserId()));
+
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(client.getAuthorities());
+ assertEquals(0, authorities.get(0).getPaths().size());
+ }
+
+ @Test
+ public void testRemoveSubtrees() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+ Uri base = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.pkg.slices").build();
+
+ PkgUser testPkg = new PkgUser("other", 2);
+
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .appendPath("second")
+ .build(), testPkg);
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .appendPath("third")
+ .build(), testPkg);
+ client.revokeUri(base.buildUpon()
+ .appendPath("first")
+ .build(), testPkg);
+
+ assertFalse(client.hasPermission(base.buildUpon()
+ .appendPath("first")
+ .appendPath("fourth")
+ .build(), testPkg.getUserId()));
+
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(client.getAuthorities());
+ assertEquals(0, authorities.get(0).getPaths().size());
+ }
+
+ @Test
+ public void testAddConsolidate_addFirst() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+ Uri base = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.pkg.slices").build();
+
+ PkgUser testPkg = new PkgUser("other", 2);
+
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .build(), testPkg);
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .appendPath("second")
+ .build(), testPkg);
+
+ assertTrue(client.hasPermission(base.buildUpon()
+ .appendPath("first")
+ .appendPath("third")
+ .build(), testPkg.getUserId()));
+
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(client.getAuthorities());
+ assertEquals(1, authorities.get(0).getPaths().size());
+ }
+
+ @Test
+ public void testAddConsolidate_addSecond() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+ Uri base = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.pkg.slices").build();
+
+ PkgUser testPkg = new PkgUser("other", 2);
+
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .appendPath("second")
+ .build(), testPkg);
+ client.grantUri(base.buildUpon()
+ .appendPath("first")
+ .build(), testPkg);
+
+ assertTrue(client.hasPermission(base.buildUpon()
+ .appendPath("first")
+ .appendPath("third")
+ .build(), testPkg.getUserId()));
+
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(client.getAuthorities());
+ assertEquals(1, authorities.get(0).getPaths().size());
+ }
+
+ @Test
+ public void testDirty_addAuthority() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+
+ client.getOrCreateAuthority(new PkgUser("some_auth", 2), new PkgUser("com.pkg", 2));
+
+ verify(tracker).onPersistableDirty(eq(client));
+ }
+
+ @Test
+ public void testDirty_addPkg() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+
+ SliceAuthority auth = client.getOrCreateAuthority(
+ new PkgUser("some_auth", 2),
+ new PkgUser("com.pkg", 2));
+ clearInvocations(tracker);
+
+ auth.addPath(Arrays.asList("/something/"));
+
+ verify(tracker).onPersistableDirty(eq(client));
+ }
+
+ @Test
+ public void testCreation() {
+ SliceClientPermissions client = createClient();
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(client.getAuthorities());
+ authorities.sort(Comparator.comparing(SliceAuthority::getAuthority));
+
+ assertEquals(2, authorities.size());
+ assertEquals("com.android.pkg", authorities.get(0).getAuthority());
+ assertEquals("com.android.pkg.slices", authorities.get(1).getAuthority());
+
+ assertEquals(1, authorities.get(0).getPaths().size());
+ assertEquals(2, authorities.get(1).getPaths().size());
+ }
+
+ @Test
+ public void testSerialization() throws XmlPullParserException, IOException {
+ SliceClientPermissions client = createClient();
+ client.setHasFullAccess(true);
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
+ serializer.setOutput(output, Encoding.UTF_8.name());
+
+ client.writeTo(serializer);
+ serializer.flush();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, Encoding.UTF_8.name());
+
+ SliceClientPermissions deser = SliceClientPermissions.createFrom(parser,
+ mock(DirtyTracker.class));
+
+ assertEquivalent(client, deser);
+ }
+
+ private void assertEquivalent(SliceClientPermissions o1, SliceClientPermissions o2) {
+ assertEquals(o1.getPkg(), o2.getPkg());
+ ArrayList<SliceAuthority> a1 = new ArrayList<>(o1.getAuthorities());
+ ArrayList<SliceAuthority> a2 = new ArrayList<>(o2.getAuthorities());
+ a1.sort(Comparator.comparing(SliceAuthority::getAuthority));
+ a2.sort(Comparator.comparing(SliceAuthority::getAuthority));
+ assertEquals(a1, a2);
+ }
+
+ private static SliceClientPermissions createClient() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 2);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceClientPermissions client = new SliceClientPermissions(pkg, tracker);
+
+ SliceAuthority auth = client.getOrCreateAuthority(
+ new PkgUser("com.android.pkg.slices", 3),
+ new PkgUser("com.android.pkg", 3));
+ auth.addPath(Arrays.asList("/something/"));
+ auth.addPath(Arrays.asList("/something/else"));
+
+ auth = client.getOrCreateAuthority(
+ new PkgUser("com.android.pkg", 3),
+ new PkgUser("com.pkg", 1));
+ auth.addPath(Arrays.asList("/somewhere"));
+ return client;
+ }
+
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SlicePermissionManagerTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SlicePermissionManagerTest.java
new file mode 100644
index 0000000..5443e73
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SlicePermissionManagerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.server.slice;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.FileUtils;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.util.Log;
+import android.util.Xml.Encoding;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class SlicePermissionManagerTest extends UiServiceTestCase {
+
+ @Test
+ public void testBackup() throws XmlPullParserException, IOException {
+ File sliceDir = new File(mContext.getDataDir(), "system/slices");
+ Uri uri = new Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("authority")
+ .path("something").build();
+ SlicePermissionManager permissions = new SlicePermissionManager(mContext,
+ TestableLooper.get(this).getLooper(), sliceDir);
+
+ permissions.grantFullAccess("com.android.mypkg", 10);
+ permissions.grantSliceAccess("com.android.otherpkg", 0, "com.android.lastpkg", 1, uri);
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
+ serializer.setOutput(output, Encoding.UTF_8.name());
+
+
+ TestableLooper.get(this).processAllMessages();
+ permissions.writeBackup(serializer);
+ serializer.flush();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, Encoding.UTF_8.name());
+
+ permissions = new SlicePermissionManager(mContext,
+ TestableLooper.get(this).getLooper());
+ permissions.readRestore(parser);
+
+ assertTrue(permissions.hasFullAccess("com.android.mypkg", 10));
+ assertTrue(permissions.hasPermission("com.android.otherpkg", 0,
+ ContentProvider.maybeAddUserId(uri, 1)));
+ permissions.removePkg("com.android.lastpkg", 1);
+ assertFalse(permissions.hasPermission("com.android.otherpkg", 0,
+ ContentProvider.maybeAddUserId(uri, 1)));
+
+ // Cleanup.
+ assertTrue(FileUtils.deleteContentsAndDir(sliceDir));
+ }
+
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceProviderPermissionsTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceProviderPermissionsTest.java
new file mode 100644
index 0000000..5775991
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceProviderPermissionsTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.server.slice;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+import android.util.Xml.Encoding;
+
+import com.android.server.UiServiceTestCase;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class SliceProviderPermissionsTest extends UiServiceTestCase {
+
+ @Test
+ public void testDirty_addAuthority() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceProviderPermissions provider = new SliceProviderPermissions(pkg, tracker);
+
+ provider.getOrCreateAuthority("some_auth");
+
+ verify(tracker).onPersistableDirty(eq(provider));
+ }
+
+ @Test
+ public void testDirty_addPkg() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 0);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceProviderPermissions provider = new SliceProviderPermissions(pkg, tracker);
+
+ SliceAuthority auth = provider.getOrCreateAuthority("some_auth");
+ clearInvocations(tracker);
+
+ auth.addPkg(new PkgUser("pkg", 0));
+
+ verify(tracker).onPersistableDirty(eq(provider));
+ }
+
+ @Test
+ public void testCreation() {
+ SliceProviderPermissions provider = createProvider();
+ ArrayList<SliceAuthority> authorities = new ArrayList<>(provider.getAuthorities());
+ authorities.sort(Comparator.comparing(SliceAuthority::getAuthority));
+
+ assertEquals(2, authorities.size());
+ assertEquals("com.android.pkg", authorities.get(0).getAuthority());
+ assertEquals("com.android.pkg.slices", authorities.get(1).getAuthority());
+
+ assertEquals(1, authorities.get(0).getPkgs().size());
+ assertEquals(2, authorities.get(1).getPkgs().size());
+ }
+
+ @Test
+ public void testSerialization() throws XmlPullParserException, IOException {
+ SliceProviderPermissions provider = createProvider();
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
+ serializer.setOutput(output, Encoding.UTF_8.name());
+
+ provider.writeTo(serializer);
+ serializer.flush();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, Encoding.UTF_8.name());
+
+ SliceProviderPermissions deser = SliceProviderPermissions.createFrom(parser,
+ mock(DirtyTracker.class));
+
+ assertEquivalent(provider, deser);
+ }
+
+ private void assertEquivalent(SliceProviderPermissions o1, SliceProviderPermissions o2) {
+ assertEquals(o1.getPkg(), o2.getPkg());
+ assertEquals(o1.getAuthorities(), o2.getAuthorities());
+ }
+
+ private static SliceProviderPermissions createProvider() {
+ PkgUser pkg = new PkgUser("com.android.pkg", 2);
+ DirtyTracker tracker = mock(DirtyTracker.class);
+ SliceProviderPermissions provider = new SliceProviderPermissions(pkg, tracker);
+
+ SliceAuthority auth = provider.getOrCreateAuthority("com.android.pkg.slices");
+ auth.addPkg(new PkgUser("com.example.pkg", 0));
+ auth.addPkg(new PkgUser("example.pkg.com", 10));
+
+ auth = provider.getOrCreateAuthority("com.android.pkg");
+ auth.addPkg(new PkgUser("com.example.pkg", 2));
+ return provider;
+ }
+
+}
\ No newline at end of file