Loading core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +5 −1 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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"); Loading services/core/java/com/android/server/notification/NotificationAttentionHelper.java +107 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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: Loading @@ -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; Loading Loading @@ -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; } Loading @@ -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), Loading @@ -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( Loading @@ -225,6 +241,11 @@ public final class NotificationAttentionHelper { } } @VisibleForTesting PolitenessStrategy getPolitenessStrategy() { return mStrategy; } public void onSystemReady() { mSystemReady = true; Loading Loading @@ -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( Loading Loading @@ -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; Loading @@ -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<>(); Loading Loading @@ -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. Loading Loading @@ -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; } Loading @@ -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); Loading Loading @@ -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 ============================= Loading Loading @@ -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()); } } } } }; Loading services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +130 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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(); } } // Loading Loading @@ -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(); Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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 Loading Loading @@ -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; Loading Loading
core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +5 −1 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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"); Loading
services/core/java/com/android/server/notification/NotificationAttentionHelper.java +107 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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: Loading @@ -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; Loading Loading @@ -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; } Loading @@ -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), Loading @@ -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( Loading @@ -225,6 +241,11 @@ public final class NotificationAttentionHelper { } } @VisibleForTesting PolitenessStrategy getPolitenessStrategy() { return mStrategy; } public void onSystemReady() { mSystemReady = true; Loading Loading @@ -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( Loading Loading @@ -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; Loading @@ -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<>(); Loading Loading @@ -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. Loading Loading @@ -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; } Loading @@ -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); Loading Loading @@ -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 ============================= Loading Loading @@ -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()); } } } } }; Loading
services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +130 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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(); } } // Loading Loading @@ -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(); Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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 Loading Loading @@ -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; Loading