[CTS] Test that permissions are revoked when listener removed.

Bug: 162233630
Test: android.app.cts.NotificationManagerTest
Change-Id: If7ca72fef3035e4a384469e69d554aa9fbc7ede3
(cherry picked from commit 65b0c0ab7c3a4e3a6284c4d5e7570b9de278a32b)
diff --git a/tests/app/Android.bp b/tests/app/Android.bp
index 827375b..bc5b110 100644
--- a/tests/app/Android.bp
+++ b/tests/app/Android.bp
@@ -33,7 +33,10 @@
         "platformprotosnano",
         "permission-test-util-lib"
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "NotificationListener/src/com/android/test/notificationlistener/INotificationUriAccessService.aidl",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/app/AndroidTest.xml b/tests/app/AndroidTest.xml
index 46cf4cc..465b633 100644
--- a/tests/app/AndroidTest.xml
+++ b/tests/app/AndroidTest.xml
@@ -34,6 +34,7 @@
         <option name="test-file-name" value="CtsCantSaveState2.apk" />
         <option name="test-file-name" value="NotificationDelegator.apk" />
         <option name="test-file-name" value="NotificationProvider.apk" />
+        <option name="test-file-name" value="NotificationListener.apk" />
         <option name="test-file-name" value="StorageDelegator.apk" />
         <option name="test-file-name" value="CtsActivityManagerApi29.apk" />
     </target_preparer>
diff --git a/tests/app/NotificationListener/Android.bp b/tests/app/NotificationListener/Android.bp
new file mode 100644
index 0000000..96a0c3c
--- /dev/null
+++ b/tests/app/NotificationListener/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2020 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.
+
+android_test_helper_app {
+    name: "NotificationListener",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "**/*.java",
+        "**/*.kt",
+        "src/com/android/test/notificationlistener/INotificationUriAccessService.aidl",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+    ],
+    platform_apis: true,
+    sdk_version: "test_current",
+}
diff --git a/tests/app/NotificationListener/AndroidManifest.xml b/tests/app/NotificationListener/AndroidManifest.xml
new file mode 100644
index 0000000..f510637
--- /dev/null
+++ b/tests/app/NotificationListener/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.notificationlistener">
+    <application android:label="Notification Listener">
+
+        <service android:name=".NotificationUriAccessService"
+                 android:exported="true"/>
+
+        <service android:name=".TestNotificationListener"
+                 android:exported="true"
+                 android:label="TestNotificationListener"
+                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService"/>
+            </intent-filter>
+        </service>
+
+    </application>
+</manifest>
diff --git a/tests/app/NotificationListener/res/layout/activity.xml b/tests/app/NotificationListener/res/layout/activity.xml
new file mode 100644
index 0000000..f001f29
--- /dev/null
+++ b/tests/app/NotificationListener/res/layout/activity.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="25dp"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:text="@string/activity_description"
+    />
+
+</LinearLayout>
diff --git a/tests/app/NotificationListener/res/values/strings.xml b/tests/app/NotificationListener/res/values/strings.xml
new file mode 100644
index 0000000..e19d5bf
--- /dev/null
+++ b/tests/app/NotificationListener/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<resources>
+    <string name="activity_description">This app has a listener and an service used for tests</string>
+</resources>
\ No newline at end of file
diff --git a/tests/app/NotificationListener/src/com/android/test/notificationlistener/INotificationUriAccessService.aidl b/tests/app/NotificationListener/src/com/android/test/notificationlistener/INotificationUriAccessService.aidl
new file mode 100644
index 0000000..eb93179
--- /dev/null
+++ b/tests/app/NotificationListener/src/com/android/test/notificationlistener/INotificationUriAccessService.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 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.test.notificationlistener;
+
+interface INotificationUriAccessService {
+    void ensureNotificationListenerServiceConnected(boolean connected);
+    boolean isFileUriAccessible(in android.net.Uri uri);
+}
diff --git a/tests/app/NotificationListener/src/com/android/test/notificationlistener/NotificationUriAccessService.kt b/tests/app/NotificationListener/src/com/android/test/notificationlistener/NotificationUriAccessService.kt
new file mode 100644
index 0000000..5e6e469
--- /dev/null
+++ b/tests/app/NotificationListener/src/com/android/test/notificationlistener/NotificationUriAccessService.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020 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.test.notificationlistener
+
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.net.Uri
+import android.os.IBinder
+import java.io.IOException
+
+class NotificationUriAccessService : Service() {
+    private inner class MyNotificationUriAccessService : INotificationUriAccessService.Stub() {
+        @Throws(IllegalStateException::class)
+        override fun ensureNotificationListenerServiceConnected(ensureConnected: Boolean) {
+            val nm = getSystemService(NotificationManager::class.java)!!
+            val testListener = TestNotificationListener.componentName
+            check(nm.isNotificationListenerAccessGranted(testListener) == ensureConnected) {
+                "$testListener has incorrect listener access; expected=$ensureConnected"
+            }
+            val listener = TestNotificationListener.instance
+            if (ensureConnected) {
+                check(listener != null) {
+                    "$testListener has not been created, but should be connected"
+                }
+            }
+            val isConnected = listener?.isConnected ?: false
+            check(isConnected == ensureConnected) {
+                "$testListener has incorrect listener connection state; expected=$ensureConnected"
+            }
+        }
+
+        override fun isFileUriAccessible(uri: Uri?): Boolean {
+            try {
+                contentResolver.openAssetFile(uri!!, "r", null).use { return true }
+            } catch (e: SecurityException) {
+                return false
+            } catch (e: IOException) {
+                throw IllegalStateException("Exception without security error", e)
+            }
+        }
+    }
+
+    private val mBinder = MyNotificationUriAccessService()
+    override fun onBind(intent: Intent): IBinder? {
+        return mBinder
+    }
+}
\ No newline at end of file
diff --git a/tests/app/NotificationListener/src/com/android/test/notificationlistener/TestNotificationListener.kt b/tests/app/NotificationListener/src/com/android/test/notificationlistener/TestNotificationListener.kt
new file mode 100644
index 0000000..19c7d82
--- /dev/null
+++ b/tests/app/NotificationListener/src/com/android/test/notificationlistener/TestNotificationListener.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 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.test.notificationlistener
+
+import android.content.ComponentName
+import android.service.notification.NotificationListenerService
+
+class TestNotificationListener : NotificationListenerService() {
+    var isConnected = false
+
+    override fun onListenerConnected() {
+        super.onListenerConnected()
+        instance = this
+        isConnected = true
+    }
+
+    override fun onListenerDisconnected() {
+        super.onListenerDisconnected()
+        isConnected = false
+    }
+
+    companion object {
+        var instance: TestNotificationListener? = null
+            private set
+        val componentName: ComponentName by lazy {
+            val javaClass = TestNotificationListener::class.java
+            ComponentName(javaClass.getPackage().name, javaClass.name)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/app/src/android/app/cts/NotificationManagerTest.java b/tests/app/src/android/app/cts/NotificationManagerTest.java
index f5721dd..b8f76bd 100644
--- a/tests/app/src/android/app/cts/NotificationManagerTest.java
+++ b/tests/app/src/android/app/cts/NotificationManagerTest.java
@@ -80,6 +80,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.OperationApplicationException;
+import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
@@ -93,6 +94,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -114,12 +116,11 @@
 import android.widget.RemoteViews;
 
 import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.FeatureUtil;
 import com.android.compatibility.common.util.SystemUtil;
-
-import junit.framework.Assert;
+import com.android.test.notificationlistener.INotificationUriAccessService;
 
 import java.io.BufferedReader;
 import java.io.FileInputStream;
@@ -136,6 +137,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 /* This tests NotificationListenerService together with NotificationManager, as you need to have
@@ -166,6 +168,7 @@
     private List<String> mRuleIds;
     private BroadcastReceiver mBubbleBroadcastReceiver;
     private boolean mBubblesEnabledSettingToRestore;
+    private INotificationUriAccessService mNotificationUriAccessService;
 
     @Override
     protected void setUp() throws Exception {
@@ -201,7 +204,8 @@
         // delay between tests so notifications aren't dropped by the rate limiter
         try {
             Thread.sleep(500);
-        } catch(InterruptedException e) {}
+        } catch (InterruptedException e) {
+        }
     }
 
     @Override
@@ -301,8 +305,8 @@
         try {
             Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
                     Uri.encode(phone));
-            String[] projection = new String[] { ContactsContract.Contacts._ID,
-                    ContactsContract.Contacts.LOOKUP_KEY };
+            String[] projection = new String[]{ContactsContract.Contacts._ID,
+                    ContactsContract.Contacts.LOOKUP_KEY};
             c = mContext.getContentResolver().query(phoneUri, projection, null, null, null);
             if (c != null && c.getCount() > 0) {
                 c.moveToFirst();
@@ -478,16 +482,16 @@
 
         if (data == null) {
             data = new Notification.BubbleMetadata.Builder(pendingIntent,
-                            Icon.createWithResource(mContext, R.drawable.black))
+                    Icon.createWithResource(mContext, R.drawable.black))
                     .build();
         }
         if (builder == null) {
             builder = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
-                            .setSmallIcon(R.drawable.black)
-                            .setWhen(System.currentTimeMillis())
-                            .setContentTitle("notify#" + id)
-                            .setContentText("This is #" + id + "notification  ")
-                            .setContentIntent(pendingIntent);
+                    .setSmallIcon(R.drawable.black)
+                    .setWhen(System.currentTimeMillis())
+                    .setContentTitle("notify#" + id)
+                    .setContentText("This is #" + id + "notification  ")
+                    .setContentIntent(pendingIntent);
         }
         builder.setBubbleMetadata(data);
 
@@ -530,7 +534,7 @@
         // getActiveNotifications()
         // we will check for it for up to 300ms before giving up
         boolean found = false;
-        for (int tries = 3; tries--> 0;) {
+        for (int tries = 3; tries-- > 0; ) {
             // Need reset flag.
             found = false;
             final StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
@@ -556,7 +560,7 @@
         // getActiveNotifications()
         // we will check for it for up to 400ms before giving up
         int lastCount = 0;
-        for (int tries = 4; tries-- > 0;) {
+        for (int tries = 4; tries-- > 0; ) {
             final StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
             lastCount = sbns.length;
             if (expectedCount == lastCount) return;
@@ -566,7 +570,7 @@
                 // pass
             }
         }
-        fail("Expected " + expectedCount + " posted notifications, were " +  lastCount);
+        fail("Expected " + expectedCount + " posted notifications, were " + lastCount);
     }
 
     private void compareChannels(NotificationChannel expected, NotificationChannel actual) {
@@ -621,7 +625,6 @@
     }
 
     private void toggleListenerAccess(boolean on) throws IOException {
-
         String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ")
                 + TestNotificationListener.getId();
 
@@ -633,6 +636,13 @@
                 on, nm.isNotificationListenerAccessGranted(listenerComponent));
     }
 
+    private void toggleExternalListenerAccess(ComponentName listenerComponent, boolean on)
+            throws IOException {
+        String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ")
+                + listenerComponent.flattenToString();
+        runCommand(command, InstrumentationRegistry.getInstrumentation());
+    }
+
     private void setBubblesGlobal(boolean enabled)
             throws InterruptedException {
         SystemUtil.runWithShellPermissionIdentity(() ->
@@ -662,15 +672,18 @@
         Thread.sleep(500); // wait for ranking update
     }
 
+    @SuppressWarnings("StatementWithEmptyBody")
     private void runCommand(String command, Instrumentation instrumentation) throws IOException {
         UiAutomation uiAutomation = instrumentation.getUiAutomation();
         // Execute command
         try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) {
-            Assert.assertNotNull("Failed to execute shell command: " + command, fd);
+            assertNotNull("Failed to execute shell command: " + command, fd);
             // Wait for the command to finish by reading until EOF
             try (InputStream in = new FileInputStream(fd.getFileDescriptor())) {
                 byte[] buffer = new byte[4096];
-                while (in.read(buffer) > 0) {}
+                while (in.read(buffer) > 0) {
+                    // discard output
+                }
             } catch (IOException e) {
                 throw new IOException("Could not read stdout of command: " + command, e);
             }
@@ -704,7 +717,7 @@
 
     private void assertExpectedDndState(int expectedState) {
         int tries = 3;
-        for (int i = tries; i >=0; i--) {
+        for (int i = tries; i >= 0; i--) {
             if (expectedState ==
                     mNotificationManager.getCurrentInterruptionFilter()) {
                 break;
@@ -814,11 +827,11 @@
         KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
         keyguardManager.requestDismissKeyguard(sendBubbleActivity,
                 new KeyguardManager.KeyguardDismissCallback() {
-            @Override
-            public void onDismissSucceeded() {
-                latch.countDown();
-            }
-        });
+                    @Override
+                    public void onDismissSucceeded() {
+                        latch.countDown();
+                    }
+                });
         try {
             latch.await(500, TimeUnit.MILLISECONDS);
         } catch (InterruptedException e) {
@@ -1104,7 +1117,7 @@
                 new NotificationChannel(mId, "name", IMPORTANCE_DEFAULT);
         channel.setDescription("bananas");
         channel.enableVibration(true);
-        channel.setVibrationPattern(new long[] {5, 8, 2, 1});
+        channel.setVibrationPattern(new long[]{5, 8, 2, 1});
         channel.setSound(new Uri.Builder().scheme("test").build(),
                 new AudioAttributes.Builder().setUsage(
                         AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED).build());
@@ -1219,7 +1232,8 @@
         try {
             mNotificationManager.createNotificationChannel(channel);
             fail("Created notification with bad group");
-        } catch (IllegalArgumentException e) {}
+        } catch (IllegalArgumentException e) {
+        }
     }
 
     public void testCreateChannelInvalidImportance() throws Exception {
@@ -1929,7 +1943,8 @@
                         .setStyle(new Notification.BigPictureStyle()
                                 .setBigContentTitle("title")
                                 .bigPicture(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565))
-                                .bigLargeIcon(Icon.createWithResource(getContext(), R.drawable.icon_blue))
+                                .bigLargeIcon(
+                                        Icon.createWithResource(getContext(), R.drawable.icon_blue))
                                 .setSummaryText("summary"))
                         .build();
         mNotificationManager.notify(id, notification);
@@ -2360,31 +2375,31 @@
     }
 
     public void testNotificationPolicyVisualEffectsEqual() {
-        NotificationManager.Policy policy = new NotificationManager.Policy(0,0 ,0 ,
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_SCREEN_ON);
-        NotificationManager.Policy policy2 = new NotificationManager.Policy(0,0 ,0 ,
+        NotificationManager.Policy policy2 = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_PEEK);
         assertTrue(policy.equals(policy2));
         assertTrue(policy2.equals(policy));
 
-        policy = new NotificationManager.Policy(0,0 ,0 ,
+        policy = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_SCREEN_ON);
-        policy2 = new NotificationManager.Policy(0,0 ,0 ,
+        policy2 = new NotificationManager.Policy(0, 0, 0,
                 0);
         assertFalse(policy.equals(policy2));
         assertFalse(policy2.equals(policy));
 
-        policy = new NotificationManager.Policy(0,0 ,0 ,
+        policy = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_SCREEN_OFF);
-        policy2 = new NotificationManager.Policy(0,0 ,0 ,
+        policy2 = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_FULL_SCREEN_INTENT | SUPPRESSED_EFFECT_AMBIENT
                         | SUPPRESSED_EFFECT_LIGHTS);
         assertTrue(policy.equals(policy2));
         assertTrue(policy2.equals(policy));
 
-        policy = new NotificationManager.Policy(0,0 ,0 ,
+        policy = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_SCREEN_OFF);
-        policy2 = new NotificationManager.Policy(0,0 ,0 ,
+        policy2 = new NotificationManager.Policy(0, 0, 0,
                 SUPPRESSED_EFFECT_LIGHTS);
         assertFalse(policy.equals(policy2));
         assertFalse(policy2.equals(policy));
@@ -2464,7 +2479,7 @@
 
         try {
             mNotificationManager.cancelAsPackage(DELEGATOR, null, 9);
-            fail ("Delegate should not be able to cancel notification they did not post");
+            fail("Delegate should not be able to cancel notification they did not post");
         } catch (SecurityException e) {
             // yay
         }
@@ -2763,6 +2778,129 @@
         }
     }
 
+    public void testNotificationUriPermissionsRevokedFromRemovedListeners() throws Exception {
+        Uri background7Uri = Uri.parse(
+                "content://com.android.test.notificationprovider.provider/background7.png");
+
+        toggleListenerAccess(true);
+        Thread.sleep(500); // wait for listener to be allowed
+
+        try {
+            // Post #7
+            performNotificationProviderAction("send-7");
+
+            mListener = TestNotificationListener.getInstance();
+            assertNotNull(mListener);
+
+            assertEquals(background7Uri, getNotificationBackgroundImageUri(7));
+            assertAccessible(background7Uri);
+
+            // Remove the listener to ensure permissions get revoked
+            toggleListenerAccess(false);
+            Thread.sleep(500); // wait for listener to be disabled
+
+            assertInaccessible(background7Uri);
+
+        } finally {
+            // Clean Up -- Cancel #7
+            performNotificationProviderAction("cancel-7");
+            Thread.sleep(500);
+        }
+    }
+
+    private class NotificationListenerConnection implements ServiceConnection {
+        private final Semaphore mSemaphore = new Semaphore(0);
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mNotificationUriAccessService = INotificationUriAccessService.Stub.asInterface(service);
+            mSemaphore.release();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            mNotificationUriAccessService = null;
+        }
+
+        public void waitForService() {
+            try {
+                if (mSemaphore.tryAcquire(5, TimeUnit.SECONDS)) {
+                    return;
+                }
+            } catch (InterruptedException e) {
+            }
+            fail("failed to connec to service");
+        }
+    }
+
+    ;
+
+    public void testNotificationUriPermissionsRevokedOnlyFromRemovedListeners() throws Exception {
+        Uri background7Uri = Uri.parse(
+                "content://com.android.test.notificationprovider.provider/background7.png");
+
+        // Connect to a service in the NotificationListener app which allows us to validate URI
+        // permissions granted to a second app, so that we show that permissions aren't being
+        // revoked too broadly.
+        final Intent intent = new Intent();
+        intent.setComponent(new ComponentName("com.android.test.notificationlistener",
+                "com.android.test.notificationlistener.NotificationUriAccessService"));
+        NotificationListenerConnection connection = new NotificationListenerConnection();
+        mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE);
+        connection.waitForService();
+
+        // Before starting the test, make sure the service works, that there is no listener, and
+        // that the URI starts inaccessible to that process.
+        mNotificationUriAccessService.ensureNotificationListenerServiceConnected(false);
+        assertFalse(mNotificationUriAccessService.isFileUriAccessible(background7Uri));
+
+        // Give the NotificationListener app access to notifications, and validate that.
+        toggleExternalListenerAccess(new ComponentName("com.android.test.notificationlistener",
+                "com.android.test.notificationlistener.TestNotificationListener"), true);
+        Thread.sleep(500);
+        mNotificationUriAccessService.ensureNotificationListenerServiceConnected(true);
+        assertFalse(mNotificationUriAccessService.isFileUriAccessible(background7Uri));
+
+        // Give the test app access to notifications, and get that listener
+        toggleListenerAccess(true);
+        Thread.sleep(500); // wait for listener to be allowed
+        mListener = TestNotificationListener.getInstance();
+        assertNotNull(mListener);
+
+        try {
+            try {
+                // Post #7
+                performNotificationProviderAction("send-7");
+
+                // Check that both the test app (this code) and the external app have URI access.
+                assertEquals(background7Uri, getNotificationBackgroundImageUri(7));
+                assertAccessible(background7Uri);
+                assertTrue(mNotificationUriAccessService.isFileUriAccessible(background7Uri));
+
+                // Remove the listener to ensure permissions get revoked
+                toggleListenerAccess(false);
+                Thread.sleep(500); // wait for listener to be disabled
+
+                // Ensure that revoking listener access to this one app does not effect the other.
+                assertInaccessible(background7Uri);
+                assertTrue(mNotificationUriAccessService.isFileUriAccessible(background7Uri));
+
+            } finally {
+                // Clean Up -- Cancel #7
+                performNotificationProviderAction("cancel-7");
+                Thread.sleep(500);
+            }
+
+            // Finally, cancelling the permission must still revoke those other permissions.
+            assertFalse(mNotificationUriAccessService.isFileUriAccessible(background7Uri));
+
+        } finally {
+            // Clean Up -- Make sure the external listener is has access revoked
+            toggleExternalListenerAccess(new ComponentName("com.android.test.notificationlistener",
+                    "com.android.test.notificationlistener.TestNotificationListener"), false);
+        }
+    }
+
     private void assertAccessible(Uri uri)
             throws IOException {
         ContentResolver contentResolver = mContext.getContentResolver();