Loading services/core/java/com/android/server/notification/NotificationAttentionHelper.java +110 −13 Original line number Diff line number Diff line Loading @@ -514,12 +514,16 @@ public final class NotificationAttentionHelper { EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, getPolitenessState(record)); } record.setAudiblyAlerted(buzz || beep); if (Flags.politeNotifications()) { // Update last alert time if (buzz || beep) { mStrategy.setLastNotificationUpdateTimeMs(record, System.currentTimeMillis()); } record.setAudiblyAlerted((buzz || beep) && getPolitenessState(record) != PolitenessStrategy.POLITE_STATE_MUTED); } else { record.setAudiblyAlerted(buzz || beep); } return buzzBeepBlinkLoggingCode; } Loading Loading @@ -678,7 +682,7 @@ public final class NotificationAttentionHelper { // The user can choose to apply cooldown for all apps/conversations only from the // Settings app if (!mNotificationCooldownApplyToAll && record.getChannel().getConversationId() == null) { if (!mNotificationCooldownApplyToAll && !record.isConversation()) { return false; } Loading Loading @@ -1203,7 +1207,7 @@ public final class NotificationAttentionHelper { setLastNotificationUpdateTimeMs(record, 0); } public final @PolitenessState int getPolitenessState(final NotificationRecord record) { public @PolitenessState int getPolitenessState(final NotificationRecord record) { return mVolumeStates.getOrDefault(getChannelKey(record), POLITE_STATE_DEFAULT); } Loading Loading @@ -1364,7 +1368,12 @@ public final class NotificationAttentionHelper { final String key = getChannelKey(record); @PolitenessState final int currState = getPolitenessState(record); @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); @PolitenessState int nextState; if (Flags.politeNotificationsAttnUpdate()) { nextState = getNextState(currState, timeSinceLastNotif, record); } else { nextState = getNextState(currState, timeSinceLastNotif); } if (DEBUG) { Log.i(TAG, Loading @@ -1379,6 +1388,26 @@ public final class NotificationAttentionHelper { mAppStrategy.onNotificationPosted(record); } @PolitenessState int getNextState(@PolitenessState final int currState, final long timeSinceLastNotif, final NotificationRecord record) { // Mute all except priority conversations if (!isAvalancheExempted(record)) { return POLITE_STATE_MUTED; } if (isAvalancheExemptedFullVolume(record)) { return POLITE_STATE_DEFAULT; } return getNextState(currState, timeSinceLastNotif); } public @PolitenessState int getPolitenessState(final NotificationRecord record) { if (isAvalancheActive()) { return super.getPolitenessState(record); } else { return mAppStrategy.getPolitenessState(record); } } @Override public float getSoundVolume(final NotificationRecord record) { if (isAvalancheActive()) { Loading @@ -1396,6 +1425,16 @@ public final class NotificationAttentionHelper { @Override String getChannelKey(final NotificationRecord record) { if (isAvalancheActive()) { if (Flags.politeNotificationsAttnUpdate()) { // Treat high importance conversations independently if (isAvalancheExempted(record)) { return super.getChannelKey(record); } else { // Use one global key per user return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY; } } else { // If the user explicitly changed the channel notification sound: // handle as a separate channel if (record.getChannel().hasUserSetSound()) { Loading @@ -1405,6 +1444,10 @@ public final class NotificationAttentionHelper { return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY; } } } else { return mAppStrategy.getChannelKey(record); } } @Override public void setLastNotificationUpdateTimeMs(NotificationRecord record, Loading @@ -1415,12 +1458,21 @@ public final class NotificationAttentionHelper { } long getLastNotificationUpdateTimeMs(final NotificationRecord record) { if (Flags.politeNotificationsAttnUpdate()) { // Mute all except priority conversations if (isAvalancheExempted(record)) { return super.getLastNotificationUpdateTimeMs(record); } else { return mLastNotificationTimestamp; } } else { if (record.getChannel().hasUserSetSound()) { return super.getLastNotificationUpdateTimeMs(record); } else { return mLastNotificationTimestamp; } } } @Override void setApplyCooldownPerPackage(boolean applyPerPackage) { Loading @@ -1445,6 +1497,51 @@ public final class NotificationAttentionHelper { void setTriggerTimeMs(long timestamp) { mLastAvalancheTriggerTimestamp = timestamp; } private boolean isAvalancheExemptedFullVolume(final NotificationRecord record) { // important conversation if (record.isConversation() && (record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT || record.getChannel().isImportantConversation())) { return true; } // call notification if (record.getNotification().isStyle(Notification.CallStyle.class)) { return true; } // alarm/reminder final String category = record.getNotification().category; if (Notification.CATEGORY_REMINDER.equals(category) || Notification.CATEGORY_EVENT.equals(category)) { return true; } return false; } private boolean isAvalancheExempted(final NotificationRecord record) { if (isAvalancheExemptedFullVolume(record)) { return true; } // recent conversation if (record.isConversation() && record.getNotification().when > mLastAvalancheTriggerTimestamp) { return true; } if (record.getNotification().fullScreenIntent != null) { return true; } if (record.getNotification().isColorized()) { return true; } return false; } } //====================== Observers ============================= Loading services/core/java/com/android/server/notification/flags.aconfig +8 −1 Original line number Diff line number Diff line Loading @@ -79,3 +79,10 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "polite_notifications_attn_update" namespace: "systemui" description: "This flag controls the polite notification attention behavior updates as per UXR feedback" bug: "270456865" } services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +159 −8 Original line number Diff line number Diff line Loading @@ -65,6 +65,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.Color; import android.graphics.drawable.Icon; Loading Loading @@ -212,7 +213,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // TODO (b/291907312): remove feature flag // Disable feature flags by default. Tests should enable as needed. mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED); Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED, Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger, mNotificationInstanceIdSequence)); Loading Loading @@ -410,8 +413,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle) { return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, userHandle, mPkg); defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false, userHandle, mPkg); } private NotificationRecord getNotificationRecord(int id, Loading @@ -419,6 +422,16 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle, String packageName) { return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false, userHandle, packageName); } private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, boolean isConversation, UserHandle userHandle, String packageName) { final Builder builder = new Builder(getContext()) .setContentTitle("foo") Loading @@ -426,6 +439,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { .setPriority(Notification.PRIORITY_HIGH) .setOnlyAlertOnce(once); if (isConversation) { builder.setStyle(new Notification.MessagingStyle("test user")); } int defaults = 0; if (noisy) { if (defaultSound) { Loading Loading @@ -485,6 +502,19 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { return r; } private NotificationRecord getConversationNotificationRecord(int id, boolean insistent, boolean once, boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle, String packageName, String shortcutId) { NotificationRecord r = getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, true, userHandle, packageName); ShortcutInfo.Builder sb = new ShortcutInfo.Builder(getContext()); r.setShortcutInfo(sb.setId(shortcutId).build()); return r; } // // Convenience functions for interacting with mocks // Loading Loading @@ -2057,12 +2087,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // update should beep at 50% volume r.isUpdate = true; mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); Loading @@ -2070,7 +2102,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @Test Loading @@ -2091,6 +2123,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications Loading @@ -2101,6 +2134,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 50% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, Loading @@ -2113,7 +2147,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2157,6 +2191,117 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_AvalancheStrategy_AttnUpdate() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); TestableFlagResolver flagResolver = new TestableFlagResolver(); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); 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 mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); // update should beep at 0% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.0f); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg"); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_AvalancheStrategy_exempt_AttnUpdate() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); TestableFlagResolver flagResolver = new TestableFlagResolver(); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); 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(); r.getNotification().category = Notification.CATEGORY_EVENT; // Should beep at 100% volume mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); verifyBeepVolume(1.0f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg", "shortcut"); // Should beep at 100% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // Use different package for next notifications mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT); NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg"); r3.getNotification().category = Notification.CATEGORY_REMINDER; // Should beep at 100% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // Same package as r3 for next notifications NotificationRecord r4 = getConversationNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg", "shortcut"); // 2nd update should beep at 50% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r4, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r4.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_applyPerApp() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); Loading @@ -2181,11 +2326,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 50% volume NotificationRecord r2 = getBeepyNotification(); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.5f); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.0f); // Use different package for next notifications Loading @@ -2199,7 +2346,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(1.0f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2227,11 +2374,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 100% volume NotificationRecord r2 = getBeepyNotification(); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // 2nd update should beep at 50% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.5f); // Use different package for next notifications Loading @@ -2246,7 +2395,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(1.0f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2355,12 +2504,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // update should beep at 50% volume r.isUpdate = true; mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); Loading @@ -2368,7 +2519,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @Test Loading Loading
services/core/java/com/android/server/notification/NotificationAttentionHelper.java +110 −13 Original line number Diff line number Diff line Loading @@ -514,12 +514,16 @@ public final class NotificationAttentionHelper { EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, getPolitenessState(record)); } record.setAudiblyAlerted(buzz || beep); if (Flags.politeNotifications()) { // Update last alert time if (buzz || beep) { mStrategy.setLastNotificationUpdateTimeMs(record, System.currentTimeMillis()); } record.setAudiblyAlerted((buzz || beep) && getPolitenessState(record) != PolitenessStrategy.POLITE_STATE_MUTED); } else { record.setAudiblyAlerted(buzz || beep); } return buzzBeepBlinkLoggingCode; } Loading Loading @@ -678,7 +682,7 @@ public final class NotificationAttentionHelper { // The user can choose to apply cooldown for all apps/conversations only from the // Settings app if (!mNotificationCooldownApplyToAll && record.getChannel().getConversationId() == null) { if (!mNotificationCooldownApplyToAll && !record.isConversation()) { return false; } Loading Loading @@ -1203,7 +1207,7 @@ public final class NotificationAttentionHelper { setLastNotificationUpdateTimeMs(record, 0); } public final @PolitenessState int getPolitenessState(final NotificationRecord record) { public @PolitenessState int getPolitenessState(final NotificationRecord record) { return mVolumeStates.getOrDefault(getChannelKey(record), POLITE_STATE_DEFAULT); } Loading Loading @@ -1364,7 +1368,12 @@ public final class NotificationAttentionHelper { final String key = getChannelKey(record); @PolitenessState final int currState = getPolitenessState(record); @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); @PolitenessState int nextState; if (Flags.politeNotificationsAttnUpdate()) { nextState = getNextState(currState, timeSinceLastNotif, record); } else { nextState = getNextState(currState, timeSinceLastNotif); } if (DEBUG) { Log.i(TAG, Loading @@ -1379,6 +1388,26 @@ public final class NotificationAttentionHelper { mAppStrategy.onNotificationPosted(record); } @PolitenessState int getNextState(@PolitenessState final int currState, final long timeSinceLastNotif, final NotificationRecord record) { // Mute all except priority conversations if (!isAvalancheExempted(record)) { return POLITE_STATE_MUTED; } if (isAvalancheExemptedFullVolume(record)) { return POLITE_STATE_DEFAULT; } return getNextState(currState, timeSinceLastNotif); } public @PolitenessState int getPolitenessState(final NotificationRecord record) { if (isAvalancheActive()) { return super.getPolitenessState(record); } else { return mAppStrategy.getPolitenessState(record); } } @Override public float getSoundVolume(final NotificationRecord record) { if (isAvalancheActive()) { Loading @@ -1396,6 +1425,16 @@ public final class NotificationAttentionHelper { @Override String getChannelKey(final NotificationRecord record) { if (isAvalancheActive()) { if (Flags.politeNotificationsAttnUpdate()) { // Treat high importance conversations independently if (isAvalancheExempted(record)) { return super.getChannelKey(record); } else { // Use one global key per user return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY; } } else { // If the user explicitly changed the channel notification sound: // handle as a separate channel if (record.getChannel().hasUserSetSound()) { Loading @@ -1405,6 +1444,10 @@ public final class NotificationAttentionHelper { return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY; } } } else { return mAppStrategy.getChannelKey(record); } } @Override public void setLastNotificationUpdateTimeMs(NotificationRecord record, Loading @@ -1415,12 +1458,21 @@ public final class NotificationAttentionHelper { } long getLastNotificationUpdateTimeMs(final NotificationRecord record) { if (Flags.politeNotificationsAttnUpdate()) { // Mute all except priority conversations if (isAvalancheExempted(record)) { return super.getLastNotificationUpdateTimeMs(record); } else { return mLastNotificationTimestamp; } } else { if (record.getChannel().hasUserSetSound()) { return super.getLastNotificationUpdateTimeMs(record); } else { return mLastNotificationTimestamp; } } } @Override void setApplyCooldownPerPackage(boolean applyPerPackage) { Loading @@ -1445,6 +1497,51 @@ public final class NotificationAttentionHelper { void setTriggerTimeMs(long timestamp) { mLastAvalancheTriggerTimestamp = timestamp; } private boolean isAvalancheExemptedFullVolume(final NotificationRecord record) { // important conversation if (record.isConversation() && (record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT || record.getChannel().isImportantConversation())) { return true; } // call notification if (record.getNotification().isStyle(Notification.CallStyle.class)) { return true; } // alarm/reminder final String category = record.getNotification().category; if (Notification.CATEGORY_REMINDER.equals(category) || Notification.CATEGORY_EVENT.equals(category)) { return true; } return false; } private boolean isAvalancheExempted(final NotificationRecord record) { if (isAvalancheExemptedFullVolume(record)) { return true; } // recent conversation if (record.isConversation() && record.getNotification().when > mLastAvalancheTriggerTimestamp) { return true; } if (record.getNotification().fullScreenIntent != null) { return true; } if (record.getNotification().isColorized()) { return true; } return false; } } //====================== Observers ============================= Loading
services/core/java/com/android/server/notification/flags.aconfig +8 −1 Original line number Diff line number Diff line Loading @@ -79,3 +79,10 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "polite_notifications_attn_update" namespace: "systemui" description: "This flag controls the polite notification attention behavior updates as per UXR feedback" bug: "270456865" }
services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +159 −8 Original line number Diff line number Diff line Loading @@ -65,6 +65,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.Color; import android.graphics.drawable.Icon; Loading Loading @@ -212,7 +213,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // TODO (b/291907312): remove feature flag // Disable feature flags by default. Tests should enable as needed. mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED); Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED, Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger, mNotificationInstanceIdSequence)); Loading Loading @@ -410,8 +413,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle) { return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, userHandle, mPkg); defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false, userHandle, mPkg); } private NotificationRecord getNotificationRecord(int id, Loading @@ -419,6 +422,16 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle, String packageName) { return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false, userHandle, packageName); } private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, boolean isConversation, UserHandle userHandle, String packageName) { final Builder builder = new Builder(getContext()) .setContentTitle("foo") Loading @@ -426,6 +439,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { .setPriority(Notification.PRIORITY_HIGH) .setOnlyAlertOnce(once); if (isConversation) { builder.setStyle(new Notification.MessagingStyle("test user")); } int defaults = 0; if (noisy) { if (defaultSound) { Loading Loading @@ -485,6 +502,19 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { return r; } private NotificationRecord getConversationNotificationRecord(int id, boolean insistent, boolean once, boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, boolean isLeanback, UserHandle userHandle, String packageName, String shortcutId) { NotificationRecord r = getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, true, userHandle, packageName); ShortcutInfo.Builder sb = new ShortcutInfo.Builder(getContext()); r.setShortcutInfo(sb.setId(shortcutId).build()); return r; } // // Convenience functions for interacting with mocks // Loading Loading @@ -2057,12 +2087,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // update should beep at 50% volume r.isUpdate = true; mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); Loading @@ -2070,7 +2102,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @Test Loading @@ -2091,6 +2123,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications Loading @@ -2101,6 +2134,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 50% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, Loading @@ -2113,7 +2147,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2157,6 +2191,117 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_AvalancheStrategy_AttnUpdate() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); TestableFlagResolver flagResolver = new TestableFlagResolver(); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); 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 mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); assertEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); // update should beep at 0% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.0f); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg"); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_AvalancheStrategy_exempt_AttnUpdate() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); TestableFlagResolver flagResolver = new TestableFlagResolver(); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); 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(); r.getNotification().category = Notification.CATEGORY_EVENT; // Should beep at 100% volume mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); verifyBeepVolume(1.0f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // Use different package for next notifications NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg", "shortcut"); // Should beep at 100% volume mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // Use different package for next notifications mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT); NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg"); r3.getNotification().category = Notification.CATEGORY_REMINDER; // Should beep at 100% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // Same package as r3 for next notifications NotificationRecord r4 = getConversationNotificationRecord(mId, false /* insistent */, false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg", "shortcut"); // 2nd update should beep at 50% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r4, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r4.getLastAudiblyAlertedMs()); } @Test public void testBeepVolume_politeNotif_applyPerApp() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); Loading @@ -2181,11 +2326,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 50% volume NotificationRecord r2 = getBeepyNotification(); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.5f); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.0f); // Use different package for next notifications Loading @@ -2199,7 +2346,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(1.0f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2227,11 +2374,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // update should beep at 100% volume NotificationRecord r2 = getBeepyNotification(); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(1.0f); // 2nd update should beep at 50% volume Mockito.reset(mRingtonePlayer); mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); verifyBeepVolume(0.5f); // Use different package for next notifications Loading @@ -2246,7 +2395,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(1.0f); verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); } @Test Loading Loading @@ -2355,12 +2504,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); // update should beep at 50% volume r.isUpdate = true; mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); verifyBeepVolume(0.5f); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); Loading @@ -2368,7 +2519,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepVolume(0.0f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @Test Loading