[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();