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