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

Commit 21ba4bf2 authored by Julia Tuttle's avatar Julia Tuttle
Browse files

Suppress HUNs for notifications with old whens

Right now, if a user turns their device (or work profile) on after it's
been off for a while, they can get flooded with visual interruptions
(like heads-up notifications) from 'stale' notifications -- recently
posted, but pertaining to something that happened over a day ago.

This change will, when the included feature flag is enabled, suppress
visual interruption for notifications with a 'when' field set more than
24 hours in the past.

Bug: 240552967
Bug: 253055301
Test: atest StatusBarNotificationPresenterTest
Change-Id: I6e8b29cd9865370fb5ad34d4b74e0f8a40e63229
parent 05857f60
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -44,4 +44,8 @@ class NotifPipelineFlags @Inject constructor(
    val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy {
        featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
    }

    val isNoHunForOldWhenEnabled: Boolean by lazy {
        featureFlags.isEnabled(Flags.NO_HUN_FOR_OLD_WHEN)
    }
}
+30 −0
Original line number Diff line number Diff line
@@ -106,6 +106,36 @@ class NotificationInterruptLogger @Inject constructor(
        })
    }

    fun logNoHeadsUpOldWhen(
        entry: NotificationEntry,
        notifWhen: Long,
        notifAge: Long
    ) {
        buffer.log(TAG, DEBUG, {
            str1 = entry.logKey
            long1 = notifWhen
            long2 = notifAge
        }, {
            "No heads up: old when $long1 (age=$long2 ms): $str1"
        })
    }

    fun logMaybeHeadsUpDespiteOldWhen(
        entry: NotificationEntry,
        notifWhen: Long,
        notifAge: Long,
        reason: String
    ) {
        buffer.log(TAG, DEBUG, {
            str1 = entry.logKey
            str2 = reason
            long1 = notifWhen
            long2 = notifAge
        }, {
            "Maybe heads up: old when $long1 (age=$long2 ms) but $str2: $str1"
        })
    }

    fun logNoHeadsUpSuppressedBy(
        entry: NotificationEntry,
        suppressor: NotificationInterruptSuppressor
+56 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.database.ContentObserver;
@@ -82,7 +83,10 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter
        FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235),

        @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard")
        FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236);
        FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236),

        @UiEvent(doc = "HUN suppressed for old when")
        HUN_SUPPRESSED_OLD_WHEN(1237);

        private final int mId;

@@ -346,6 +350,10 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter
            return false;
        }

        if (shouldSuppressHeadsUpWhenAwakeForOldWhen(entry, log)) {
            return false;
        }

        for (int i = 0; i < mSuppressors.size(); i++) {
            if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) {
                if (log) mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i));
@@ -470,4 +478,51 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter
    private boolean isSnoozedPackage(StatusBarNotification sbn) {
        return mHeadsUpManager.isSnoozed(sbn.getPackageName());
    }

    private boolean shouldSuppressHeadsUpWhenAwakeForOldWhen(NotificationEntry entry, boolean log) {
        if (!mFlags.isNoHunForOldWhenEnabled()) {
            return false;
        }

        final Notification notification = entry.getSbn().getNotification();
        if (notification == null) {
            return false;
        }

        final long when = notification.when;
        final long now = System.currentTimeMillis();
        final long age = now - when;

        if (age < MAX_HUN_WHEN_AGE_MS) {
            return false;
        }

        if (when <= 0) {
            // Some notifications (including many system notifications) are posted with the "when"
            // field set to 0. Nothing in the Javadocs for Notification mentions a special meaning
            // for a "when" of 0, but Android didn't even exist at the dawn of the Unix epoch.
            // Therefore, assume that these notifications effectively don't have a "when" value,
            // and don't suppress HUNs.
            if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "when <= 0");
            return false;
        }

        if (notification.fullScreenIntent != null) {
            if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "full-screen intent");
            return false;
        }

        if (notification.isForegroundService()) {
            if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "foreground service");
            return false;
        }

        if (log) mLogger.logNoHeadsUpOldWhen(entry, when, age);
        final int uid = entry.getSbn().getUid();
        final String packageName = entry.getSbn().getPackageName();
        mUiEventLogger.log(NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN, uid, packageName);
        return true;
    }

    public static final long MAX_HUN_WHEN_AGE_MS = 24 * 60 * 60 * 1000;
}
+134 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.interruption;


import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.Notification.GROUP_ALERT_SUMMARY;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
@@ -33,6 +34,8 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -390,6 +393,127 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase {
        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
    }

    private long makeWhenHoursAgo(long hoursAgo) {
        return System.currentTimeMillis() - (1000 * 60 * 60 * hoursAgo);
    }

    @Test
    public void testShouldHeadsUp_oldWhen_flagDisabled() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(false);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = makeWhenHoursAgo(25);

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any());
    }

    @Test
    public void testShouldHeadsUp_oldWhen_whenNow() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any());
    }

    @Test
    public void testShouldHeadsUp_oldWhen_whenRecent() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = makeWhenHoursAgo(13);

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any());
    }

    @Test
    public void testShouldHeadsUp_oldWhen_whenZero() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = 0L;

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(0L), anyLong(),
                eq("when <= 0"));
    }

    @Test
    public void testShouldHeadsUp_oldWhen_whenNegative() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = -1L;

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(-1L), anyLong(),
                eq("when <= 0"));
    }

    @Test
    public void testShouldHeadsUp_oldWhen_hasFullScreenIntent() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);
        long when = makeWhenHoursAgo(25);

        NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silent= */ false);
        entry.getSbn().getNotification().when = when;

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(when), anyLong(),
                eq("full-screen intent"));
    }

    @Test
    public void testShouldHeadsUp_oldWhen_isForegroundService() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);
        long when = makeWhenHoursAgo(25);

        NotificationEntry entry = createFgsNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = when;

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();

        verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong());
        verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(when), anyLong(),
                eq("foreground service"));
    }

    @Test
    public void testShouldNotHeadsUp_oldWhen() throws Exception {
        ensureStateForHeadsUpWhenAwake();
        when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true);
        long when = makeWhenHoursAgo(25);

        NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
        entry.getSbn().getNotification().when = when;

        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();

        verify(mLogger).logNoHeadsUpOldWhen(eq(entry), eq(when), anyLong());
        verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any());
    }

    @Test
    public void testShouldNotFullScreen_notPendingIntent_withStrictFlag() throws Exception {
        when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
@@ -763,6 +887,16 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase {
        return createNotification(importance, n);
    }

    private NotificationEntry createFgsNotification(int importance) {
        Notification n = new Notification.Builder(getContext(), "a")
                .setContentTitle("title")
                .setContentText("content text")
                .setFlag(FLAG_FOREGROUND_SERVICE, true)
                .build();

        return createNotification(importance, n);
    }

    private final NotificationInterruptSuppressor
            mSuppressAwakeHeadsUp =
            new NotificationInterruptSuppressor() {