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:
File diff suppressed because it is too large
Load Diff
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user