Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 331a64d0 authored by Geoffrey Pitsch's avatar Geoffrey Pitsch
Browse files

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
parent 8e8269fc
Loading
Loading
Loading
Loading
+321 −264

File changed.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -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" />
+175 −15
Original line number Diff line number Diff line
@@ -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);
    }
}