diff options
3 files changed, 242 insertions, 35 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index eeea17bf39dd..90ca95a7fbab 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -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"); diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 85c4ffe6ac67..f852b8173f30 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -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,55 +1322,60 @@ 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 (shouldIgnoreNotification(record)) { - return; - } + if (isAvalancheActive()) { + if (shouldIgnoreNotification(record)) { + return; + } - long timeSinceLastNotif = + long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); - final String key = getChannelKey(record); - @PolitenessState final int currState = getPolitenessState(record); - @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); + final String key = getChannelKey(record); + @PolitenessState final int currState = getPolitenessState(record); + @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); - if (DEBUG) { - Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif - + " vol state: " + nextState + " key: " + key); - } + if (DEBUG) { + Log.i(TAG, + "StrategyAvalanche onNotificationPosted time delta: " + + timeSinceLastNotif + + " vol state: " + nextState + " key: " + key); + } - mVolumeStates.put(key, nextState); + 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()); + } + } + } } }; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index bfd2df2d2b7d..e75afccfdfdf 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -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, - mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver); + 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; |