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

Commit 75a42e1e authored by Valentin Iftime's avatar Valentin Iftime
Browse files

Add notification avalanche triggers

 Update the cross-app cooldown strategy to only be active for 120 seconds
 after an 'avalanche trigger' was detected.
 Avalanche triggers are:
  - airplane mode off
  - boot completed
  - user switched
  - work profile enabled

 Adjust cooldown timers to: t1=60 seconds and t2=10 seconds.

Test: atest NotificationAttentionHelperTest
Bug: 270456865
Change-Id: I2ff9e4600df1d9d88896869ac20f39d4513b141c
parent 01642009
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ public class SystemUiSystemPropertiesFlags {
                "persist.debug.sysui.notification.notif_cooldown_t1", 60000);
        /** Value used by polite notif. feature */
        public static final Flag NOTIF_COOLDOWN_T2 = devFlag(
                "persist.debug.sysui.notification.notif_cooldown_t2", 5000);
                "persist.debug.sysui.notification.notif_cooldown_t2", 10000);
        /** Value used by polite notif. feature */
        public static final Flag NOTIF_VOLUME1 = devFlag(
                "persist.debug.sysui.notification.notif_volume1", 30);
@@ -81,6 +81,10 @@ public class SystemUiSystemPropertiesFlags {
        public static final Flag NOTIF_COOLDOWN_COUNTER_RESET = devFlag(
                "persist.debug.sysui.notification.notif_cooldown_counter_reset", 10);

        /** Value used by polite notif. feature */
        public static final Flag NOTIF_AVALANCHE_TIMEOUT = devFlag(
                "persist.debug.sysui.notification.notif_avalanche_timeout", 120_000);

        /** b/303716154: For debugging only: use short bitmap duration. */
        public static final Flag DEBUG_SHORT_BITMAP_DURATION = devFlag(
                "persist.sysui.notification.debug_short_bitmap_duration");
+107 −27
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -71,7 +72,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.server.EventLogTags;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
import com.android.server.notification.Flags;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -81,6 +81,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * NotificationManagerService helper for handling notification attention effects:
@@ -100,6 +101,20 @@ public final class NotificationAttentionHelper {
    private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1;
    private static final int DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED = 0;

    @VisibleForTesting
    static final Set<String> NOTIFICATION_AVALANCHE_TRIGGER_INTENTS = Set.of(
            Intent.ACTION_AIRPLANE_MODE_CHANGED,
            Intent.ACTION_BOOT_COMPLETED,
            Intent.ACTION_USER_SWITCHED,
            Intent.ACTION_MANAGED_PROFILE_AVAILABLE
    );

    @VisibleForTesting
    static final Map<String, Pair<String, Boolean>> NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS = Map.of(
            Intent.ACTION_AIRPLANE_MODE_CHANGED, new Pair<>("state", false),
            Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false)
    );

    private final Context mContext;
    private final PackageManager mPackageManager;
    private final TelephonyManager mTelephonyManager;
@@ -191,7 +206,7 @@ public final class NotificationAttentionHelper {
        mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume);

        if (Flags.politeNotifications()) {
            mStrategy = getPolitenessStrategy();
            mStrategy = createPolitenessStrategy();
        } else {
            mStrategy = null;
        }
@@ -200,7 +215,7 @@ public final class NotificationAttentionHelper {
        loadUserSettings();
    }

    private PolitenessStrategy getPolitenessStrategy() {
    private PolitenessStrategy createPolitenessStrategy() {
        if (Flags.crossAppPoliteNotifications()) {
            PolitenessStrategy appStrategy = new StrategyPerApp(
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
@@ -209,11 +224,12 @@ public final class NotificationAttentionHelper {
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET));

            return new StrategyGlobal(
            return new StrategyAvalanche(
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2),
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT),
                    appStrategy);
        } else {
            return new StrategyPerApp(
@@ -225,6 +241,11 @@ public final class NotificationAttentionHelper {
        }
    }

    @VisibleForTesting
    PolitenessStrategy getPolitenessStrategy() {
        return mStrategy;
    }

    public void onSystemReady() {
        mSystemReady = true;

@@ -259,6 +280,11 @@ public final class NotificationAttentionHelper {
        filter.addAction(Intent.ACTION_USER_REMOVED);
        filter.addAction(Intent.ACTION_USER_SWITCHED);
        filter.addAction(Intent.ACTION_USER_UNLOCKED);
        if (Flags.crossAppPoliteNotifications()) {
            for (String avalancheIntent : NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
                filter.addAction(avalancheIntent);
            }
        }
        mContext.registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);

        mContext.getContentResolver().registerContentObserver(
@@ -1052,7 +1078,8 @@ public final class NotificationAttentionHelper {
        }
    }

    abstract private static class PolitenessStrategy {
    @VisibleForTesting
    abstract static class PolitenessStrategy {
        static final int POLITE_STATE_DEFAULT = 0;
        static final int POLITE_STATE_POLITE = 1;
        static final int POLITE_STATE_MUTED = 2;
@@ -1079,6 +1106,8 @@ public final class NotificationAttentionHelper {
        protected boolean mApplyPerPackage;
        protected final Map<String, Long> mLastUpdatedTimestampByPackage;

        protected boolean mIsActive = true;

        public PolitenessStrategy(int timeoutPolite, int timeoutMuted, int volumePolite,
                int volumeMuted) {
            mVolumeStates = new HashMap<>();
@@ -1218,6 +1247,10 @@ public final class NotificationAttentionHelper {
            }
            return nextState;
        }

        boolean isActive() {
            return mIsActive;
        }
    }

    // TODO b/270456865: Only one of the two strategies will be released.
@@ -1289,27 +1322,33 @@ public final class NotificationAttentionHelper {
    }

    /**
     * Global (cross-app) strategy.
     * Avalanche (cross-app) strategy.
     */
    private static class StrategyGlobal extends PolitenessStrategy {
    private static class StrategyAvalanche extends PolitenessStrategy {
        private static final String COMMON_KEY = "cross_app_common_key";

        private final PolitenessStrategy mAppStrategy;
        private long mLastNotificationTimestamp = 0;

        public StrategyGlobal(int timeoutPolite, int timeoutMuted, int volumePolite,
                int volumeMuted, PolitenessStrategy appStrategy) {
        private final int mTimeoutAvalanche;
        private long mLastAvalancheTriggerTimestamp = 0;

        StrategyAvalanche(int timeoutPolite, int timeoutMuted, int volumePolite,
                    int volumeMuted, int timeoutAvalanche, PolitenessStrategy appStrategy) {
            super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted);

            mTimeoutAvalanche = timeoutAvalanche;
            mAppStrategy = appStrategy;

            if (DEBUG) {
                Log.i(TAG, "StrategyGlobal: " + timeoutPolite + " " + timeoutMuted);
                Log.i(TAG, "StrategyAvalanche: " + timeoutPolite + " " + timeoutMuted + " "
                        + timeoutAvalanche);
            }
        }

        @Override
        void onNotificationPosted(NotificationRecord record) {
            if (isAvalancheActive()) {
                if (shouldIgnoreNotification(record)) {
                    return;
                }
@@ -1322,22 +1361,21 @@ public final class NotificationAttentionHelper {
                @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);

                if (DEBUG) {
                Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif
                    Log.i(TAG,
                            "StrategyAvalanche onNotificationPosted time delta: "
                            + timeSinceLastNotif
                            + " vol state: " + nextState + " key: " + key);
                }

                mVolumeStates.put(key, nextState);
            }

            mAppStrategy.onNotificationPosted(record);
        }

        @Override
        public float getSoundVolume(final NotificationRecord record) {
            final @PolitenessState int globalVolState = getPolitenessState(record);
            final @PolitenessState int appVolState = mAppStrategy.getPolitenessState(record);

            // Prioritize the most polite outcome
            if (globalVolState > appVolState) {
            if (isAvalancheActive()) {
                return super.getSoundVolume(record);
            } else {
                return mAppStrategy.getSoundVolume(record);
@@ -1382,6 +1420,24 @@ public final class NotificationAttentionHelper {
            super.setApplyCooldownPerPackage(applyPerPackage);
            mAppStrategy.setApplyCooldownPerPackage(applyPerPackage);
        }

        boolean isAvalancheActive() {
            mIsActive = (System.currentTimeMillis() - mLastAvalancheTriggerTimestamp
                    < mTimeoutAvalanche);
            if (DEBUG) {
                Log.i(TAG, "StrategyAvalanche: active " + mIsActive);
            }
            return mIsActive;
        }

        @Override
        boolean isActive() {
            return isAvalancheActive();
        }

        void setTriggerTimeMs(long timestamp) {
            mLastAvalancheTriggerTimestamp = timestamp;
        }
    }

    //======================  Observers  =============================
@@ -1415,6 +1471,30 @@ public final class NotificationAttentionHelper {
                        || action.equals(Intent.ACTION_USER_UNLOCKED)) {
                loadUserSettings();
            }

            if (Flags.crossAppPoliteNotifications()) {
                if (NOTIFICATION_AVALANCHE_TRIGGER_INTENTS.contains(action)) {
                    boolean enableAvalancheStrategy = true;
                    // Some actions must also match extras, ie. airplane mode => disabled
                    Pair<String, Boolean> expectedExtras =
                            NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS.get(action);
                    if (expectedExtras != null) {
                        enableAvalancheStrategy =
                                intent.getBooleanExtra(expectedExtras.first, false)
                                == expectedExtras.second;
                    }

                    if (DEBUG) {
                        Log.i(TAG, "Avalanche trigger intent received: " + action
                                + ". Enabling avalanche strategy: " + enableAvalancheStrategy);
                    }

                    if (enableAvalancheStrategy && mStrategy instanceof StrategyAvalanche) {
                        ((StrategyAvalanche) mStrategy)
                                .setTriggerTimeMs(System.currentTimeMillis());
                    }
                }
            }
        }
    };

+130 −7
Original line number Diff line number Diff line
@@ -27,12 +27,13 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
import static android.media.AudioAttributes.USAGE_NOTIFICATION;
import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.any;
@@ -43,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -59,7 +61,10 @@ import android.app.Notification.Builder;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.Color;
@@ -80,6 +85,7 @@ import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Pair;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManager;
@@ -100,6 +106,7 @@ import com.android.server.pm.PackageManagerService;
import java.util.List;
import java.util.Objects;

import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -132,6 +139,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
    KeyguardManager mKeyguardManager;
    @Mock
    private UserManager mUserManager;
    @Mock
    private PackageManager mPackageManager;
    NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
    private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
        1 << 30);
@@ -171,11 +180,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
    private static final int CUSTOM_LIGHT_OFF = 10000;
    private static final int MAX_VIBRATION_DELAY = 1000;
    private static final float DEFAULT_VOLUME = 1.0f;
    private BroadcastReceiver mAvalancheBroadcastReceiver;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        getContext().addMockSystemService(Vibrator.class, mVibrator);
        getContext().addMockSystemService(PackageManager.class, mPackageManager);
        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)).thenReturn(false);

        when(mAudioManager.isAudioFocusExclusive()).thenReturn(false);
        when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer);
@@ -214,8 +226,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {

    private void initAttentionHelper(TestableFlagResolver flagResolver) {
        mAttentionHelper = new NotificationAttentionHelper(getContext(), mock(LightsManager.class),
            mAccessibilityManager, getContext().getPackageManager(), mUserManager, mUsageStats,
                mAccessibilityManager, mPackageManager, mUserManager, mUsageStats,
                mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver);
        mAttentionHelper.onSystemReady();
        mAttentionHelper.setVibratorHelper(spy(new VibratorHelper(getContext())));
        mAttentionHelper.setAudioManager(mAudioManager);
        mAttentionHelper.setSystemReady(true);
@@ -226,6 +239,29 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
        mAttentionHelper.setScreenOn(false);
        mAttentionHelper.setInCallStateOffHook(false);
        mAttentionHelper.mNotificationPulseEnabled = true;

        if (Flags.crossAppPoliteNotifications()) {
            // Capture BroadcastReceiver for avalanche triggers
            ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
                    ArgumentCaptor.forClass(BroadcastReceiver.class);
            ArgumentCaptor<IntentFilter> intentFilterCaptor =
                    ArgumentCaptor.forClass(IntentFilter.class);
            verify(getContext(), atLeastOnce()).registerReceiverAsUser(
                    broadcastReceiverCaptor.capture(),
                    any(), intentFilterCaptor.capture(), any(), any());
            List<BroadcastReceiver> broadcastReceivers = broadcastReceiverCaptor.getAllValues();
            List<IntentFilter> intentFilters = intentFilterCaptor.getAllValues();

            assertThat(broadcastReceivers.size()).isAtLeast(1);
            assertThat(intentFilters.size()).isAtLeast(1);
            for (int i = 0; i < intentFilters.size(); i++) {
                final IntentFilter filter = intentFilters.get(i);
                if (filter.hasAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
                    mAvalancheBroadcastReceiver = broadcastReceivers.get(i);
                }
            }
            assertThat(mAvalancheBroadcastReceiver).isNotNull();
        }
    }

    //
@@ -2040,7 +2076,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
    }

    @Test
    public void testBeepVolume_politeNotif_GlobalStrategy() throws Exception {
    public void testBeepVolume_politeNotif_AvalancheStrategy() throws Exception {
        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
        TestableFlagResolver flagResolver = new TestableFlagResolver();
@@ -2048,6 +2084,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
        initAttentionHelper(flagResolver);

        // Trigger avalanche trigger intent
        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        intent.putExtra("state", false);
        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);

        NotificationRecord r = getBeepyNotification();

        // set up internal state
@@ -2078,7 +2119,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
    }

    @Test
    public void testBeepVolume_politeNotif_GlobalStrategy_ChannelHasUserSound() throws Exception {
    public void testBeepVolume_politeNotif_AvalancheStrategy_ChannelHasUserSound()
            throws Exception {
        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
        TestableFlagResolver flagResolver = new TestableFlagResolver();
@@ -2086,6 +2128,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
        initAttentionHelper(flagResolver);

        // Trigger avalanche trigger intent
        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        intent.putExtra("state", false);
        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);

        NotificationRecord r = getBeepyNotification();

        // set up internal state
@@ -2364,6 +2411,82 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase {
        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
    }

    @Test
    public void testAvalancheStrategyTriggers() throws Exception {
        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
        TestableFlagResolver flagResolver = new TestableFlagResolver();
        final int avalancheTimeoutMs = 100;
        flagResolver.setFlagOverride(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT, avalancheTimeoutMs);
        initAttentionHelper(flagResolver);

        // Trigger avalanche trigger intents
        for (String intentAction
                : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
            // Set the action and extras to trigger the avalanche strategy
            Intent intent = new Intent(intentAction);
            Pair<String, Boolean> extras =
                    NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS
                        .get(intentAction);
            if (extras != null) {
                intent.putExtra(extras.first, extras.second);
            }
            mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
            assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isTrue();

            // Wait for avalanche timeout
            Thread.sleep(avalancheTimeoutMs + 1);

            // Check that avalanche strategy is inactive
            assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
        }
    }

    @Test
    public void testAvalancheStrategyTriggers_disabledExtras() throws Exception {
        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
        TestableFlagResolver flagResolver = new TestableFlagResolver();
        initAttentionHelper(flagResolver);

        for (String intentAction
                : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
            Intent intent = new Intent(intentAction);
            Pair<String, Boolean> extras =
                    NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS
                        .get(intentAction);
            // Test only for intents with extras
            if (extras != null) {
                // Set the action extras to NOT trigger the avalanche strategy
                intent.putExtra(extras.first, !extras.second);
                mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
                // Check that avalanche strategy is inactive
                assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
            }
        }
    }

    @Test
    public void testAvalancheStrategyTriggers_nonAvalancheIntents() throws Exception {
        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
        TestableFlagResolver flagResolver = new TestableFlagResolver();
        initAttentionHelper(flagResolver);

        // Broadcast intents that are not avalanche triggers
        final Set<String> notAvalancheTriggerIntents = Set.of(
                Intent.ACTION_USER_ADDED,
                Intent.ACTION_SCREEN_ON,
                Intent.ACTION_POWER_CONNECTED
        );
        for (String intentAction : notAvalancheTriggerIntents) {
            Intent intent = new Intent(intentAction);
            mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
            // Check that avalanche strategy is inactive
            assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
        }
    }

    static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> {
        private final int mRepeatIndex;