Merge "Give SliceManagerService a concept of pinned slices."
diff --git a/Android.bp b/Android.bp
index 948b0a0..6a59abc 100644
--- a/Android.bp
+++ b/Android.bp
@@ -99,6 +99,7 @@
         "core/java/android/app/backup/IRestoreSession.aidl",
         "core/java/android/app/backup/ISelectBackupTransportCallback.aidl",
         "core/java/android/app/slice/ISliceManager.aidl",
+        "core/java/android/app/slice/ISliceListener.aidl",
         "core/java/android/app/timezone/ICallback.aidl",
         "core/java/android/app/timezone/IRulesManager.aidl",
         "core/java/android/app/usage/ICacheQuotaService.aidl",
diff --git a/Android.mk b/Android.mk
index ec5fb84..800b992 100644
--- a/Android.mk
+++ b/Android.mk
@@ -97,6 +97,7 @@
 	frameworks/base/core/java/android/app/admin/SystemUpdatePolicy.aidl \
 	frameworks/base/core/java/android/app/admin/PasswordMetrics.aidl \
 	frameworks/base/core/java/android/app/slice/ISliceManager.aidl \
+	frameworks/base/core/java/android/app/slice/ISliceListener.aidl \
 	frameworks/base/core/java/android/print/PrintDocumentInfo.aidl \
 	frameworks/base/core/java/android/print/PageRange.aidl \
 	frameworks/base/core/java/android/print/PrintAttributes.aidl \
diff --git a/core/java/android/app/slice/ISliceListener.aidl b/core/java/android/app/slice/ISliceListener.aidl
new file mode 100644
index 0000000..d293fd4
--- /dev/null
+++ b/core/java/android/app/slice/ISliceListener.aidl
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2017, 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 android.app.slice;
+
+import android.app.slice.ISliceManager;
+import android.app.slice.Slice;
+
+/** @hide */
+oneway interface ISliceListener {
+    void onSliceUpdated(in Slice s);
+}
diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl
index 6e52f38..5f0e542 100644
--- a/core/java/android/app/slice/ISliceManager.aidl
+++ b/core/java/android/app/slice/ISliceManager.aidl
@@ -16,6 +16,17 @@
 
 package android.app.slice;
 
+import android.app.slice.ISliceListener;
+import android.app.slice.SliceSpec;
+import android.net.Uri;
+
 /** @hide */
 interface ISliceManager {
+    void addSliceListener(in Uri uri, String pkg, in ISliceListener listener,
+            in SliceSpec[] specs);
+    void removeSliceListener(in Uri uri, String pkg, in ISliceListener listener);
+    void pinSlice(String pkg, in Uri uri, in SliceSpec[] specs);
+    void unpinSlice(String pkg, in Uri uri);
+    boolean hasSliceAccess(String pkg);
+    SliceSpec[] getPinnedSpecs(in Uri uri, String pkg);
 }
diff --git a/core/java/android/app/slice/Slice.aidl b/core/java/android/app/slice/Slice.aidl
new file mode 100644
index 0000000..e097f9d
--- /dev/null
+++ b/core/java/android/app/slice/Slice.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2017, 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 android.app.slice;
+
+parcelable Slice;
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index e99f676..f8e19c1 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -17,8 +17,11 @@
 package android.app.slice;
 
 import android.annotation.SystemService;
+import android.app.slice.ISliceListener.Stub;
 import android.content.Context;
+import android.net.Uri;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
 
@@ -36,4 +39,93 @@
         mService = ISliceManager.Stub.asInterface(
                 ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
     }
+
+    /**
+     */
+    public void addSliceListener(Uri uri, SliceListener listener, SliceSpec[] specs) {
+        try {
+            mService.addSliceListener(uri, mContext.getPackageName(), listener.mStub, specs);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public void removeSliceListener(Uri uri, SliceListener listener) {
+        try {
+            mService.removeSliceListener(uri, mContext.getPackageName(), listener.mStub);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public void pinSlice(Uri uri, SliceSpec[] specs) {
+        try {
+            mService.pinSlice(mContext.getPackageName(), uri, specs);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public void unpinSlice(Uri uri) {
+        try {
+            mService.unpinSlice(mContext.getPackageName(), uri);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public boolean hasSliceAccess() {
+        try {
+            return mService.hasSliceAccess(mContext.getPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public SliceSpec[] getPinnedSpecs(Uri uri) {
+        try {
+            return mService.getPinnedSpecs(uri, mContext.getPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     */
+    public abstract static class SliceListener {
+        private final Handler mHandler;
+
+        /**
+         */
+        public SliceListener() {
+            this(Handler.getMain());
+        }
+
+        /**
+         */
+        public SliceListener(Handler h) {
+            mHandler = h;
+        }
+
+        /**
+         */
+        public abstract void onSliceUpdated(Slice s);
+
+        private final ISliceListener.Stub mStub = new Stub() {
+            @Override
+            public void onSliceUpdated(Slice s) throws RemoteException {
+                mHandler.post(() -> SliceListener.this.onSliceUpdated(s));
+            }
+        };
+    }
 }
diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java
index ac5365c..7dcd2fe 100644
--- a/core/java/android/app/slice/SliceProvider.java
+++ b/core/java/android/app/slice/SliceProvider.java
@@ -105,6 +105,14 @@
     /**
      * @hide
      */
+    public static final String METHOD_PIN = "pin";
+    /**
+     * @hide
+     */
+    public static final String METHOD_UNPIN = "unpin";
+    /**
+     * @hide
+     */
     public static final String EXTRA_INTENT = "slice_intent";
     /**
      * @hide
@@ -143,6 +151,18 @@
     }
 
     /**
+     * @hide
+     */
+    public void onSlicePinned(Uri sliceUri) {
+    }
+
+    /**
+     * @hide
+     */
+    public void onSliceUnpinned(Uri sliceUri) {
+    }
+
+    /**
      * This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider.
      * In that case, this method can be called and is expected to return a non-null Uri representing
      * a slice. Otherwise this will throw {@link UnsupportedOperationException}.
@@ -221,6 +241,7 @@
             getContext().enforceCallingPermission(permission.BIND_SLICE,
                     "Slice binding requires the permission BIND_SLICE");
             Intent intent = extras.getParcelable(EXTRA_INTENT);
+            if (intent == null) return null;
             Uri uri = onMapIntentToUri(intent);
             List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
             Bundle b = new Bundle();
@@ -231,10 +252,62 @@
                 b.putParcelable(EXTRA_SLICE, null);
             }
             return b;
+        } else if (method.equals(METHOD_PIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+                getContext().enforceUriPermission(uri, permission.BIND_SLICE,
+                        permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                        "Slice binding requires the permission BIND_SLICE");
+            }
+            handlePinSlice(uri);
+        } else if (method.equals(METHOD_UNPIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+                getContext().enforceUriPermission(uri, permission.BIND_SLICE,
+                        permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                        "Slice binding requires the permission BIND_SLICE");
+            }
+            handleUnpinSlice(uri);
         }
         return super.call(method, arg, extras);
     }
 
+    private void handlePinSlice(Uri sliceUri) {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            onSlicePinned(sliceUri);
+        } else {
+            CountDownLatch latch = new CountDownLatch(1);
+            Handler.getMain().post(() -> {
+                onSlicePinned(sliceUri);
+                latch.countDown();
+            });
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private void handleUnpinSlice(Uri sliceUri) {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            onSliceUnpinned(sliceUri);
+        } else {
+            CountDownLatch latch = new CountDownLatch(1);
+            Handler.getMain().post(() -> {
+                onSliceUnpinned(sliceUri);
+                latch.countDown();
+            });
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
         if (Looper.myLooper() == Looper.getMainLooper()) {
             return onBindSliceStrict(sliceUri, supportedSpecs);
diff --git a/core/java/android/app/slice/SliceSpec.aidl b/core/java/android/app/slice/SliceSpec.aidl
new file mode 100644
index 0000000..92e98b7
--- /dev/null
+++ b/core/java/android/app/slice/SliceSpec.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2017, 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 android.app.slice;
+
+parcelable SliceSpec;
diff --git a/core/java/android/app/slice/SliceSpec.java b/core/java/android/app/slice/SliceSpec.java
index 433b67e..8cc0384 100644
--- a/core/java/android/app/slice/SliceSpec.java
+++ b/core/java/android/app/slice/SliceSpec.java
@@ -103,6 +103,11 @@
         return mType.equals(other.mType) && mRevision == other.mRevision;
     }
 
+    @Override
+    public String toString() {
+        return String.format("SliceSpec{%s,%d}", mType, mRevision);
+    }
+
     public static final Creator<SliceSpec> CREATOR = new Creator<SliceSpec>() {
         @Override
         public SliceSpec createFromParcel(Parcel source) {
diff --git a/services/core/java/com/android/server/slice/PinnedSliceState.java b/services/core/java/com/android/server/slice/PinnedSliceState.java
new file mode 100644
index 0000000..cf930f5
--- /dev/null
+++ b/services/core/java/com/android/server/slice/PinnedSliceState.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2017 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.app.slice.ISliceListener;
+import android.app.slice.Slice;
+import android.app.slice.SliceProvider;
+import android.app.slice.SliceSpec;
+import android.content.ContentProviderClient;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages the state of a pinned slice.
+ */
+public class PinnedSliceState {
+
+    private static final long SLICE_TIMEOUT = 5000;
+    private static final String TAG = "PinnedSliceState";
+
+    private final Object mLock;
+
+    private final SliceManagerService mService;
+    private final Uri mUri;
+    @GuardedBy("mLock")
+    private final ArraySet<String> mPinnedPkgs = new ArraySet<>();
+    @GuardedBy("mLock")
+    private final ArraySet<ISliceListener> mListeners = new ArraySet<>();
+    @GuardedBy("mLock")
+    private SliceSpec[] mSupportedSpecs = null;
+
+    public PinnedSliceState(SliceManagerService service, Uri uri) {
+        mService = service;
+        mUri = uri;
+        mService.getHandler().post(this::handleSendPinned);
+        mLock = mService.getLock();
+    }
+
+    public SliceSpec[] getSpecs() {
+        return mSupportedSpecs;
+    }
+
+    public void mergeSpecs(SliceSpec[] supportedSpecs) {
+        synchronized (mLock) {
+            if (mSupportedSpecs == null) {
+                mSupportedSpecs = supportedSpecs;
+            } else {
+                List<SliceSpec> specs = Arrays.asList(mSupportedSpecs);
+                mSupportedSpecs = specs.stream().map(s -> {
+                    SliceSpec other = findSpec(supportedSpecs, s.getType());
+                    if (other == null) return null;
+                    if (other.getRevision() < s.getRevision()) {
+                        return other;
+                    }
+                    return s;
+                }).filter(s -> s != null).toArray(SliceSpec[]::new);
+            }
+        }
+    }
+
+    private SliceSpec findSpec(SliceSpec[] specs, String type) {
+        for (SliceSpec spec : specs) {
+            if (Objects.equals(spec.getType(), type)) {
+                return spec;
+            }
+        }
+        return null;
+    }
+
+    public Uri getUri() {
+        return mUri;
+    }
+
+    public void destroy() {
+        mService.getHandler().post(this::handleSendUnpinned);
+    }
+
+    public void onChange() {
+        mService.getHandler().post(this::handleBind);
+    }
+
+    public void addSliceListener(ISliceListener listener, SliceSpec[] specs) {
+        synchronized (mLock) {
+            if (mListeners.add(listener) && mListeners.size() == 1) {
+                mService.listen(mUri);
+            }
+            mergeSpecs(specs);
+        }
+    }
+
+    public boolean removeSliceListener(ISliceListener listener) {
+        synchronized (mLock) {
+            if (mListeners.remove(listener) && mListeners.size() == 0) {
+                mService.unlisten(mUri);
+            }
+        }
+        return !isPinned();
+    }
+
+    public void pin(String pkg, SliceSpec[] specs) {
+        synchronized (mLock) {
+            mPinnedPkgs.add(pkg);
+            mergeSpecs(specs);
+        }
+    }
+
+    public boolean unpin(String pkg) {
+        synchronized (mLock) {
+            mPinnedPkgs.remove(pkg);
+        }
+        return !isPinned();
+    }
+
+    public boolean isListening() {
+        synchronized (mLock) {
+            return !mListeners.isEmpty();
+        }
+    }
+
+    @VisibleForTesting
+    public boolean isPinned() {
+        synchronized (mLock) {
+            return !mPinnedPkgs.isEmpty() || !mListeners.isEmpty();
+        }
+    }
+
+    ContentProviderClient getClient() {
+        ContentProviderClient client =
+                mService.getContext().getContentResolver().acquireContentProviderClient(mUri);
+        client.setDetectNotResponding(SLICE_TIMEOUT);
+        return client;
+    }
+
+    private void handleBind() {
+        Slice s;
+        try (ContentProviderClient client = getClient()) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+                    new ArrayList<>(Arrays.asList(mSupportedSpecs)));
+            final Bundle res;
+            try {
+                res = client.call(SliceProvider.METHOD_SLICE, null, extras);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Unable to bind slice " + mUri, e);
+                return;
+            }
+            if (res == null) return;
+            Bundle.setDefusable(res, true);
+            s = res.getParcelable(SliceProvider.EXTRA_SLICE);
+        }
+        synchronized (mLock) {
+            mListeners.removeIf(l -> {
+                try {
+                    l.onSliceUpdated(s);
+                    return false;
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Unable to notify slice " + mUri, e);
+                    return true;
+                }
+            });
+            if (!isPinned()) {
+                // All the listeners died, remove from pinned state.
+                mService.removePinnedSlice(mUri);
+            }
+        }
+    }
+
+    private void handleSendPinned() {
+        try (ContentProviderClient client = getClient()) {
+            Bundle b = new Bundle();
+            b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+            try {
+                client.call(SliceProvider.METHOD_PIN, null, b);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Unable to contact " + mUri, e);
+            }
+        }
+    }
+
+    private void handleSendUnpinned() {
+        try (ContentProviderClient client = getClient()) {
+            Bundle b = new Bundle();
+            b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+            try {
+                client.call(SliceProvider.METHOD_UNPIN, null, b);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Unable to contact " + mUri, e);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/slice/SliceManagerService.java b/services/core/java/com/android/server/slice/SliceManagerService.java
index 047e270..2d9e772 100644
--- a/services/core/java/com/android/server/slice/SliceManagerService.java
+++ b/services/core/java/com/android/server/slice/SliceManagerService.java
@@ -16,21 +16,307 @@
 
 package com.android.server.slice;
 
-import android.app.slice.ISliceManager;
-import android.content.Context;
+import static android.content.ContentProvider.getUserIdFromUri;
+import static android.content.ContentProvider.maybeAddUserId;
 
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.app.slice.ISliceListener;
+import android.app.slice.ISliceManager;
+import android.app.slice.SliceSpec;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
 import com.android.server.SystemService;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
 public class SliceManagerService extends ISliceManager.Stub {
 
-    public SliceManagerService(Context context) {
+    private static final String TAG = "SliceManagerService";
+    private final Object mLock = new Object();
 
+    private final Context mContext;
+    private final PackageManagerInternal mPackageManagerInternal;
+    private final AppOpsManager mAppOps;
+    private final AssistUtils mAssistUtils;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
+    private final Handler mHandler;
+    private final ContentObserver mObserver;
+
+    public SliceManagerService(Context context) {
+        this(context, createHandler().getLooper());
     }
 
+    @VisibleForTesting
+    SliceManagerService(Context context, Looper looper) {
+        mContext = context;
+        mPackageManagerInternal = Preconditions.checkNotNull(
+                LocalServices.getService(PackageManagerInternal.class));
+        mAppOps = context.getSystemService(AppOpsManager.class);
+        mAssistUtils = new AssistUtils(context);
+        mHandler = new Handler(looper);
+
+        mObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                try {
+                    getPinnedSlice(uri).onChange();
+                } catch (IllegalStateException e) {
+                    Log.e(TAG, "Received change for unpinned slice " + uri, e);
+                }
+            }
+        };
+    }
+
+    ///  ----- Lifecycle stuff -----
     private void systemReady() {
     }
 
-    private void onUnlockUser(int userHandle) {
+    private void onUnlockUser(int userId) {
+    }
+
+    private void onStopUser(int userId) {
+        synchronized (mLock) {
+            mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId);
+        }
+    }
+
+    ///  ----- ISliceManager stuff -----
+    @Override
+    public void addSliceListener(Uri uri, String pkg, ISliceListener listener, SliceSpec[] specs)
+            throws RemoteException {
+        verifyCaller(pkg);
+        uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+        enforceAccess(pkg, uri);
+        getOrCreatePinnedSlice(uri).addSliceListener(listener, specs);
+    }
+
+    @Override
+    public void removeSliceListener(Uri uri, String pkg, ISliceListener listener)
+            throws RemoteException {
+        verifyCaller(pkg);
+        uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+        enforceAccess(pkg, uri);
+        if (getPinnedSlice(uri).removeSliceListener(listener)) {
+            removePinnedSlice(uri);
+        }
+    }
+
+    @Override
+    public void pinSlice(String pkg, Uri uri, SliceSpec[] specs) throws RemoteException {
+        verifyCaller(pkg);
+        enforceFullAccess(pkg, "pinSlice", uri);
+        uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+        getOrCreatePinnedSlice(uri).pin(pkg, specs);
+    }
+
+    @Override
+    public void unpinSlice(String pkg, Uri uri) throws RemoteException {
+        verifyCaller(pkg);
+        enforceFullAccess(pkg, "unpinSlice", uri);
+        uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+        if (getPinnedSlice(uri).unpin(pkg)) {
+            removePinnedSlice(uri);
+        }
+    }
+
+    @Override
+    public boolean hasSliceAccess(String pkg) throws RemoteException {
+        verifyCaller(pkg);
+        return hasFullSliceAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
+    }
+
+    @Override
+    public SliceSpec[] getPinnedSpecs(Uri uri, String pkg) throws RemoteException {
+        verifyCaller(pkg);
+        enforceAccess(pkg, uri);
+        return getPinnedSlice(uri).getSpecs();
+    }
+
+    ///  ----- internal code -----
+    void removePinnedSlice(Uri uri) {
+        synchronized (mLock) {
+            mPinnedSlicesByUri.remove(uri).destroy();
+        }
+    }
+
+    private PinnedSliceState getPinnedSlice(Uri uri) {
+        synchronized (mLock) {
+            PinnedSliceState manager = mPinnedSlicesByUri.get(uri);
+            if (manager == null) {
+                throw new IllegalStateException(String.format("Slice %s not pinned",
+                        uri.toString()));
+            }
+            return manager;
+        }
+    }
+
+    private PinnedSliceState getOrCreatePinnedSlice(Uri uri) {
+        synchronized (mLock) {
+            PinnedSliceState manager = mPinnedSlicesByUri.get(uri);
+            if (manager == null) {
+                manager = createPinnedSlice(uri);
+                mPinnedSlicesByUri.put(uri, manager);
+            }
+            return manager;
+        }
+    }
+
+    @VisibleForTesting
+    PinnedSliceState createPinnedSlice(Uri uri) {
+        return new PinnedSliceState(this, uri);
+    }
+
+    public Object getLock() {
+        return mLock;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    private void enforceAccess(String pkg, Uri uri) {
+        getContext().enforceUriPermission(uri, permission.BIND_SLICE,
+                permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                "Slice binding requires the permission BIND_SLICE");
+        int user = Binder.getCallingUserHandle().getIdentifier();
+        if (getUserIdFromUri(uri, user) != user) {
+            getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL,
+                    "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL");
+        }
+    }
+
+    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));
+        }
+        if (getUserIdFromUri(uri, user) != user) {
+            getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL,
+                    "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL");
+        }
+    }
+
+    private void verifyCaller(String pkg) {
+        mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+    }
+
+    private boolean hasFullSliceAccess(String pkg, int userId) {
+        return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId)
+                || isGrantedFullAccess(pkg, userId);
+    }
+
+    private boolean isAssistant(String pkg, int userId) {
+        final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId);
+        if (cn == null) {
+            return false;
+        }
+        return cn.getPackageName().equals(pkg);
+    }
+
+    public void listen(Uri uri) {
+        mContext.getContentResolver().registerContentObserver(uri, true, mObserver);
+    }
+
+    public void unlisten(Uri uri) {
+        mContext.getContentResolver().unregisterContentObserver(mObserver);
+        synchronized (mLock) {
+            mPinnedSlicesByUri.forEach((u, s) -> {
+                if (s.isListening()) {
+                    listen(u);
+                }
+            });
+        }
+    }
+
+    private boolean isDefaultHomeApp(String pkg, int userId) {
+        String defaultHome = getDefaultHome(userId);
+        return Objects.equals(pkg, defaultHome);
+    }
+
+    // Based on getDefaultHome in ShortcutService.
+    // TODO: Unify if possible
+    @VisibleForTesting
+    String getDefaultHome(int userId) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
+
+            // Default launcher from package manager.
+            final ComponentName defaultLauncher = mPackageManagerInternal
+                    .getHomeActivitiesAsUser(allHomeCandidates, userId);
+
+            ComponentName detected = null;
+            if (defaultLauncher != null) {
+                detected = defaultLauncher;
+            }
+
+            if (detected == null) {
+                // If we reach here, that means it's the first check since the user was created,
+                // and there's already multiple launchers and there's no default set.
+                // Find the system one with the highest priority.
+                // (We need to check the priority too because of FallbackHome in Settings.)
+                // If there's no system launcher yet, then no one can access slices, until
+                // the user explicitly sets one.
+                final int size = allHomeCandidates.size();
+
+                int lastPriority = Integer.MIN_VALUE;
+                for (int i = 0; i < size; i++) {
+                    final ResolveInfo ri = allHomeCandidates.get(i);
+                    if (!ri.activityInfo.applicationInfo.isSystemApp()) {
+                        continue;
+                    }
+                    if (ri.priority < lastPriority) {
+                        continue;
+                    }
+                    detected = ri.activityInfo.getComponentName();
+                    lastPriority = ri.priority;
+                }
+            }
+            return detected.getPackageName();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private boolean isGrantedFullAccess(String pkg, int userId) {
+        // TODO: This will be user granted access, if we allow this through a prompt.
+        return false;
+    }
+
+    private static ServiceThread createHandler() {
+        ServiceThread handlerThread = new ServiceThread(TAG,
+                Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
+        handlerThread.start();
+        return handlerThread;
     }
 
     public static class Lifecycle extends SystemService {
@@ -57,5 +343,10 @@
         public void onUnlockUser(int userHandle) {
             mService.onUnlockUser(userHandle);
         }
+
+        @Override
+        public void onStopUser(int userHandle) {
+            mService.onStopUser(userHandle);
+        }
     }
 }
diff --git a/services/tests/uiservicestests/Android.mk b/services/tests/uiservicestests/Android.mk
index 40e7878..d8e14ec 100644
--- a/services/tests/uiservicestests/Android.mk
+++ b/services/tests/uiservicestests/Android.mk
@@ -10,7 +10,8 @@
 
 # Include test java files and source from notifications package.
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
-	$(call all-java-files-under, ../../core/java/com/android/server/notification)
+	$(call all-java-files-under, ../../core/java/com/android/server/notification) \
+	$(call all-java-files-under, ../../core/java/com/android/server/slice) \
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     frameworks-base-testutils \
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index 621b457..f022dcf 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -28,6 +28,9 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
+
+        <provider android:name=".DummyProvider"
+            android:authorities="com.android.services.uitests" />
     </application>
 
     <instrumentation
diff --git a/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java b/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java
new file mode 100644
index 0000000..574c226
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 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.frameworks.tests.uiservices;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class DummyProvider extends ContentProvider {
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
new file mode 100644
index 0000000..f534b5c
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+
+
+public class UiServiceTestCase {
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(InstrumentationRegistry.getContext(), null);
+
+    protected TestableContext getContext() {
+        return mContext;
+    }
+
+    @Before
+    public void setup() {
+        // Share classloader to allow package access.
+        System.setProperty("dexmaker.share_classloader", "true");
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java b/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java
index faf6a9b..d4c41e0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java
@@ -22,13 +22,16 @@
 
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class AlertRateLimiterTest extends NotificationTestCase {
+public class AlertRateLimiterTest extends UiServiceTestCase {
 
     private long mTestStartTime;
     private
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
index 262516d..142041a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
@@ -27,13 +27,13 @@
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.NotificationChannel;
-import android.app.NotificationManager;
 import android.os.UserHandle;
-import android.provider.Settings.Secure;
 import android.service.notification.StatusBarNotification;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,7 +42,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class BadgeExtractorTest extends NotificationTestCase {
+public class BadgeExtractorTest extends UiServiceTestCase {
 
     @Mock RankingConfig mConfig;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 0b4d61f..a92f7e7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -58,13 +58,13 @@
 import android.service.notification.StatusBarNotification;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Slog;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IAccessibilityManager;
 import android.view.accessibility.IAccessibilityManagerClient;
 
 import com.android.internal.util.IntPair;
+import com.android.server.UiServiceTestCase;
 import com.android.server.lights.Light;
 
 import org.junit.Before;
@@ -74,12 +74,10 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class BuzzBeepBlinkTest extends NotificationTestCase {
+public class BuzzBeepBlinkTest extends UiServiceTestCase {
 
     @Mock AudioManager mAudioManager;
     @Mock Vibrator mVibrator;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java
index f92bd84..97f2104 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java
@@ -28,6 +28,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -37,7 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class GlobalSortKeyComparatorTest extends NotificationTestCase {
+public class GlobalSortKeyComparatorTest extends UiServiceTestCase {
 
     private final String PKG = "PKG";
     private final int UID = 1111111;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index f75c648..8d4c5b1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -38,6 +38,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -45,7 +47,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class GroupHelperTest extends NotificationTestCase {
+public class GroupHelperTest extends UiServiceTestCase {
     private @Mock GroupHelper.Callback mCallback;
 
     private GroupHelper mGroupHelper;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java
index d325e10..73d5961 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java
@@ -30,7 +30,6 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import static org.mockito.Matchers.anyInt;
@@ -39,9 +38,11 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.server.UiServiceTestCase;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class ImportanceExtractorTest extends NotificationTestCase {
+public class ImportanceExtractorTest extends UiServiceTestCase {
 
     @Mock RankingConfig mConfig;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index a4b9b25..9ef0ec7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -49,6 +49,7 @@
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.server.UiServiceTestCase;
 
 import com.google.android.collect.Lists;
 
@@ -68,7 +69,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class ManagedServicesTest extends NotificationTestCase {
+public class ManagedServicesTest extends UiServiceTestCase {
 
     @Mock
     private IPackageManager mIpm;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
index e527644..fd674f0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
@@ -31,12 +31,14 @@
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Test;
 
 import java.util.ArrayList;
 import java.util.Objects;
 
-public class NotificationAdjustmentExtractorTest extends NotificationTestCase {
+public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
 
     @Test
     public void testExtractsAdjustment() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
index d75213c..eb45960 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.notification;
 
-import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 
@@ -31,17 +30,17 @@
 
 import android.app.Notification;
 import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.content.Intent;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-public class NotificationChannelExtractorTest extends NotificationTestCase {
+public class NotificationChannelExtractorTest extends UiServiceTestCase {
 
     @Mock RankingConfig mConfig;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java
index f457f6a..2241047 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java
@@ -26,6 +26,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.server.UiServiceTestCase;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -36,7 +37,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class NotificationChannelTest extends NotificationTestCase {
+public class NotificationChannelTest extends UiServiceTestCase {
 
     @Test
     public void testWriteToParcel() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
index 1e5f96f..3dcd5b9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
@@ -38,6 +38,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -50,7 +52,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class NotificationComparatorTest extends NotificationTestCase {
+public class NotificationComparatorTest extends UiServiceTestCase {
     @Mock Context mContext;
     @Mock TelecomManager mTm;
     @Mock RankingHandler handler;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java
index 85852f9..00d93de 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java
@@ -32,9 +32,11 @@
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Test;
 
-public class NotificationIntrusivenessExtractorTest extends NotificationTestCase {
+public class NotificationIntrusivenessExtractorTest extends UiServiceTestCase {
 
     @Test
     public void testNonIntrusive() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index d767ba2..f4313b8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -37,6 +37,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -45,7 +47,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class NotificationListenerServiceTest extends NotificationTestCase {
+public class NotificationListenerServiceTest extends UiServiceTestCase {
 
     private String[] mKeys = new String[] { "key", "key1", "key2", "key3"};
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 0343ab2..ad3fecf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -83,6 +83,7 @@
 import android.util.AtomicFile;
 
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.server.UiServiceTestCase;
 import com.android.server.lights.Light;
 import com.android.server.lights.LightsManager;
 import com.android.server.notification.NotificationManagerService.NotificationAssistants;
@@ -110,7 +111,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
-public class NotificationManagerServiceTest extends NotificationTestCase {
+public class NotificationManagerServiceTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
     private final int mUid = Binder.getCallingUid();
     private NotificationManagerService mService;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index ef26705a..a5fa903 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -53,6 +53,7 @@
 
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.server.UiServiceTestCase;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -65,7 +66,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class NotificationRecordTest extends NotificationTestCase {
+public class NotificationRecordTest extends UiServiceTestCase {
 
     private final Context mMockContext = Mockito.mock(Context.class);
     @Mock PackageManager mPm;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java
index fec2811..4f153ee 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java
@@ -11,12 +11,14 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class NotificationStatsTest extends NotificationTestCase {
+public class NotificationStatsTest extends UiServiceTestCase {
 
     @Test
     public void testConstructor() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
index 4165e9e..4bfb236 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
@@ -33,6 +33,8 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,7 +43,7 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-public class NotificationTest extends NotificationTestCase {
+public class NotificationTest extends UiServiceTestCase {
 
     @Mock
     ActivityManager mAm;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java
deleted file mode 100644
index 1ee3412..0000000
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2017 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.notification;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.testing.TestableContext;
-
-import org.junit.Rule;
-
-
-public class NotificationTestCase {
-    @Rule
-    public final TestableContext mContext =
-            new TestableContext(InstrumentationRegistry.getContext(), null);
-
-    protected TestableContext getContext() {
-        return mContext;
-    }
-}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index 2d03f11..abfc54d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -65,6 +65,7 @@
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.server.UiServiceTestCase;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
@@ -90,7 +91,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class RankingHelperTest extends NotificationTestCase {
+public class RankingHelperTest extends UiServiceTestCase {
     private static final String PKG = "com.android.server.notification";
     private static final int UID = 0;
     private static final UserHandle USER = UserHandle.of(0);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
index e354267..5d8d48f1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
@@ -24,9 +24,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 
+import com.android.server.UiServiceTestCase;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class RateEstimatorTest extends NotificationTestCase {
+public class RateEstimatorTest extends UiServiceTestCase {
     private long mTestStartTime;
     private RateEstimator mEstimator;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java
index 5ebfd48..9564ab9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java
@@ -27,6 +27,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -36,7 +38,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class ScheduleCalendarTest extends NotificationTestCase {
+public class ScheduleCalendarTest extends UiServiceTestCase {
 
     private ScheduleCalendar mScheduleCalendar;
     private ZenModeConfig.ScheduleInfo mScheduleInfo;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
index 610592f..17fed83 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
@@ -13,6 +13,8 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -23,7 +25,7 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-public class ScheduleConditionProviderTest extends NotificationTestCase {
+public class ScheduleConditionProviderTest extends UiServiceTestCase {
 
     ScheduleConditionProvider mService;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 07b21fb..88c6fcf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -32,7 +32,6 @@
 import android.service.notification.StatusBarNotification;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Slog;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -46,10 +45,12 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.android.server.UiServiceTestCase;
+
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SnoozeHelperTest extends NotificationTestCase {
+public class SnoozeHelperTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "test_channel_id";
 
     @Mock SnoozeHelper.Callback mCallback;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
index 4ac0c65..58f0ded 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
@@ -30,9 +30,11 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertEquals;
 
+import com.android.server.UiServiceTestCase;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class ValidateNotificationPeopleTest extends NotificationTestCase {
+public class ValidateNotificationPeopleTest extends UiServiceTestCase {
 
     @Test
     public void testNoExtra() throws Exception {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 8ac6481..0c7397a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -32,6 +32,8 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import com.android.server.UiServiceTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,7 +43,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class ZenModeHelperTest extends NotificationTestCase {
+public class ZenModeHelperTest extends UiServiceTestCase {
 
     @Mock ConditionProviders mConditionProviders;
     private TestableLooper mTestableLooper;
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java
new file mode 100644
index 0000000..ce328c2
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java
@@ -0,0 +1,214 @@
+package com.android.server.slice;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+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 static org.mockito.Mockito.when;
+
+import android.app.slice.ISliceListener;
+import android.app.slice.Slice;
+import android.app.slice.SliceProvider;
+import android.app.slice.SliceSpec;
+import android.content.ContentProvider;
+import android.content.IContentProvider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class PinnedSliceStateTest extends UiServiceTestCase {
+
+    private static final String AUTH = "my.authority";
+    private static final Uri TEST_URI = Uri.parse("content://" + AUTH + "/path");
+
+    private static final SliceSpec[] FIRST_SPECS = new SliceSpec[]{
+            new SliceSpec("spec1", 3),
+            new SliceSpec("spec2", 3),
+            new SliceSpec("spec3", 2),
+            new SliceSpec("spec4", 1),
+    };
+
+    private static final SliceSpec[] SECOND_SPECS = new SliceSpec[]{
+            new SliceSpec("spec2", 1),
+            new SliceSpec("spec3", 2),
+            new SliceSpec("spec4", 3),
+            new SliceSpec("spec5", 4),
+    };
+
+    private SliceManagerService mSliceService;
+    private PinnedSliceState mPinnedSliceManager;
+    private IContentProvider mIContentProvider;
+    private ContentProvider mContentProvider;
+
+    @Before
+    public void setup() {
+        mSliceService = mock(SliceManagerService.class);
+        when(mSliceService.getLock()).thenReturn(new Object());
+        when(mSliceService.getContext()).thenReturn(mContext);
+        when(mSliceService.getHandler()).thenReturn(new Handler(TestableLooper.get(this).getLooper()));
+        mContentProvider = mock(ContentProvider.class);
+        mIContentProvider = mock(IContentProvider.class);
+        when(mContentProvider.getIContentProvider()).thenReturn(mIContentProvider);
+        mContext.getContentResolver().addProvider(AUTH, mContentProvider);
+        mPinnedSliceManager = new PinnedSliceState(mSliceService, TEST_URI);
+    }
+
+    @Test
+    public void testMergeSpecs() {
+        // No annotations to start.
+        assertNull(mPinnedSliceManager.getSpecs());
+
+        mPinnedSliceManager.mergeSpecs(FIRST_SPECS);
+        assertArrayEquals(FIRST_SPECS, mPinnedSliceManager.getSpecs());
+
+        mPinnedSliceManager.mergeSpecs(SECOND_SPECS);
+        assertArrayEquals(new SliceSpec[]{
+                // spec1 is gone because it's not in the second set.
+                new SliceSpec("spec2", 1), // spec2 is 1 because it's smaller in the second set.
+                new SliceSpec("spec3", 2), // spec3 is the same in both sets
+                new SliceSpec("spec4", 1), // spec4 is 1 because it's smaller in the first set.
+                // spec5 is gone because it's not in the first set.
+        }, mPinnedSliceManager.getSpecs());
+    }
+
+    @Test
+    public void testSendPinnedOnCreate() throws RemoteException {
+        // When created, a pinned message should be sent.
+        TestableLooper.get(this).processAllMessages();
+
+        verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_PIN), eq(null),
+                argThat(b -> {
+                    assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI));
+                    return true;
+                }));
+    }
+
+    @Test
+    public void testSendUnpinnedOnDestroy() throws RemoteException {
+        TestableLooper.get(this).processAllMessages();
+        clearInvocations(mIContentProvider);
+
+        mPinnedSliceManager.destroy();
+        TestableLooper.get(this).processAllMessages();
+
+        verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_UNPIN), eq(null),
+                argThat(b -> {
+                    assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI));
+                    return true;
+                }));
+    }
+
+    @Test
+    public void testPkgPin() {
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.pin("pkg", FIRST_SPECS);
+        assertTrue(mPinnedSliceManager.isPinned());
+
+        assertTrue(mPinnedSliceManager.unpin("pkg"));
+        assertFalse(mPinnedSliceManager.isPinned());
+    }
+
+    @Test
+    public void testMultiPkgPin() {
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.pin("pkg", FIRST_SPECS);
+        assertTrue(mPinnedSliceManager.isPinned());
+        mPinnedSliceManager.pin("pkg2", FIRST_SPECS);
+
+        assertFalse(mPinnedSliceManager.unpin("pkg"));
+        assertTrue(mPinnedSliceManager.unpin("pkg2"));
+        assertFalse(mPinnedSliceManager.isPinned());
+    }
+
+    @Test
+    public void testListenerPin() {
+        ISliceListener listener = mock(ISliceListener.class);
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS);
+        assertTrue(mPinnedSliceManager.isPinned());
+
+        assertTrue(mPinnedSliceManager.removeSliceListener(listener));
+        assertFalse(mPinnedSliceManager.isPinned());
+    }
+
+    @Test
+    public void testMultiListenerPin() {
+        ISliceListener listener = mock(ISliceListener.class);
+        ISliceListener listener2 = mock(ISliceListener.class);
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS);
+        assertTrue(mPinnedSliceManager.isPinned());
+        mPinnedSliceManager.addSliceListener(listener2, FIRST_SPECS);
+
+        assertFalse(mPinnedSliceManager.removeSliceListener(listener));
+        assertTrue(mPinnedSliceManager.removeSliceListener(listener2));
+        assertFalse(mPinnedSliceManager.isPinned());
+    }
+
+    @Test
+    public void testPkgListenerPin() {
+        ISliceListener listener = mock(ISliceListener.class);
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS);
+        assertTrue(mPinnedSliceManager.isPinned());
+        mPinnedSliceManager.pin("pkg", FIRST_SPECS);
+
+        assertFalse(mPinnedSliceManager.removeSliceListener(listener));
+        assertTrue(mPinnedSliceManager.unpin("pkg"));
+        assertFalse(mPinnedSliceManager.isPinned());
+    }
+
+    @Test
+    public void testBind() throws RemoteException {
+        TestableLooper.get(this).processAllMessages();
+        clearInvocations(mIContentProvider);
+
+        ISliceListener listener = mock(ISliceListener.class);
+        Slice s = new Slice.Builder(TEST_URI).build();
+        Bundle b = new Bundle();
+        b.putParcelable(SliceProvider.EXTRA_SLICE, s);
+        when(mIContentProvider.call(anyString(), eq(SliceProvider.METHOD_SLICE), eq(null),
+                any())).thenReturn(b);
+
+        assertFalse(mPinnedSliceManager.isPinned());
+
+        mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS);
+
+        mPinnedSliceManager.onChange();
+        TestableLooper.get(this).processAllMessages();
+
+        verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_SLICE), eq(null),
+                argThat(bundle -> {
+                    assertEquals(TEST_URI, bundle.getParcelable(SliceProvider.EXTRA_BIND_URI));
+                    return true;
+                }));
+        verify(listener).onSliceUpdated(eq(s));
+    }
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java
new file mode 100644
index 0000000..fe9ea7a
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 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 android.content.ContentProvider.maybeAddUserId;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.app.slice.ISliceListener;
+import android.app.slice.SliceSpec;
+import android.content.pm.PackageManagerInternal;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import com.android.server.LocalServices;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class SliceManagerServiceTest extends UiServiceTestCase {
+
+    private static final String AUTH = "com.android.services.uitests";
+    private static final Uri TEST_URI = maybeAddUserId(Uri.parse("content://" + AUTH + "/path"), 0);
+
+    private static final SliceSpec[] EMPTY_SPECS = new SliceSpec[]{
+    };
+
+    private SliceManagerService mService;
+    private PinnedSliceState mCreatedSliceState;
+
+    @Before
+    public void setup() {
+        LocalServices.addService(PackageManagerInternal.class, mock(PackageManagerInternal.class));
+        mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
+        mContext.getTestablePermissions().setPermission(TEST_URI, PERMISSION_GRANTED);
+
+        mService = spy(new SliceManagerService(mContext, TestableLooper.get(this).getLooper()));
+        mCreatedSliceState = mock(PinnedSliceState.class);
+        doReturn(mCreatedSliceState).when(mService).createPinnedSlice(eq(TEST_URI));
+    }
+
+    @After
+    public void teardown() {
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+    }
+
+    @Test
+    public void testAddListenerCreatesPinned() throws RemoteException {
+        mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS);
+        verify(mService, times(1)).createPinnedSlice(eq(TEST_URI));
+    }
+
+    @Test
+    public void testAddListenerCreatesOnePinned() throws RemoteException {
+        mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS);
+        mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS);
+        verify(mService, times(1)).createPinnedSlice(eq(TEST_URI));
+    }
+
+    @Test
+    public void testRemoveListenerDestroysPinned() throws RemoteException {
+        ISliceListener listener = mock(ISliceListener.class);
+        mService.addSliceListener(TEST_URI, "pkg", listener, EMPTY_SPECS);
+
+        when(mCreatedSliceState.removeSliceListener(eq(listener))).thenReturn(false);
+        mService.removeSliceListener(TEST_URI, "pkg", listener);
+        verify(mCreatedSliceState, never()).destroy();
+
+        when(mCreatedSliceState.removeSliceListener(eq(listener))).thenReturn(true);
+        mService.removeSliceListener(TEST_URI, "pkg", listener);
+        verify(mCreatedSliceState).destroy();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testUnrecognizedThrows() throws RemoteException {
+        mService.removeSliceListener(TEST_URI, "pkg", mock(ISliceListener.class));
+    }
+
+    @Test
+    public void testAddPinCreatesPinned() throws RemoteException {
+        doReturn("pkg").when(mService).getDefaultHome(anyInt());
+
+        mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS);
+        mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS);
+        verify(mService, times(1)).createPinnedSlice(eq(TEST_URI));
+    }
+
+    @Test
+    public void testRemovePinDestroysPinned() throws RemoteException {
+        doReturn("pkg").when(mService).getDefaultHome(anyInt());
+
+        mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS);
+
+        when(mCreatedSliceState.unpin(eq("pkg"))).thenReturn(false);
+        mService.unpinSlice("pkg", TEST_URI);
+        verify(mCreatedSliceState, never()).destroy();
+
+        when(mCreatedSliceState.unpin(eq("pkg"))).thenReturn(true);
+        mService.unpinSlice("pkg", TEST_URI);
+        verify(mCreatedSliceState).destroy();
+    }
+
+}
\ No newline at end of file