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

Commit 2fb8abd4 authored by Alexander Roederer's avatar Alexander Roederer
Browse files

Lifetime extension to not update on multicancel

Adds a new field to NotificationRecord that records when a lifetime
extended notification has been cancelled by an app. Then, when an app
sends a cancellation, we check that field before we update system UI.
This allows us to avoid sending multiple updates to system ui when an
app cancels repeatedly. These multiple cancelations were causing
rendering issues.

As a side effect, we can also remove the ONLY_ALERT_ONCE flag we added,
because we'll no longer cause multiple huns to appear because of sending
multiple updates.

Bug: 299448097
Flag: android.app.lifetime_extension_refactor
Test: flash, atest NotificationManagerServiceTest
Change-Id: Idc8e16d7aef321eaa76c64214febb79af4690463
parent 028cedc3
Loading
Loading
Loading
Loading
+48 −9
Original line number Original line Diff line number Diff line
@@ -1583,6 +1583,8 @@ public class NotificationManagerService extends SystemService {
                    // respond to direct replies with updates. So we need to update System UI
                    // respond to direct replies with updates. So we need to update System UI
                    // immediately.
                    // immediately.
                    if (lifetimeExtensionRefactor()) {
                    if (lifetimeExtensionRefactor()) {
                        // We need to reset this to allow the notif to be updated again.
                        r.setCanceledAfterLifetimeExtension(false);
                        maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                        maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                                r.getSbn().getPackageName(), packageImportance);
                                r.getSbn().getPackageName(), packageImportance);
                    }
                    }
@@ -1639,9 +1641,12 @@ public class NotificationManagerService extends SystemService {
                    // respond to direct replies with updates. So we need to update System UI
                    // respond to direct replies with updates. So we need to update System UI
                    // immediately.
                    // immediately.
                    if (lifetimeExtensionRefactor()) {
                    if (lifetimeExtensionRefactor()) {
                        // We need to reset this to allow the notif to be updated again.
                        r.setCanceledAfterLifetimeExtension(false);
                        maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                        maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                                r.getSbn().getPackageName(), packageImportance);
                                r.getSbn().getPackageName(), packageImportance);
                    }
                    }
                    r.recordSmartReplied();
                    r.recordSmartReplied();
                    LogMaker logMaker = r.getLogMaker()
                    LogMaker logMaker = r.getLogMaker()
                            .setCategory(MetricsEvent.SMART_REPLY_ACTION)
                            .setCategory(MetricsEvent.SMART_REPLY_ACTION)
@@ -11741,17 +11746,37 @@ public class NotificationManagerService extends SystemService {
    private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
    private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
            String pkg, int packageImportance) {
            String pkg, int packageImportance) {
        if (record != null && (record.getSbn().getNotification().flags
        if (record != null && (record.getSbn().getNotification().flags
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
                && !record.isCanceledAfterLifetimeExtension()) {
            boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
            boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
            // Lifetime extended notifications don't need to alert on state change.
            // Save the original Record's post silently value, so we can restore it after we send
            // the SystemUI specific silent update.
            boolean savedPostSilentlyState = record.shouldPostSilently();
            boolean savedOnlyAlertOnceState = (record.getNotification().flags
                    & FLAG_ONLY_ALERT_ONCE) > 0;
            // Lifetime extended notifications don't need to alert on new state change.
            record.setPostSilently(true);
            record.setPostSilently(true);
            // We also set FLAG_ONLY_ALERT_ONCE to avoid the notification from HUN-ing again.
            // We also set FLAG_ONLY_ALERT_ONCE to avoid the notification from HUN-ing again.
            record.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
            record.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
            PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null);
            tracker.addCleanupRunnable(() -> {
                synchronized (mNotificationLock) {
                    // Mark that the notification has been updated due to cancelation, so it won't
                    // be updated again if the app cancels multiple times.
                    record.setCanceledAfterLifetimeExtension(true);
                    // Set the post silently status to the record's previous value.
                    record.setPostSilently(savedPostSilentlyState);
                    // Remove FLAG_ONLY_ALERT_ONCE if the notification did not previously have it.
                    if (!savedOnlyAlertOnceState) {
                        record.getNotification().flags &= ~FLAG_ONLY_ALERT_ONCE;
                    }
                }
            });
            mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
            mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
                    record, isAppForeground, /* isAppProvided= */ false,
                    record, isAppForeground, /* isAppProvided= */ false, tracker));
                    mPostNotificationTrackerFactory.newTracker(null)));
        }
        }
    }
    }
@@ -13351,17 +13376,23 @@ public class NotificationManagerService extends SystemService {
        @ElapsedRealtimeLong private final long mStartTime;
        @ElapsedRealtimeLong private final long mStartTime;
        @Nullable private final WakeLock mWakeLock;
        @Nullable private final WakeLock mWakeLock;
        private boolean mOngoing;
        private boolean mOngoing;
        private final List<Runnable> mCleanupRunnables;
        @VisibleForTesting
        @VisibleForTesting
        PostNotificationTracker(@Nullable WakeLock wakeLock) {
        PostNotificationTracker(@Nullable WakeLock wakeLock) {
            mStartTime = SystemClock.elapsedRealtime();
            mStartTime = SystemClock.elapsedRealtime();
            mWakeLock = wakeLock;
            mWakeLock = wakeLock;
            mOngoing = true;
            mOngoing = true;
            mCleanupRunnables = new ArrayList<Runnable>();
            if (DBG) {
            if (DBG) {
                Slog.d(TAG, "PostNotification: Started");
                Slog.d(TAG, "PostNotification: Started");
            }
            }
        }
        }
        void addCleanupRunnable(Runnable runnable) {
            mCleanupRunnables.add(runnable);
        }
        @ElapsedRealtimeLong
        @ElapsedRealtimeLong
        long getStartTime() {
        long getStartTime() {
            return mStartTime;
            return mStartTime;
@@ -13373,8 +13404,9 @@ public class NotificationManagerService extends SystemService {
        }
        }
        /**
        /**
         * Cancels the tracker (releasing the acquired WakeLock). Either {@link #finish} or
         * Cancels the tracker (releasing the acquired WakeLock) and runs any set cleanup runnables.
         * {@link #cancel} (exclusively) should be called on this object before it's discarded.
         * Either {@link #finish} or {@link #cancel} (exclusively) should be called on this object
         * before it's discarded.
         */
         */
        void cancel() {
        void cancel() {
            if (!isOngoing()) {
            if (!isOngoing()) {
@@ -13385,6 +13417,9 @@ public class NotificationManagerService extends SystemService {
            if (mWakeLock != null) {
            if (mWakeLock != null) {
                Binder.withCleanCallingIdentity(() -> mWakeLock.release());
                Binder.withCleanCallingIdentity(() -> mWakeLock.release());
            }
            }
            for (Runnable r : mCleanupRunnables) {
                r.run();
            }
            if (DBG) {
            if (DBG) {
                long elapsedTime = SystemClock.elapsedRealtime() - mStartTime;
                long elapsedTime = SystemClock.elapsedRealtime() - mStartTime;
                Slog.d(TAG, TextUtils.formatSimple("PostNotification: Abandoned after %d ms",
                Slog.d(TAG, TextUtils.formatSimple("PostNotification: Abandoned after %d ms",
@@ -13393,9 +13428,10 @@ public class NotificationManagerService extends SystemService {
        }
        }
        /**
        /**
         * Finishes the tracker (releasing the acquired WakeLock) and returns the time elapsed since
         * Finishes the tracker (releasing the acquired WakeLock), runs any set cleanup runnables,
         * the operation started, in milliseconds. Either {@link #finish} or {@link #cancel}
         * and returns the time elapsed since the operation started, in milliseconds.
         * (exclusively) should be called on this object before it's discarded.
         * Either {@link #finish} or {@link #cancel} (exclusively) should be called on this object
         * before it's discarded.
         */
         */
        @DurationMillisLong
        @DurationMillisLong
        long finish() {
        long finish() {
@@ -13408,6 +13444,9 @@ public class NotificationManagerService extends SystemService {
            if (mWakeLock != null) {
            if (mWakeLock != null) {
                Binder.withCleanCallingIdentity(() -> mWakeLock.release());
                Binder.withCleanCallingIdentity(() -> mWakeLock.release());
            }
            }
            for (Runnable r : mCleanupRunnables) {
                r.run();
            }
            if (DBG) {
            if (DBG) {
                Slog.d(TAG,
                Slog.d(TAG,
                        TextUtils.formatSimple("PostNotification: Finished in %d ms", elapsedTime));
                        TextUtils.formatSimple("PostNotification: Finished in %d ms", elapsedTime));
+12 −0
Original line number Original line Diff line number Diff line
@@ -222,6 +222,9 @@ public final class NotificationRecord {
    private boolean mPendingLogUpdate = false;
    private boolean mPendingLogUpdate = false;
    private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
    private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
    private boolean mSensitiveContent = false;
    private boolean mSensitiveContent = false;
    // Whether an app has attempted to cancel this notification after it has been marked as
    // lifetime extended.
    private boolean mCanceledAfterLifetimeExtension = false;


    public NotificationRecord(Context context, StatusBarNotification sbn,
    public NotificationRecord(Context context, StatusBarNotification sbn,
            NotificationChannel channel) {
            NotificationChannel channel) {
@@ -535,6 +538,7 @@ public final class NotificationRecord {
                + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
                + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
        pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
        pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
        pw.println(prefix + "mSensitiveContent=" + mSensitiveContent);
        pw.println(prefix + "mSensitiveContent=" + mSensitiveContent);
        pw.println(prefix + "mCanceledAfterLifetimeExtension=" + mCanceledAfterLifetimeExtension);
        pw.println(prefix + "mIntercept=" + mIntercept);
        pw.println(prefix + "mIntercept=" + mIntercept);
        pw.println(prefix + "mHidden==" + mHidden);
        pw.println(prefix + "mHidden==" + mHidden);
        pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
        pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
@@ -1620,6 +1624,14 @@ public final class NotificationRecord {
        mPkgAllowedAsConvo = allowedAsConvo;
        mPkgAllowedAsConvo = allowedAsConvo;
    }
    }


    public boolean isCanceledAfterLifetimeExtension() {
        return mCanceledAfterLifetimeExtension;
    }

    public void setCanceledAfterLifetimeExtension(boolean canceledAfterLifetimeExtension) {
        mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension;
    }

    /**
    /**
     * Whether this notification is a conversation notification.
     * Whether this notification is a conversation notification.
     */
     */
+133 −3
Original line number Original line Diff line number Diff line
@@ -3045,6 +3045,41 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
    }
    }
    @Test
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    public void testMultipleCancelOfLifetimeExtendedSendsOneUpdate() throws Exception {
        final NotificationRecord notif = generateNotificationRecord(null);
        notif.getSbn().getNotification().flags =
                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        mService.addNotification(notif);
        final StatusBarNotification sbn = notif.getSbn();
        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
        // Send two cancelations.
        mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
                sbn.getUserId());
        waitForIdle();
        mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
                sbn.getUserId());
        waitForIdle();
        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
        // Checks that only one post update is sent.
        verify(mWorkerHandler, times(1))
                .post(any(NotificationManagerService.PostNotificationRunnable.class));
        ArgumentCaptor<NotificationRecord> captor =
                ArgumentCaptor.forClass(NotificationRecord.class);
        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
                anyBoolean());
        assertThat(captor.getValue().getNotification().flags
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
    }
    @Test
    @Test
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    public void testCancelAllClearsLifetimeExtended() throws Exception {
    public void testCancelAllClearsLifetimeExtended() throws Exception {
@@ -6419,12 +6454,31 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    public void testStats_DirectReplyLifetimeExtendedPostsUpdate() throws Exception {
    public void testStats_DirectReplyLifetimeExtendedPostsUpdate() throws Exception {
        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        // Marks the notification as having already been lifetime extended and canceled.
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        r.setCanceledAfterLifetimeExtension(true);
        r.setPostSilently(true);
        mService.addNotification(r);
        mService.addNotification(r);
        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
        waitForIdle();
        waitForIdle();
        // At the moment prepareNotifyPostedLocked is called on the listeners,
        // verify that FLAG_ONLY_ALERT_ONCE and shouldPostSilently are set, regardless of initial
        // values.
        doAnswer(
                invocation -> {
                    int flags = ((NotificationRecord) invocation.getArgument(0))
                            .getSbn().getNotification().flags;
                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
                            .shouldPostSilently();
                    assertThat(shouldPostSilently).isTrue();
                    return null;
                }
        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
        // Checks that the record gets marked as a direct reply having occurred.
        assertThat(mService.getNotificationRecord(r.getKey()).getStats().hasDirectReplied())
        assertThat(mService.getNotificationRecord(r.getKey()).getStats().hasDirectReplied())
                .isTrue();
                .isTrue();
        // Checks that a post update is sent.
        // Checks that a post update is sent.
@@ -6437,9 +6491,65 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        assertThat(captor.getValue().getNotification().flags
        assertThat(captor.getValue().getNotification().flags
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
        // FLAG_ONLY_ALERT_ONCE was not present on the original notification, so it's not here.
        assertThat(captor.getValue().getNotification().flags
        assertThat(captor.getValue().getNotification().flags
                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
                & FLAG_ONLY_ALERT_ONCE).isEqualTo(0);
        assertThat(captor.getValue().shouldPostSilently()).isTrue();
        assertThat(captor.getValue().shouldPostSilently()).isTrue();
        assertThat(captor.getValue().isCanceledAfterLifetimeExtension()).isTrue();
    }
    @Test
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    public void testStats_DirectReplyLifetimeExtendedPostsUpdate_RestorePostSilently()
            throws Exception {
        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        // Marks the notification as having already been lifetime extended and canceled.
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        r.setPostSilently(false);
        mService.addNotification(r);
        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
        waitForIdle();
        // Checks that a post update is sent with shouldPostSilently set to true.
        doAnswer(
                invocation -> {
                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
                            .shouldPostSilently();
                    assertThat(shouldPostSilently).isTrue();
                    return null;
                }
        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
        // Checks that shouldPostSilently is restored to its false state afterward.
        assertThat(mService.getNotificationRecord(r.getKey()).shouldPostSilently()).isFalse();
    }
    @Test
    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
    public void testStats_DirectReplyLifetimeExtendedPostsUpdate_RestoreOnlyAlertOnceFlag()
            throws Exception {
        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        // Marks the notification as having already been lifetime extended and canceled.
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        mService.addNotification(r);
        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
        waitForIdle();
        // Checks that a post update is sent with FLAG_ONLY_ALERT_ONCE set to true.
        doAnswer(
                invocation -> {
                    int flags = ((NotificationRecord) invocation.getArgument(0))
                            .getSbn().getNotification().flags;
                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
                    return null;
                }
        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
        // Checks that the flag is removed afterward.
        assertThat(mService.getNotificationRecord(r.getKey()).getSbn().getNotification().flags
                & FLAG_ONLY_ALERT_ONCE).isEqualTo(0);
    }
    }
    @Test
    @Test
@@ -6476,6 +6586,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
                anyBoolean());
                anyBoolean());
        assertThat(captor.getValue().getNotification().flags
        assertThat(captor.getValue().getNotification().flags
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
        assertThat(captor.getValue().isCanceledAfterLifetimeExtension()).isFalse();
        assertThat(captor.getValue()
        assertThat(captor.getValue()
                .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                .isEqualTo("new title");
                .isEqualTo("new title");
@@ -9143,11 +9254,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        final int replyIndex = 2;
        final int replyIndex = 2;
        final String reply = "Hello";
        final String reply = "Hello";
        final boolean modifiedBeforeSending = true;
        final boolean modifiedBeforeSending = true;
        final boolean generatedByAssistant = true;
        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
        r.setSuggestionsGeneratedByAssistant(generatedByAssistant);
        r.getSbn().getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
        r.setSuggestionsGeneratedByAssistant(true);
        r.setCanceledAfterLifetimeExtension(true);
        r.setPostSilently(true);
        mService.addNotification(r);
        mService.addNotification(r);
        mService.mNotificationDelegate.onNotificationSmartReplySent(
        mService.mNotificationDelegate.onNotificationSmartReplySent(
@@ -9155,6 +9268,21 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
                modifiedBeforeSending);
                modifiedBeforeSending);
        waitForIdle();
        waitForIdle();
        // At the moment prepareNotifyPostedLocked is called on the listeners,
        // verify that FLAG_ONLY_ALERT_ONCE and shouldPostSilently are set, regardless of initial
        // values.
        doAnswer(
                invocation -> {
                    int flags = ((NotificationRecord) invocation.getArgument(0))
                            .getSbn().getNotification().flags;
                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
                            .shouldPostSilently();
                    assertThat(shouldPostSilently).isTrue();
                    return null;
                }
        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
        // Checks that a post update is sent.
        // Checks that a post update is sent.
        verify(mWorkerHandler, times(1))
        verify(mWorkerHandler, times(1))
                .post(any(NotificationManagerService.PostNotificationRunnable.class));
                .post(any(NotificationManagerService.PostNotificationRunnable.class));
@@ -9165,8 +9293,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        assertThat(captor.getValue().getNotification().flags
        assertThat(captor.getValue().getNotification().flags
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
        // Flag was present before, so it's set afterward
        assertThat(captor.getValue().getNotification().flags
        assertThat(captor.getValue().getNotification().flags
                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
        // Should post silently was set before, so it's set afterward.
        assertThat(captor.getValue().shouldPostSilently()).isTrue();
        assertThat(captor.getValue().shouldPostSilently()).isTrue();
    }
    }