Fix cancel notification race condition bug.

Cancel should always remove notifications, regardless of whether
they've been posted internally.  Primarily accomplished by ensuring
all cancels happen on the same handler, and that they check the
enqueued list as well the main notifications list.

Also fixes snoozing for enqueued notifications, since it is
implemented via cancelling.

Test: runtest systemui-notification
Change-Id: If8295a38102f245013d29831a060fabd80716e4b
This commit is contained in:
Geoffrey Pitsch
2017-01-17 14:00:47 -05:00
parent 8e8269fc11
commit 331a64d091
3 changed files with 500 additions and 282 deletions

View File

@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
<application>
<uses-library android:name="android.test.runner" />

View File

@@ -24,7 +24,9 @@ import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,18 +35,25 @@ import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.support.test.annotation.UiThreadTest;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -58,30 +67,76 @@ import com.android.server.lights.LightsManager;
@RunWith(AndroidJUnit4.class)
public class NotificationManagerServiceTest {
private final String pkg = "com.android.server.notification";
private final int uid = 0;
private final int uid = Binder.getCallingUid();
private NotificationManagerService mNotificationManagerService;
private INotificationManager mBinderService;
private IPackageManager mPackageManager = mock(IPackageManager.class);
private Context mContext;
private HandlerThread mThread;
@Before
@UiThreadTest
public void setUp() throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
mNotificationManagerService = new NotificationManagerService(context);
mContext = InstrumentationRegistry.getTargetContext();
mNotificationManagerService = new NotificationManagerService(mContext);
// MockPackageManager - default returns ApplicationInfo with matching calling UID
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.uid = Binder.getCallingUid();
applicationInfo.uid = uid;
when(mPackageManager.getApplicationInfo(any(), anyInt(), anyInt()))
.thenReturn(applicationInfo);
final PackageManager mockPackageManagerClient = mock(PackageManager.class);
when(mockPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
.thenReturn(applicationInfo);
final LightsManager mockLightsManager = mock(LightsManager.class);
when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class));
mNotificationManagerService.init(mPackageManager, mockLightsManager);
// Use a separate thread for service looper.
mThread = new HandlerThread("TestThread");
mThread.start();
// Mock NotificationListeners to bypass security checks.
final NotificationManagerService.NotificationListeners mockNotificationListeners =
mock(NotificationManagerService.NotificationListeners.class);
when(mockNotificationListeners.checkServiceTokenLocked(any())).thenReturn(
mockNotificationListeners.new ManagedServiceInfo(null,
new ComponentName(pkg, "test_class"), uid, true, null, 0));
mNotificationManagerService.init(mThread.getLooper(), mPackageManager,
mockPackageManagerClient, mockLightsManager, mockNotificationListeners);
// Tests call directly into the Binder.
mBinderService = mNotificationManagerService.getBinderService();
}
public void waitForIdle() throws Exception {
MessageQueue queue = mThread.getLooper().getQueue();
CountDownLatch latch = new CountDownLatch(1);
queue.addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
latch.countDown();
return false;
}
});
latch.await();
if (!queue.isIdle()) {
waitForIdle();
}
}
private NotificationRecord generateNotificationRecord(NotificationChannel channel) {
if (channel == null) {
channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
}
Notification n = new Notification.Builder(mContext)
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setPriority(Notification.PRIORITY_HIGH)
.build();
StatusBarNotification sbn = new StatusBarNotification(mContext.getPackageName(),
mContext.getPackageName(), channel, 1, "tag", uid, 0,
n, new UserHandle(uid), null, 0);
return new NotificationRecord(mContext, sbn);
}
@Test
@UiThreadTest
public void testCreateNotificationChannels_SingleChannel() throws Exception {
@@ -203,15 +258,120 @@ public class NotificationManagerServiceTest {
verify(usageStats, times(1)).registerBlocked(eq(r));
}
private NotificationRecord generateNotificationRecord(NotificationChannel channel) {
final Context context = InstrumentationRegistry.getTargetContext();
Notification n = new Notification.Builder(context)
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setPriority(Notification.PRIORITY_HIGH)
.build();
StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, channel, 1, "tag", uid, uid,
n, UserHandle.SYSTEM, null, uid);
return new NotificationRecord(context, sbn);
@Test
@UiThreadTest
public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception {
mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
generateNotificationRecord(null).getNotification(), new int[1], 0);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(mContext.getPackageName());
assertEquals(1, notifs.length);
}
@Test
@UiThreadTest
public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception {
mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
generateNotificationRecord(null).getNotification(), new int[1], 0);
mBinderService.cancelNotificationWithTag(mContext.getPackageName(), "tag", 0, 0);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(mContext.getPackageName());
assertEquals(0, notifs.length);
}
@Test
@UiThreadTest
public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.cancelNotificationsFromListener(null, null);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(0, notifs.length);
}
@Test
@UiThreadTest
public void testCancelAllNotificationsImmediatelyAfterEnqueue() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(0, notifs.length);
}
@Test
@UiThreadTest
public void testCancelAllNotifications_IgnoreForegroundService() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(1, notifs.length);
}
@Test
@UiThreadTest
public void testCancelAllNotifications_IgnoreOtherPackages() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.cancelAllNotifications("other_pkg_name", sbn.getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(1, notifs.length);
}
@Test
@UiThreadTest
public void testCancelAllNotifications_NullPkgRemovesAll() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.cancelAllNotifications(null, sbn.getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(0, notifs.length);
}
@Test
@UiThreadTest
public void testCancelAllNotifications_NullPkgIgnoresUserAllNotifications() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], UserHandle.USER_ALL);
// Null pkg is how we signal a user switch.
mBinderService.cancelAllNotifications(null, sbn.getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(1, notifs.length);
}
@Test
@UiThreadTest
public void testSnoozeNotificationImmediatelyAfterEnqueue() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId());
mBinderService.snoozeNotificationFromListener(null, sbn.getKey());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(sbn.getPackageName());
assertEquals(0, notifs.length);
}
}