summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Iavor-Valentin Iftime <valiiftime@google.com> 2023-09-22 11:11:42 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-09-22 11:11:42 +0000
commitf079ed18f9ec9aa46112a8001e043105dd5089fd (patch)
tree9bb5d20d4eaab9247c0ad867112531b615574b3e
parentac50f74fd87320f04f05fabf398c11bac15764b0 (diff)
parent8a1dacc004932283081ad49f2558bea074bbf5bf (diff)
Merge "Extract alert handling into NotificationAttentionHelper" into main
-rw-r--r--core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java5
-rw-r--r--services/core/java/com/android/server/notification/NotificationAttentionHelper.java951
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerPrivate.java28
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java179
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java1973
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java65
6 files changed, 3141 insertions, 60 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index 4a848f68abab..cb2d93474971 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -81,6 +81,11 @@ public class SystemUiSystemPropertiesFlags {
public static final Flag PROPAGATE_CHANNEL_UPDATES_TO_CONVERSATIONS = releasedFlag(
"persist.sysui.notification.propagate_channel_updates_to_conversations");
+ // TODO: b/291907312 - remove feature flags
+ /** Gating the NMS->NotificationAttentionHelper buzzBeepBlink refactor */
+ public static final Flag ENABLE_ATTENTION_HELPER_REFACTOR = devFlag(
+ "persist.debug.sysui.notification.enable_attention_helper_refactor");
+
/** b/301242692: Visit extra URIs used in notifications to prevent security issues. */
public static final Flag VISIT_RISKY_URIS = devFlag(
"persist.sysui.notification.visit_risky_uris");
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
new file mode 100644
index 000000000000..75a0cf521a1d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -0,0 +1,951 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static android.app.Notification.FLAG_INSISTENT;
+import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS;
+
+import android.annotation.IntDef;
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.IRingtonePlayer;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.provider.Settings;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+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 java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * NotificationManagerService helper for handling notification attention effects:
+ * make noise, vibrate, or flash the LED.
+ * @hide
+ */
+public final class NotificationAttentionHelper {
+ static final String TAG = "NotifAttentionHelper";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ static final boolean DEBUG_INTERRUPTIVENESS = SystemProperties.getBoolean(
+ "debug.notification.interruptiveness", false);
+
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+ private final TelephonyManager mTelephonyManager;
+ private final NotificationManagerPrivate mNMP;
+ private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
+ private AccessibilityManager mAccessibilityManager;
+ private KeyguardManager mKeyguardManager;
+ private AudioManager mAudioManager;
+ private final LightsManager mLightsManager;
+ private final NotificationUsageStats mUsageStats;
+ private final ZenModeHelper mZenModeHelper;
+
+ private VibratorHelper mVibratorHelper;
+ // The last key in this list owns the hardware.
+ ArrayList<String> mLights = new ArrayList<>();
+ private LogicalLight mNotificationLight;
+ private LogicalLight mAttentionLight;
+
+ private final boolean mUseAttentionLight;
+ boolean mHasLight = true;
+
+ private final SettingsObserver mSettingsObserver;
+
+ private boolean mIsAutomotive;
+ private boolean mNotificationEffectsEnabledForAutomotive;
+ private boolean mDisableNotificationEffects;
+ private int mCallState;
+ private String mSoundNotificationKey;
+ private String mVibrateNotificationKey;
+ private boolean mSystemReady;
+ private boolean mInCallStateOffHook = false;
+ private boolean mScreenOn = true;
+ private boolean mUserPresent = false;
+ boolean mNotificationPulseEnabled;
+ private final Uri mInCallNotificationUri;
+ private final AudioAttributes mInCallNotificationAudioAttributes;
+ private final float mInCallNotificationVolume;
+ private Binder mCallNotificationToken = null;
+
+
+ public NotificationAttentionHelper(Context context, LightsManager lightsManager,
+ AccessibilityManager accessibilityManager, PackageManager packageManager,
+ NotificationUsageStats usageStats,
+ NotificationManagerPrivate notificationManagerPrivate,
+ ZenModeHelper zenModeHelper, SystemUiSystemPropertiesFlags.FlagResolver flagResolver) {
+ mContext = context;
+ mPackageManager = packageManager;
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mAccessibilityManager = accessibilityManager;
+ mLightsManager = lightsManager;
+ mNMP = notificationManagerPrivate;
+ mUsageStats = usageStats;
+ mZenModeHelper = zenModeHelper;
+ mFlagResolver = flagResolver;
+
+ mVibratorHelper = new VibratorHelper(context);
+
+ mNotificationLight = mLightsManager.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS);
+ mAttentionLight = mLightsManager.getLight(LightsManager.LIGHT_ID_ATTENTION);
+
+ Resources resources = context.getResources();
+ mUseAttentionLight = resources.getBoolean(R.bool.config_useAttentionLight);
+ mHasLight =
+ resources.getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed);
+
+ // Don't start allowing notifications until the setup wizard has run once.
+ // After that, including subsequent boots, init with notifications turned on.
+ // This works on the first boot because the setup wizard will toggle this
+ // flag at least once and we'll go back to 0 after that.
+ if (Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) == 0) {
+ mDisableNotificationEffects = true;
+ }
+
+ mInCallNotificationUri = Uri.parse(
+ "file://" + resources.getString(R.string.config_inCallNotificationSound));
+ mInCallNotificationAudioAttributes = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .build();
+ mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume);
+
+ mSettingsObserver = new SettingsObserver();
+ }
+
+ public void onSystemReady() {
+ mSystemReady = true;
+
+ mIsAutomotive = mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0);
+ mNotificationEffectsEnabledForAutomotive = mContext.getResources().getBoolean(
+ R.bool.config_enableServerNotificationEffectsForAutomotive);
+
+ mAudioManager = mContext.getSystemService(AudioManager.class);
+ mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+
+ registerBroadcastListeners();
+ }
+
+ private void registerBroadcastListeners() {
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ mTelephonyManager.listen(new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ if (mCallState == state) return;
+ if (DEBUG) Slog.d(TAG, "Call state changed: " + callStateToString(state));
+ mCallState = state;
+ }
+ }, PhoneStateListener.LISTEN_CALL_STATE);
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+ mContext.registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);
+
+ mContext.getContentResolver().registerContentObserver(
+ SettingsObserver.NOTIFICATION_LIGHT_PULSE_URI, false, mSettingsObserver,
+ UserHandle.USER_ALL);
+ }
+
+ @VisibleForTesting
+ /**
+ * Determine whether this notification should attempt to make noise, vibrate, or flash the LED
+ * @return buzzBeepBlink - bitfield (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) |
+ * (polite_attenuated ? 8 : 0) | (polite_muted ? 16 : 0)
+ */
+ int buzzBeepBlinkLocked(NotificationRecord record, Signals signals) {
+ if (mIsAutomotive && !mNotificationEffectsEnabledForAutomotive) {
+ return 0;
+ }
+ boolean buzz = false;
+ boolean beep = false;
+ boolean blink = false;
+
+ final String key = record.getKey();
+
+ if (DEBUG) {
+ Log.d(TAG, "buzzBeepBlinkLocked " + record);
+ }
+
+ // Should this notification make noise, vibe, or use the LED?
+ final boolean aboveThreshold =
+ mIsAutomotive
+ ? record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT
+ : record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT;
+ // Remember if this notification already owns the notification channels.
+ boolean wasBeep = key != null && key.equals(mSoundNotificationKey);
+ boolean wasBuzz = key != null && key.equals(mVibrateNotificationKey);
+ // These are set inside the conditional if the notification is allowed to make noise.
+ boolean hasValidVibrate = false;
+ boolean hasValidSound = false;
+ boolean sentAccessibilityEvent = false;
+
+ // If the notification will appear in the status bar, it should send an accessibility event
+ final boolean suppressedByDnd = record.isIntercepted()
+ && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0;
+ if (!record.isUpdate
+ && record.getImportance() > IMPORTANCE_MIN
+ && !suppressedByDnd
+ && isNotificationForCurrentUser(record, signals)) {
+ sendAccessibilityEvent(record);
+ sentAccessibilityEvent = true;
+ }
+
+ if (aboveThreshold && isNotificationForCurrentUser(record, signals)) {
+ if (mSystemReady && mAudioManager != null) {
+ Uri soundUri = record.getSound();
+ hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);
+ VibrationEffect vibration = record.getVibration();
+ // Demote sound to vibration if vibration missing & phone in vibration mode.
+ if (vibration == null
+ && hasValidSound
+ && (mAudioManager.getRingerModeInternal()
+ == AudioManager.RINGER_MODE_VIBRATE)
+ && mAudioManager.getStreamVolume(
+ AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) == 0) {
+ boolean insistent = (record.getFlags() & Notification.FLAG_INSISTENT) != 0;
+ vibration = mVibratorHelper.createFallbackVibration(insistent);
+ }
+ hasValidVibrate = vibration != null;
+ boolean hasAudibleAlert = hasValidSound || hasValidVibrate;
+ if (hasAudibleAlert && !shouldMuteNotificationLocked(record, signals)) {
+ if (!sentAccessibilityEvent) {
+ sendAccessibilityEvent(record);
+ sentAccessibilityEvent = true;
+ }
+ if (DEBUG) Slog.v(TAG, "Interrupting!");
+ boolean isInsistentUpdate = isInsistentUpdate(record);
+ if (hasValidSound) {
+ if (isInsistentUpdate) {
+ // don't reset insistent sound, it's jarring
+ beep = true;
+ } else {
+ if (isInCall()) {
+ playInCallNotification();
+ beep = true;
+ } else {
+ beep = playSound(record, soundUri);
+ }
+ if (beep) {
+ mSoundNotificationKey = key;
+ }
+ }
+ }
+
+ final boolean ringerModeSilent =
+ mAudioManager.getRingerModeInternal()
+ == AudioManager.RINGER_MODE_SILENT;
+ if (!isInCall() && hasValidVibrate && !ringerModeSilent) {
+ if (isInsistentUpdate) {
+ buzz = true;
+ } else {
+ buzz = playVibration(record, vibration, hasValidSound);
+ if (buzz) {
+ mVibrateNotificationKey = key;
+ }
+ }
+ }
+
+ // Try to start flash notification event whenever an audible and non-suppressed
+ // notification is received
+ mAccessibilityManager.startFlashNotificationEvent(mContext,
+ AccessibilityManager.FLASH_REASON_NOTIFICATION,
+ record.getSbn().getPackageName());
+
+ } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
+ hasValidSound = false;
+ }
+ }
+ }
+ // If a notification is updated to remove the actively playing sound or vibrate,
+ // cancel that feedback now
+ if (wasBeep && !hasValidSound) {
+ clearSoundLocked();
+ }
+ if (wasBuzz && !hasValidVibrate) {
+ clearVibrateLocked();
+ }
+
+ // light
+ // release the light
+ boolean wasShowLights = mLights.remove(key);
+ if (canShowLightsLocked(record, signals, aboveThreshold)) {
+ mLights.add(key);
+ updateLightsLocked();
+ if (mUseAttentionLight && mAttentionLight != null) {
+ mAttentionLight.pulse();
+ }
+ blink = true;
+ } else if (wasShowLights) {
+ updateLightsLocked();
+ }
+ final int buzzBeepBlink =
+ (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0);
+ if (buzzBeepBlink > 0) {
+ // Ignore summary updates because we don't display most of the information.
+ if (record.getSbn().isGroup() && record.getSbn().getNotification().isGroupSummary()) {
+ if (DEBUG_INTERRUPTIVENESS) {
+ Slog.v(TAG, "INTERRUPTIVENESS: "
+ + record.getKey() + " is not interruptive: summary");
+ }
+ } else if (record.canBubble()) {
+ if (DEBUG_INTERRUPTIVENESS) {
+ Slog.v(TAG, "INTERRUPTIVENESS: "
+ + record.getKey() + " is not interruptive: bubble");
+ }
+ } else {
+ record.setInterruptive(true);
+ if (DEBUG_INTERRUPTIVENESS) {
+ Slog.v(TAG, "INTERRUPTIVENESS: "
+ + record.getKey() + " is interruptive: alerted");
+ }
+ }
+ MetricsLogger.action(record.getLogMaker()
+ .setCategory(MetricsEvent.NOTIFICATION_ALERT)
+ .setType(MetricsEvent.TYPE_OPEN)
+ .setSubtype(buzzBeepBlink));
+ EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0);
+ }
+ record.setAudiblyAlerted(buzz || beep);
+
+ return buzzBeepBlink;
+ }
+
+ boolean isInsistentUpdate(final NotificationRecord record) {
+ return (Objects.equals(record.getKey(), mSoundNotificationKey)
+ || Objects.equals(record.getKey(), mVibrateNotificationKey))
+ && isCurrentlyInsistent();
+ }
+
+ boolean isCurrentlyInsistent() {
+ return isLoopingRingtoneNotification(mNMP.getNotificationByKey(mSoundNotificationKey))
+ || isLoopingRingtoneNotification(
+ mNMP.getNotificationByKey(mVibrateNotificationKey));
+ }
+
+ boolean shouldMuteNotificationLocked(final NotificationRecord record, final Signals signals) {
+ // Suppressed because it's a silent update
+ final Notification notification = record.getNotification();
+ if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) {
+ return true;
+ }
+
+ // Suppressed because a user manually unsnoozed something (or similar)
+ if (record.shouldPostSilently()) {
+ return true;
+ }
+
+ // muted by listener
+ final String disableEffects = disableNotificationEffects(record, signals.listenerHints);
+ if (disableEffects != null) {
+ ZenLog.traceDisableEffects(record, disableEffects);
+ return true;
+ }
+
+ // suppressed due to DND
+ if (record.isIntercepted()) {
+ return true;
+ }
+
+ // Suppressed because another notification in its group handles alerting
+ if (record.getSbn().isGroup()) {
+ if (notification.suppressAlertingDueToGrouping()) {
+ return true;
+ }
+ }
+
+ // Suppressed for being too recently noisy
+ final String pkg = record.getSbn().getPackageName();
+ if (mUsageStats.isAlertRateLimited(pkg)) {
+ Slog.e(TAG, "Muting recently noisy " + record.getKey());
+ return true;
+ }
+
+ // A different looping ringtone, such as an incoming call is playing
+ if (isCurrentlyInsistent() && !isInsistentUpdate(record)) {
+ return true;
+ }
+
+ // Suppressed since it's a non-interruptive update to a bubble-suppressed notification
+ final boolean isBubbleOrOverflowed = record.canBubble() && (record.isFlagBubbleRemoved()
+ || record.getNotification().isBubbleNotification());
+ if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed
+ && record.getNotification().getBubbleMetadata() != null) {
+ if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) {
+ if (playingRecord != null) {
+ if (playingRecord.getAudioAttributes().getUsage() == USAGE_NOTIFICATION_RINGTONE
+ && (playingRecord.getNotification().flags & FLAG_INSISTENT) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean playSound(final NotificationRecord record, Uri soundUri) {
+ boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0;
+ // play notifications if there is no user of exclusive audio focus
+ // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
+ // VIBRATE ringer mode)
+ if (!mAudioManager.isAudioFocusExclusive()
+ && (mAudioManager.getStreamVolume(
+ AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0)) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ if (DEBUG) {
+ Slog.v(TAG, "Playing sound " + soundUri + " with attributes "
+ + record.getAudioAttributes());
+ }
+ player.playAsync(soundUri, record.getSbn().getUser(), looping,
+ record.getAudioAttributes());
+ return true;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed playSound: " + e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return false;
+ }
+
+ private boolean playVibration(final NotificationRecord record, final VibrationEffect effect,
+ boolean delayVibForSound) {
+ // Escalate privileges so we can use the vibrator even if the
+ // notifying app does not have the VIBRATE permission.
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (delayVibForSound) {
+ new Thread(() -> {
+ // delay the vibration by the same amount as the notification sound
+ final int waitMs = mAudioManager.getFocusRampTimeMs(
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ record.getAudioAttributes());
+ if (DEBUG) {
+ Slog.v(TAG, "Delaying vibration for notification "
+ + record.getKey() + " by " + waitMs + "ms");
+ }
+ try {
+ Thread.sleep(waitMs);
+ } catch (InterruptedException e) { }
+ // Notifications might be canceled before it actually vibrates due to waitMs,
+ // so need to check that the notification is still valid for vibrate.
+ if (mNMP.getNotificationByKey(record.getKey()) != null) {
+ if (record.getKey().equals(mVibrateNotificationKey)) {
+ vibrate(record, effect, true);
+ } else {
+ if (DEBUG) {
+ Slog.v(TAG, "No vibration for notification "
+ + record.getKey() + ": a new notification is "
+ + "vibrating, or effects were cleared while waiting");
+ }
+ }
+ } else {
+ Slog.w(TAG, "No vibration for canceled notification "
+ + record.getKey());
+ }
+ }).start();
+ } else {
+ vibrate(record, effect, false);
+ }
+ return true;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void vibrate(NotificationRecord record, VibrationEffect effect, boolean delayed) {
+ // We need to vibrate as "android" so we can breakthrough DND. VibratorManagerService
+ // doesn't have a concept of vibrating on an app's behalf, so add the app information
+ // to the reason so we can still debug from bugreports
+ String reason = "Notification (" + record.getSbn().getOpPkg() + " "
+ + record.getSbn().getUid() + ") " + (delayed ? "(Delayed)" : "");
+ mVibratorHelper.vibrate(effect, record.getAudioAttributes(), reason);
+ }
+
+ void playInCallNotification() {
+ // TODO: Should we apply politeness to mInCallNotificationVolume ?
+ final ContentResolver cr = mContext.getContentResolver();
+ if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL
+ && Settings.Secure.getIntForUser(cr,
+ Settings.Secure.IN_CALL_NOTIFICATION_ENABLED, 1, cr.getUserId()) != 0) {
+ new Thread() {
+ @Override
+ public void run() {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ if (mCallNotificationToken != null) {
+ player.stop(mCallNotificationToken);
+ }
+ mCallNotificationToken = new Binder();
+ player.play(mCallNotificationToken, mInCallNotificationUri,
+ mInCallNotificationAudioAttributes,
+ mInCallNotificationVolume, false);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed playInCallNotification: " + e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }.start();
+ }
+ }
+
+ void clearSoundLocked() {
+ mSoundNotificationKey = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ player.stopAsync();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed clearSoundLocked: " + e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ void clearVibrateLocked() {
+ mVibrateNotificationKey = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mVibratorHelper.cancelVibration();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void clearLightsLocked() {
+ // light
+ mLights.clear();
+ updateLightsLocked();
+ }
+
+ public void clearEffectsLocked(String key) {
+ if (key.equals(mSoundNotificationKey)) {
+ clearSoundLocked();
+ }
+ if (key.equals(mVibrateNotificationKey)) {
+ clearVibrateLocked();
+ }
+ boolean removed = mLights.remove(key);
+ if (removed) {
+ updateLightsLocked();
+ }
+ }
+
+ public void clearAttentionEffects() {
+ clearSoundLocked();
+ clearVibrateLocked();
+ clearLightsLocked();
+ }
+
+ void updateLightsLocked() {
+ if (mNotificationLight == null) {
+ return;
+ }
+
+ // handle notification lights
+ NotificationRecord ledNotification = null;
+ while (ledNotification == null && !mLights.isEmpty()) {
+ final String owner = mLights.get(mLights.size() - 1);
+ ledNotification = mNMP.getNotificationByKey(owner);
+ if (ledNotification == null) {
+ Slog.wtfStack(TAG, "LED Notification does not exist: " + owner);
+ mLights.remove(owner);
+ }
+ }
+
+ // Don't flash while we are in a call or screen is on
+ if (ledNotification == null || isInCall() || mScreenOn) {
+ mNotificationLight.turnOff();
+ } else {
+ NotificationRecord.Light light = ledNotification.getLight();
+ if (light != null && mNotificationPulseEnabled) {
+ // pulse repeatedly
+ mNotificationLight.setFlashing(light.color, LogicalLight.LIGHT_FLASH_TIMED,
+ light.onMs, light.offMs);
+ }
+ }
+ }
+
+ boolean canShowLightsLocked(final NotificationRecord record, final Signals signals,
+ boolean aboveThreshold) {
+ // device lacks light
+ if (!mHasLight) {
+ return false;
+ }
+ // user turned lights off globally
+ if (!mNotificationPulseEnabled) {
+ return false;
+ }
+ // the notification/channel has no light
+ if (record.getLight() == null) {
+ return false;
+ }
+ // unimportant notification
+ if (!aboveThreshold) {
+ return false;
+ }
+ // suppressed due to DND
+ if ((record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_LIGHTS) != 0) {
+ return false;
+ }
+ // Suppressed because it's a silent update
+ final Notification notification = record.getNotification();
+ if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) {
+ return false;
+ }
+ // Suppressed because another notification in its group handles alerting
+ if (record.getSbn().isGroup() && record.getNotification().suppressAlertingDueToGrouping()) {
+ return false;
+ }
+ // not if in call
+ if (isInCall()) {
+ return false;
+ }
+ // check current user
+ if (!isNotificationForCurrentUser(record, signals)) {
+ return false;
+ }
+ // Light, but only when the screen is off
+ return true;
+ }
+
+ private String disableNotificationEffects(NotificationRecord record, int listenerHints) {
+ if (mDisableNotificationEffects) {
+ return "booleanState";
+ }
+
+ if ((listenerHints & HINT_HOST_DISABLE_EFFECTS) != 0) {
+ return "listenerHints";
+ }
+ if (record != null && record.getAudioAttributes() != null) {
+ if ((listenerHints & HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) != 0) {
+ if (record.getAudioAttributes().getUsage()
+ != AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
+ return "listenerNoti";
+ }
+ }
+ if ((listenerHints & HINT_HOST_DISABLE_CALL_EFFECTS) != 0) {
+ if (record.getAudioAttributes().getUsage()
+ == AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
+ return "listenerCall";
+ }
+ }
+ }
+ if (mCallState != TelephonyManager.CALL_STATE_IDLE && !mZenModeHelper.isCall(record)) {
+ return "callState";
+ }
+
+ return null;
+ }
+
+ public void updateDisableNotificationEffectsLocked(int status) {
+ mDisableNotificationEffects =
+ (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
+ //if (disableNotificationEffects(null) != null) {
+ if (mDisableNotificationEffects) {
+ // cancel whatever is going on
+ clearAttentionEffects();
+ }
+ }
+
+ private boolean isInCall() {
+ if (mInCallStateOffHook) {
+ return true;
+ }
+ int audioMode = mAudioManager.getMode();
+ if (audioMode == AudioManager.MODE_IN_CALL
+ || audioMode == AudioManager.MODE_IN_COMMUNICATION) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String callStateToString(int state) {
+ switch (state) {
+ case TelephonyManager.CALL_STATE_IDLE: return "CALL_STATE_IDLE";
+ case TelephonyManager.CALL_STATE_RINGING: return "CALL_STATE_RINGING";
+ case TelephonyManager.CALL_STATE_OFFHOOK: return "CALL_STATE_OFFHOOK";
+ default: return "CALL_STATE_UNKNOWN_" + state;
+ }
+ }
+
+ private boolean isNotificationForCurrentUser(final NotificationRecord record,
+ final Signals signals) {
+ final int currentUser;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ currentUser = ActivityManager.getCurrentUser();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return (record.getUserId() == UserHandle.USER_ALL || record.getUserId() == currentUser
+ || signals.isCurrentProfile);
+ }
+
+ void sendAccessibilityEvent(NotificationRecord record) {
+ if (!mAccessibilityManager.isEnabled()) {
+ return;
+ }
+
+ final Notification notification = record.getNotification();
+ final CharSequence packageName = record.getSbn().getPackageName();
+ final AccessibilityEvent event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ event.setPackageName(packageName);
+ event.setClassName(Notification.class.getName());
+ final int visibilityOverride = record.getPackageVisibilityOverride();
+ final int notifVisibility = visibilityOverride == NotificationManager.VISIBILITY_NO_OVERRIDE
+ ? notification.visibility : visibilityOverride;
+ final int userId = record.getUser().getIdentifier();
+ final boolean needPublic = userId >= 0 && mKeyguardManager.isDeviceLocked(userId);
+ if (needPublic && notifVisibility != Notification.VISIBILITY_PUBLIC) {
+ // Emit the public version if we're on the lockscreen and this notification isn't
+ // publicly visible.
+ event.setParcelableData(notification.publicVersion);
+ } else {
+ event.setParcelableData(notification);
+ }
+ final CharSequence tickerText = notification.tickerText;
+ if (!TextUtils.isEmpty(tickerText)) {
+ event.getText().add(tickerText);
+ }
+
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
+ pw.println("\n Notification attention state:");
+ pw.print(prefix);
+ pw.println(" mSoundNotificationKey=" + mSoundNotificationKey);
+ pw.print(prefix);
+ pw.println(" mVibrateNotificationKey=" + mVibrateNotificationKey);
+ pw.print(prefix);
+ pw.println(" mDisableNotificationEffects=" + mDisableNotificationEffects);
+ pw.print(prefix);
+ pw.println(" mCallState=" + callStateToString(mCallState));
+ pw.print(prefix);
+ pw.println(" mSystemReady=" + mSystemReady);
+ pw.print(prefix);
+ pw.println(" mNotificationPulseEnabled=" + mNotificationPulseEnabled);
+
+ int N = mLights.size();
+ if (N > 0) {
+ pw.print(prefix);
+ pw.println(" Lights List:");
+ for (int i=0; i<N; i++) {
+ if (i == N - 1) {
+ pw.print(" > ");
+ } else {
+ pw.print(" ");
+ }
+ pw.println(mLights.get(i));
+ }
+ pw.println(" ");
+ }
+
+ }
+
+ // External signals set from NMS
+ public static class Signals {
+ private final boolean isCurrentProfile;
+ private final int listenerHints;
+
+ public Signals(boolean isCurrentProfile, int listenerHints) {
+ this.isCurrentProfile = isCurrentProfile;
+ this.listenerHints = listenerHints;
+ }
+ }
+
+ //====================== Observers =============================
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (action.equals(Intent.ACTION_SCREEN_ON)) {
+ // Keep track of screen on/off state, but do not turn off the notification light
+ // until user passes through the lock screen or views the notification.
+ mScreenOn = true;
+ updateLightsLocked();
+ } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+ mScreenOn = false;
+ mUserPresent = false;
+ updateLightsLocked();
+ } else if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
+ mInCallStateOffHook = TelephonyManager.EXTRA_STATE_OFFHOOK
+ .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
+ updateLightsLocked();
+ } else if (action.equals(Intent.ACTION_USER_PRESENT)) {
+ mUserPresent = true;
+ // turn off LED when user passes through lock screen
+ if (mNotificationLight != null) {
+ mNotificationLight.turnOff();
+ }
+ }
+ }
+ };
+
+ private final class SettingsObserver extends ContentObserver {
+
+ private static final Uri NOTIFICATION_LIGHT_PULSE_URI = Settings.System.getUriFor(
+ Settings.System.NOTIFICATION_LIGHT_PULSE);
+ public SettingsObserver() {
+ super(null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
+ boolean pulseEnabled = Settings.System.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.System.NOTIFICATION_LIGHT_PULSE, 0,
+ UserHandle.USER_CURRENT)
+ != 0;
+ if (mNotificationPulseEnabled != pulseEnabled) {
+ mNotificationPulseEnabled = pulseEnabled;
+ updateLightsLocked();
+ }
+ }
+ }
+ }
+
+
+ //TODO: cleanup most (all?) of these
+ //======================= FOR TESTS =====================
+ @VisibleForTesting
+ void setIsAutomotive(boolean isAutomotive) {
+ mIsAutomotive = isAutomotive;
+ }
+
+ @VisibleForTesting
+ void setNotificationEffectsEnabledForAutomotive(boolean isEnabled) {
+ mNotificationEffectsEnabledForAutomotive = isEnabled;
+ }
+
+ @VisibleForTesting
+ void setSystemReady(boolean systemReady) {
+ mSystemReady = systemReady;
+ }
+
+ @VisibleForTesting
+ void setKeyguardManager(KeyguardManager keyguardManager) {
+ mKeyguardManager = keyguardManager;
+ }
+
+ @VisibleForTesting
+ void setAccessibilityManager(AccessibilityManager am) {
+ mAccessibilityManager = am;
+ }
+
+ @VisibleForTesting
+ VibratorHelper getVibratorHelper() {
+ return mVibratorHelper;
+ }
+
+ @VisibleForTesting
+ void setVibratorHelper(VibratorHelper helper) {
+ mVibratorHelper = helper;
+ }
+
+ @VisibleForTesting
+ void setScreenOn(boolean on) {
+ mScreenOn = on;
+ }
+
+ @VisibleForTesting
+ void setLights(LogicalLight light) {
+ mNotificationLight = light;
+ mAttentionLight = light;
+ mNotificationPulseEnabled = true;
+ mHasLight = true;
+ }
+
+ @VisibleForTesting
+ void setAudioManager(AudioManager audioManager) {
+ mAudioManager = audioManager;
+ }
+
+ @VisibleForTesting
+ void setInCallStateOffHook(boolean inCallStateOffHook) {
+ mInCallStateOffHook = inCallStateOffHook;
+ }
+
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerPrivate.java b/services/core/java/com/android/server/notification/NotificationManagerPrivate.java
new file mode 100644
index 000000000000..2cc63ebfc962
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationManagerPrivate.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.annotation.Nullable;
+
+/**
+ * Interface that allows components (helpers) to access NotificationRecords
+ * without an explicit reference to NotificationManagerService.
+ */
+interface NotificationManagerPrivate {
+ @Nullable
+ NotificationRecord getNotificationByKey(String key);
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index bada8165766c..802dfb182297 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -287,6 +287,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.compat.IPlatformCompat;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.MetricsLogger;
@@ -315,6 +316,7 @@ import com.android.server.SystemService;
import com.android.server.job.JobSchedulerInternal;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
+import com.android.server.notification.Flags;
import com.android.server.notification.ManagedServices.ManagedServiceInfo;
import com.android.server.notification.ManagedServices.UserProfiles;
import com.android.server.notification.toast.CustomToastRecord;
@@ -684,6 +686,7 @@ public class NotificationManagerService extends SystemService {
private boolean mIsAutomotive;
private boolean mNotificationEffectsEnabledForAutomotive;
private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
+ protected NotificationAttentionHelper mAttentionHelper;
private int mWarnRemoteViewsSizeBytes;
private int mStripRemoteViewsSizeBytes;
@@ -1167,12 +1170,16 @@ public class NotificationManagerService extends SystemService {
@Override
public void onSetDisabled(int status) {
synchronized (mNotificationLock) {
- mDisableNotificationEffects =
- (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
- if (disableNotificationEffects(null) != null) {
- // cancel whatever's going on
- clearSoundLocked();
- clearVibrateLocked();
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.updateDisableNotificationEffectsLocked(status);
+ } else {
+ mDisableNotificationEffects =
+ (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
+ if (disableNotificationEffects(null) != null) {
+ // cancel whatever's going on
+ clearSoundLocked();
+ clearVibrateLocked();
+ }
}
}
}
@@ -1309,9 +1316,13 @@ public class NotificationManagerService extends SystemService {
public void clearEffects() {
synchronized (mNotificationLock) {
if (DBG) Slog.d(TAG, "clearEffects");
- clearSoundLocked();
- clearVibrateLocked();
- clearLightsLocked();
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.clearAttentionEffects();
+ } else {
+ clearSoundLocked();
+ clearVibrateLocked();
+ clearLightsLocked();
+ }
}
}
@@ -1534,7 +1545,12 @@ public class NotificationManagerService extends SystemService {
int changedFlags = data.getFlags() ^ flags;
if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) {
// Suppress notification flag changed, clear any effects
- clearEffectsLocked(key);
+ if (mFlagResolver.isEnabled(
+ NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.clearEffectsLocked(key);
+ } else {
+ clearEffectsLocked(key);
+ }
}
data.setFlags(flags);
// Shouldn't alert again just because of a flag change.
@@ -1626,6 +1642,12 @@ public class NotificationManagerService extends SystemService {
};
+ NotificationManagerPrivate mNotificationManagerPrivate = key -> {
+ synchronized (mNotificationLock) {
+ return mNotificationsByKey.get(key);
+ }
+ };
+
@VisibleForTesting
void logSmartSuggestionsVisible(NotificationRecord r, int notificationLocation) {
// If the newly visible notification has smart suggestions
@@ -1873,19 +1895,28 @@ public class NotificationManagerService extends SystemService {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (action.equals(Intent.ACTION_SCREEN_ON)) {
- // Keep track of screen on/off state, but do not turn off the notification light
- // until user passes through the lock screen or views the notification.
- mScreenOn = true;
- updateNotificationPulse();
- } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
- mScreenOn = false;
- updateNotificationPulse();
- } else if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
- mInCallStateOffHook = TelephonyManager.EXTRA_STATE_OFFHOOK
+ if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ if (action.equals(Intent.ACTION_SCREEN_ON)) {
+ // Keep track of screen on/off state, but do not turn off the notification light
+ // until user passes through the lock screen or views the notification.
+ mScreenOn = true;
+ updateNotificationPulse();
+ } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+ mScreenOn = false;
+ updateNotificationPulse();
+ } else if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
+ mInCallStateOffHook = TelephonyManager.EXTRA_STATE_OFFHOOK
.equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
- updateNotificationPulse();
- } else if (action.equals(Intent.ACTION_USER_STOPPED)) {
+ updateNotificationPulse();
+ } else if (action.equals(Intent.ACTION_USER_PRESENT)) {
+ // turn off LED when user passes through lock screen
+ if (mNotificationLight != null) {
+ mNotificationLight.turnOff();
+ }
+ }
+ }
+
+ if (action.equals(Intent.ACTION_USER_STOPPED)) {
int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
if (userHandle >= 0) {
cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle,
@@ -1898,11 +1929,6 @@ public class NotificationManagerService extends SystemService {
REASON_PROFILE_TURNED_OFF);
mSnoozeHelper.clearData(userHandle);
}
- } else if (action.equals(Intent.ACTION_USER_PRESENT)) {
- // turn off LED when user passes through lock screen
- if (mNotificationLight != null) {
- mNotificationLight.turnOff();
- }
} else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
mUserProfiles.updateCache(context);
@@ -1976,8 +2002,10 @@ public class NotificationManagerService extends SystemService {
ContentResolver resolver = getContext().getContentResolver();
resolver.registerContentObserver(NOTIFICATION_BADGING_URI,
false, this, UserHandle.USER_ALL);
- resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
+ if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
false, this, UserHandle.USER_ALL);
+ }
resolver.registerContentObserver(NOTIFICATION_RATE_LIMIT_URI,
false, this, UserHandle.USER_ALL);
resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI,
@@ -2000,13 +2028,15 @@ public class NotificationManagerService extends SystemService {
public void update(Uri uri) {
ContentResolver resolver = getContext().getContentResolver();
- if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
- boolean pulseEnabled = Settings.System.getIntForUser(resolver,
- Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT)
+ if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
+ boolean pulseEnabled = Settings.System.getIntForUser(resolver,
+ Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT)
!= 0;
- if (mNotificationPulseEnabled != pulseEnabled) {
- mNotificationPulseEnabled = pulseEnabled;
- updateNotificationPulse();
+ if (mNotificationPulseEnabled != pulseEnabled) {
+ mNotificationPulseEnabled = pulseEnabled;
+ updateNotificationPulse();
+ }
}
}
if (uri == null || NOTIFICATION_RATE_LIMIT_URI.equals(uri)) {
@@ -2491,14 +2521,22 @@ public class NotificationManagerService extends SystemService {
mToastRateLimiter = toastRateLimiter;
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager,
+ mAccessibilityManager, mPackageManagerClient, usageStats,
+ mNotificationManagerPrivate, mZenModeHelper, flagResolver);
+ }
+
// register for various Intents.
// If this is called within a test, make sure to unregister the intent receivers by
// calling onDestroy()
IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_SCREEN_ON);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
- filter.addAction(Intent.ACTION_USER_PRESENT);
+ if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+ }
filter.addAction(Intent.ACTION_USER_STOPPED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_USER_ADDED);
@@ -2818,6 +2856,9 @@ public class NotificationManagerService extends SystemService {
}
registerNotificationPreferencesPullers();
new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker);
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.onSystemReady();
+ }
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
// This observer will force an update when observe is called, causing us to
// bind to listener services.
@@ -6375,6 +6416,9 @@ public class NotificationManagerService extends SystemService {
pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate);
pw.println(" hideSilentStatusBar="
+ mPreferencesHelper.shouldHideSilentStatusIcons());
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.dump(pw, " ", filter);
+ }
}
pw.println(" mArchive=" + mArchive.toString());
mArchive.dumpImpl(pw, filter);
@@ -7632,7 +7676,11 @@ public class NotificationManagerService extends SystemService {
boolean wasPosted = removeFromNotificationListsLocked(r);
cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null,
SystemClock.elapsedRealtime());
- updateLightsLocked();
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.updateLightsLocked();
+ } else {
+ updateLightsLocked();
+ }
if (isSnoozable(r)) {
if (mSnoozeCriterionId != null) {
mAssistants.notifyAssistantSnoozedLocked(r, mSnoozeCriterionId);
@@ -7761,7 +7809,11 @@ public class NotificationManagerService extends SystemService {
cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
mSendDelete, childrenFlagChecker, mReason,
mCancellationElapsedTimeMs);
- updateLightsLocked();
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.updateLightsLocked();
+ } else {
+ updateLightsLocked();
+ }
if (mShortcutHelper != null) {
mShortcutHelper.maybeListenForShortcutChangesForBubbles(r,
true /* isRemoved */,
@@ -8054,7 +8106,14 @@ public class NotificationManagerService extends SystemService {
int buzzBeepBlinkLoggingCode = 0;
if (!r.isHidden()) {
- buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
+ new NotificationAttentionHelper.Signals(
+ mUserProfiles.isCurrentProfile(r.getUserId()),
+ mListenerHints));
+ } else {
+ buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
+ }
}
if (notification.getSmallIcon() != null) {
@@ -9034,7 +9093,13 @@ public class NotificationManagerService extends SystemService {
|| interruptiveChanged;
if (interceptBefore && !record.isIntercepted()
&& record.isNewEnoughForAlerting(System.currentTimeMillis())) {
- buzzBeepBlinkLocked(record);
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.buzzBeepBlinkLocked(record,
+ new NotificationAttentionHelper.Signals(
+ mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints));
+ } else {
+ buzzBeepBlinkLocked(record);
+ }
// Log alert after change in intercepted state to Zen Log as well
ZenLog.traceAlertOnUpdatedIntercept(record);
@@ -9408,18 +9473,22 @@ public class NotificationManagerService extends SystemService {
});
}
- // sound
- if (canceledKey.equals(mSoundNotificationKey)) {
- clearSoundLocked();
- }
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.clearEffectsLocked(canceledKey);
+ } else {
+ // sound
+ if (canceledKey.equals(mSoundNotificationKey)) {
+ clearSoundLocked();
+ }
- // vibrate
- if (canceledKey.equals(mVibrateNotificationKey)) {
- clearVibrateLocked();
- }
+ // vibrate
+ if (canceledKey.equals(mVibrateNotificationKey)) {
+ clearVibrateLocked();
+ }
- // light
- mLights.remove(canceledKey);
+ // light
+ mLights.remove(canceledKey);
+ }
}
// Record usage stats
@@ -9768,7 +9837,11 @@ public class NotificationManagerService extends SystemService {
cancellationElapsedTimeMs);
}
}
- updateLightsLocked();
+ if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mAttentionHelper.updateLightsLocked();
+ } else {
+ updateLightsLocked();
+ }
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
new file mode 100644
index 000000000000..81867df74abd
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -0,0 +1,1973 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://`www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import static android.app.Notification.FLAG_BUBBLE;
+import static android.app.Notification.GROUP_ALERT_ALL;
+import static android.app.Notification.GROUP_ALERT_CHILDREN;
+import static android.app.Notification.GROUP_ALERT_SUMMARY;
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+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.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+import android.view.accessibility.IAccessibilityManagerClient;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
+import com.android.internal.config.sysui.TestableFlagResolver;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.InstanceIdSequenceFake;
+import com.android.internal.util.IntPair;
+import com.android.server.UiServiceTestCase;
+import com.android.server.lights.LightsManager;
+import com.android.server.lights.LogicalLight;
+import com.android.server.pm.PackageManagerService;
+import java.util.Objects;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SuppressLint("GuardedBy")
+public class NotificationAttentionHelperTest extends UiServiceTestCase {
+
+ @Mock AudioManager mAudioManager;
+ @Mock Vibrator mVibrator;
+ @Mock android.media.IRingtonePlayer mRingtonePlayer;
+ @Mock LogicalLight mLight;
+ @Mock
+ NotificationManagerService.WorkerHandler mHandler;
+ @Mock
+ NotificationUsageStats mUsageStats;
+ @Mock
+ IAccessibilityManager mAccessibilityService;
+ @Mock
+ KeyguardManager mKeyguardManager;
+ NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
+ private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
+ 1 << 30);
+
+ private NotificationManagerService mService;
+ private String mPkg = "com.android.server.notification";
+ private int mId = 1001;
+ private int mOtherId = 1002;
+ private String mTag = null;
+ private int mUid = 1000;
+ private int mPid = 2000;
+ private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser());
+ private NotificationChannel mChannel;
+
+ private NotificationAttentionHelper mAttentionHelper;
+ private TestableFlagResolver mTestFlagResolver = new TestableFlagResolver();
+ private AccessibilityManager mAccessibilityManager;
+ private static final NotificationAttentionHelper.Signals DEFAULT_SIGNALS =
+ new NotificationAttentionHelper.Signals(false, 0);
+
+ private VibrateRepeatMatcher mVibrateOnceMatcher = new VibrateRepeatMatcher(-1);
+ private VibrateRepeatMatcher mVibrateLoopMatcher = new VibrateRepeatMatcher(0);
+
+ private static final long[] CUSTOM_VIBRATION = new long[] {
+ 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400,
+ 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400,
+ 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 };
+ private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI;
+ private static final AudioAttributes CUSTOM_ATTRIBUTES = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .build();
+ private static final int CUSTOM_LIGHT_COLOR = Color.BLACK;
+ private static final int CUSTOM_LIGHT_ON = 10000;
+ private static final int CUSTOM_LIGHT_OFF = 10000;
+ private static final int MAX_VIBRATION_DELAY = 1000;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ getContext().addMockSystemService(Vibrator.class, mVibrator);
+
+ when(mAudioManager.isAudioFocusExclusive()).thenReturn(false);
+ when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer);
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+ when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50);
+ when(mUsageStats.isAlertRateLimited(any())).thenReturn(false);
+ when(mVibrator.hasFrequencyControl()).thenReturn(false);
+ when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false);
+
+ long serviceReturnValue = IntPair.of(
+ AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED,
+ AccessibilityEvent.TYPES_ALL_MASK);
+ when(mAccessibilityService.addClient(any(), anyInt())).thenReturn(serviceReturnValue);
+ mAccessibilityManager =
+ new AccessibilityManager(getContext(), Handler.getMain(), mAccessibilityService, 0,
+ true);
+ verify(mAccessibilityService).addClient(any(IAccessibilityManagerClient.class), anyInt());
+ assertTrue(mAccessibilityManager.isEnabled());
+
+ // TODO (b/291907312): remove feature flag
+ mTestFlagResolver.setFlagOverride(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR, true);
+
+ mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger,
+ mNotificationInstanceIdSequence));
+
+ initAttentionHelper(mTestFlagResolver);
+
+ mChannel = new NotificationChannel("test", "test", IMPORTANCE_HIGH);
+ }
+
+ private void initAttentionHelper(TestableFlagResolver flagResolver) {
+ mAttentionHelper = new NotificationAttentionHelper(getContext(), mock(LightsManager.class),
+ mAccessibilityManager, getContext().getPackageManager(), mUsageStats,
+ mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver);
+ mAttentionHelper.setVibratorHelper(new VibratorHelper(getContext()));
+ mAttentionHelper.setAudioManager(mAudioManager);
+ mAttentionHelper.setSystemReady(true);
+ mAttentionHelper.setLights(mLight);
+ mAttentionHelper.setScreenOn(false);
+ mAttentionHelper.setAccessibilityManager(mAccessibilityManager);
+ mAttentionHelper.setKeyguardManager(mKeyguardManager);
+ mAttentionHelper.setScreenOn(false);
+ mAttentionHelper.setInCallStateOffHook(false);
+ mAttentionHelper.mNotificationPulseEnabled = true;
+ }
+
+ //
+ // Convenience functions for creating notification records
+ //
+
+ private NotificationRecord getNoisyOtherNotification() {
+ return getNotificationRecord(mOtherId, false /* insistent */, false /* once */,
+ true /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBeepyNotification() {
+ return getNotificationRecord(mId, false /* insistent */, false /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBeepyOtherNotification() {
+ return getNotificationRecord(mOtherId, false /* insistent */, false /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBeepyOnceNotification() {
+ return getNotificationRecord(mId, false /* insistent */, true /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getQuietNotification() {
+ return getNotificationRecord(mId, false /* insistent */, true /* once */,
+ false /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getQuietOtherNotification() {
+ return getNotificationRecord(mOtherId, false /* insistent */, false /* once */,
+ false /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getQuietOnceNotification() {
+ return getNotificationRecord(mId, false /* insistent */, true /* once */,
+ false /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getInsistentBeepyNotification() {
+ return getNotificationRecord(mId, true /* insistent */, false /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getInsistentBeepyOnceNotification() {
+ return getNotificationRecord(mId, true /* insistent */, true /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getInsistentBeepyLeanbackNotification() {
+ return getLeanbackNotificationRecord(mId, true /* insistent */, false /* once */,
+ true /* noisy */, false /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBuzzyNotification() {
+ return getNotificationRecord(mId, false /* insistent */, false /* once */,
+ false /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBuzzyOtherNotification() {
+ return getNotificationRecord(mOtherId, false /* insistent */, false /* once */,
+ false /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBuzzyOnceNotification() {
+ return getNotificationRecord(mId, false /* insistent */, true /* once */,
+ false /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getInsistentBuzzyNotification() {
+ return getNotificationRecord(mId, true /* insistent */, false /* once */,
+ false /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getBuzzyBeepyNotification() {
+ return getNotificationRecord(mId, false /* insistent */, false /* once */,
+ true /* noisy */, true /* buzzy*/, false /* lights */);
+ }
+
+ private NotificationRecord getLightsNotification() {
+ return getNotificationRecord(mId, false /* insistent */, false /* once */,
+ false /* noisy */, false /* buzzy*/, true /* lights */);
+ }
+
+ private NotificationRecord getLightsOnceNotification() {
+ return getNotificationRecord(mId, false /* insistent */, true /* once */,
+ false /* noisy */, false /* buzzy*/, true /* lights */);
+ }
+
+ private NotificationRecord getCallRecord(int id, NotificationChannel channel, boolean looping) {
+ final Builder builder = new Builder(getContext())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setPriority(Notification.PRIORITY_HIGH);
+ Notification n = builder.build();
+ if (looping) {
+ n.flags |= Notification.FLAG_INSISTENT;
+ }
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid,
+ mPid, n, mUser, null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(getContext(), sbn, channel);
+ mService.addNotification(r);
+
+ return r;
+ }
+
+ private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once,
+ boolean noisy, boolean buzzy, boolean lights) {
+ return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, buzzy, noisy,
+ lights, null, Notification.GROUP_ALERT_ALL, false);
+ }
+
+ private NotificationRecord getLeanbackNotificationRecord(int id, boolean insistent,
+ boolean once,
+ boolean noisy, boolean buzzy, boolean lights) {
+ return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, true, true,
+ true,
+ null, Notification.GROUP_ALERT_ALL, true);
+ }
+
+ private NotificationRecord getBeepyNotificationRecord(String groupKey, int groupAlertBehavior) {
+ return getNotificationRecord(mId, false, false, true, false, false, true, true, true,
+ groupKey, groupAlertBehavior, false);
+ }
+
+ private NotificationRecord getLightsNotificationRecord(String groupKey,
+ int groupAlertBehavior) {
+ return getNotificationRecord(mId, false, false, false, false, true /*lights*/, true,
+ true, true, groupKey, groupAlertBehavior, false);
+ }
+
+ 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) {
+
+ final Builder builder = new Builder(getContext())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setOnlyAlertOnce(once);
+
+ int defaults = 0;
+ if (noisy) {
+ if (defaultSound) {
+ defaults |= Notification.DEFAULT_SOUND;
+ mChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+ Notification.AUDIO_ATTRIBUTES_DEFAULT);
+ } else {
+ builder.setSound(CUSTOM_SOUND);
+ mChannel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
+ }
+ } else {
+ mChannel.setSound(null, null);
+ }
+ if (buzzy) {
+ if (defaultVibration) {
+ defaults |= Notification.DEFAULT_VIBRATE;
+ } else {
+ builder.setVibrate(CUSTOM_VIBRATION);
+ mChannel.setVibrationPattern(CUSTOM_VIBRATION);
+ }
+ mChannel.enableVibration(true);
+ } else {
+ mChannel.setVibrationPattern(null);
+ mChannel.enableVibration(false);
+ }
+
+ if (lights) {
+ if (defaultLights) {
+ defaults |= Notification.DEFAULT_LIGHTS;
+ } else {
+ builder.setLights(CUSTOM_LIGHT_COLOR, CUSTOM_LIGHT_ON, CUSTOM_LIGHT_OFF);
+ }
+ mChannel.enableLights(true);
+ } else {
+ mChannel.enableLights(false);
+ }
+ builder.setDefaults(defaults);
+
+ builder.setGroup(groupKey);
+ builder.setGroupAlertBehavior(groupAlertBehavior);
+
+ Notification n = builder.build();
+ if (insistent) {
+ n.flags |= Notification.FLAG_INSISTENT;
+ }
+
+ Context context = spy(getContext());
+ PackageManager packageManager = spy(context.getPackageManager());
+ when(context.getPackageManager()).thenReturn(packageManager);
+ when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
+ .thenReturn(isLeanback);
+
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid,
+ mPid, n, mUser, null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(context, sbn, mChannel);
+ mService.addNotification(r);
+ return r;
+ }
+
+ //
+ // Convenience functions for interacting with mocks
+ //
+
+ private void verifyNeverBeep() throws RemoteException {
+ verify(mRingtonePlayer, never()).playAsync(any(), any(), anyBoolean(), any());
+ }
+
+ private void verifyBeepUnlooped() throws RemoteException {
+ verify(mRingtonePlayer, times(1)).playAsync(any(), any(), eq(false), any());
+ }
+
+ private void verifyBeepLooped() throws RemoteException {
+ verify(mRingtonePlayer, times(1)).playAsync(any(), any(), eq(true), any());
+ }
+
+ private void verifyBeep(int times) throws RemoteException {
+ verify(mRingtonePlayer, times(times)).playAsync(any(), any(), anyBoolean(), any());
+ }
+
+ private void verifyNeverStopAudio() throws RemoteException {
+ verify(mRingtonePlayer, never()).stopAsync();
+ }
+
+ private void verifyStopAudio() throws RemoteException {
+ verify(mRingtonePlayer, times(1)).stopAsync();
+ }
+
+ private void verifyNeverVibrate() {
+ verify(mVibrator, never()).vibrate(anyInt(), anyString(), any(), anyString(),
+ any(VibrationAttributes.class));
+ }
+
+ private void verifyVibrate() {
+ verifyVibrate(/* times= */ 1);
+ }
+
+ private void verifyVibrate(int times) {
+ verifyVibrate(mVibrateOnceMatcher, times(times));
+ }
+
+ private void verifyVibrateLooped() {
+ verifyVibrate(mVibrateLoopMatcher, times(1));
+ }
+
+ private void verifyDelayedVibrateLooped() {
+ verifyVibrate(mVibrateLoopMatcher, timeout(MAX_VIBRATION_DELAY).times(1));
+ }
+
+ private void verifyDelayedVibrate(VibrationEffect effect) {
+ verifyVibrate(argument -> Objects.equals(effect, argument),
+ timeout(MAX_VIBRATION_DELAY).times(1));
+ }
+
+ private void verifyDelayedNeverVibrate() {
+ verify(mVibrator, after(MAX_VIBRATION_DELAY).never()).vibrate(anyInt(), anyString(), any(),
+ anyString(), any(VibrationAttributes.class));
+ }
+
+ private void verifyVibrate(ArgumentMatcher<VibrationEffect> effectMatcher,
+ VerificationMode verification) {
+ ArgumentCaptor<VibrationAttributes> captor =
+ ArgumentCaptor.forClass(VibrationAttributes.class);
+ verify(mVibrator, verification).vibrate(eq(Process.SYSTEM_UID),
+ eq(PackageManagerService.PLATFORM_PACKAGE_NAME), argThat(effectMatcher),
+ anyString(), captor.capture());
+ assertEquals(0, (captor.getValue().getFlags()
+ & VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY));
+ }
+
+ private void verifyStopVibrate() {
+ int alarmClassUsageFilter =
+ VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK;
+ verify(mVibrator, times(1)).cancel(eq(alarmClassUsageFilter));
+ }
+
+ private void verifyNeverStopVibrate() {
+ verify(mVibrator, never()).cancel();
+ verify(mVibrator, never()).cancel(anyInt());
+ }
+
+ private void verifyNeverLights() {
+ verify(mLight, never()).setFlashing(anyInt(), anyInt(), anyInt(), anyInt());
+ }
+
+ private void verifyLights() {
+ verify(mLight, times(1)).setFlashing(anyInt(), anyInt(), anyInt(), anyInt());
+ }
+
+ //
+ // Tests
+ //
+
+ @Test
+ public void testLights() throws Exception {
+ NotificationRecord r = getLightsNotification();
+ r.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyLights();
+ assertTrue(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testBeep() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ verifyNeverVibrate();
+ verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt());
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLockedPrivateA11yRedaction() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE);
+ r.getNotification().visibility = Notification.VISIBILITY_PRIVATE;
+ when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true);
+ AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
+ when(accessibilityManager.isEnabled()).thenReturn(true);
+ mAttentionHelper.setAccessibilityManager(accessibilityManager);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ ArgumentCaptor<AccessibilityEvent> eventCaptor =
+ ArgumentCaptor.forClass(AccessibilityEvent.class);
+
+ verify(accessibilityManager, times(1))
+ .sendAccessibilityEvent(eventCaptor.capture());
+
+ AccessibilityEvent event = eventCaptor.getValue();
+ assertEquals(r.getNotification().publicVersion, event.getParcelableData());
+ }
+
+ @Test
+ public void testLockedOverridePrivateA11yRedaction() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setPackageVisibilityOverride(Notification.VISIBILITY_PRIVATE);
+ r.getNotification().visibility = Notification.VISIBILITY_PUBLIC;
+ when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true);
+ AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
+ when(accessibilityManager.isEnabled()).thenReturn(true);
+ mAttentionHelper.setAccessibilityManager(accessibilityManager);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ ArgumentCaptor<AccessibilityEvent> eventCaptor =
+ ArgumentCaptor.forClass(AccessibilityEvent.class);
+
+ verify(accessibilityManager, times(1))
+ .sendAccessibilityEvent(eventCaptor.capture());
+
+ AccessibilityEvent event = eventCaptor.getValue();
+ assertEquals(r.getNotification().publicVersion, event.getParcelableData());
+ }
+
+ @Test
+ public void testLockedPublicA11yNoRedaction() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE);
+ r.getNotification().visibility = Notification.VISIBILITY_PUBLIC;
+ when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true);
+ AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
+ when(accessibilityManager.isEnabled()).thenReturn(true);
+ mAttentionHelper.setAccessibilityManager(accessibilityManager);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ ArgumentCaptor<AccessibilityEvent> eventCaptor =
+ ArgumentCaptor.forClass(AccessibilityEvent.class);
+
+ verify(accessibilityManager, times(1))
+ .sendAccessibilityEvent(eventCaptor.capture());
+
+ AccessibilityEvent event = eventCaptor.getValue();
+ assertEquals(r.getNotification(), event.getParcelableData());
+ }
+
+ @Test
+ public void testUnlockedPrivateA11yNoRedaction() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE);
+ r.getNotification().visibility = Notification.VISIBILITY_PRIVATE;
+ when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false);
+ AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
+ when(accessibilityManager.isEnabled()).thenReturn(true);
+ mAttentionHelper.setAccessibilityManager(accessibilityManager);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ ArgumentCaptor<AccessibilityEvent> eventCaptor =
+ ArgumentCaptor.forClass(AccessibilityEvent.class);
+
+ verify(accessibilityManager, times(1))
+ .sendAccessibilityEvent(eventCaptor.capture());
+
+ AccessibilityEvent event = eventCaptor.getValue();
+ assertEquals(r.getNotification(), event.getParcelableData());
+ }
+
+ @Test
+ public void testBeepInsistently() throws Exception {
+ NotificationRecord r = getInsistentBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyBeepLooped();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoLeanbackBeep() throws Exception {
+ NotificationRecord r = getInsistentBeepyLeanbackNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoBeepForAutomotiveIfEffectsDisabled() throws Exception {
+ mAttentionHelper.setIsAutomotive(true);
+ mAttentionHelper.setNotificationEffectsEnabledForAutomotive(false);
+
+ NotificationRecord r = getBeepyNotification();
+ r.setSystemImportance(NotificationManager.IMPORTANCE_HIGH);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ }
+
+ @Test
+ public void testNoBeepForImportanceDefaultInAutomotiveIfEffectsEnabled() throws Exception {
+ mAttentionHelper.setIsAutomotive(true);
+ mAttentionHelper.setNotificationEffectsEnabledForAutomotive(true);
+
+ NotificationRecord r = getBeepyNotification();
+ r.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ }
+
+ @Test
+ public void testBeepForImportanceHighInAutomotiveIfEffectsEnabled() throws Exception {
+ mAttentionHelper.setIsAutomotive(true);
+ mAttentionHelper.setNotificationEffectsEnabledForAutomotive(true);
+
+ NotificationRecord r = getBeepyNotification();
+ r.setSystemImportance(NotificationManager.IMPORTANCE_HIGH);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ assertTrue(r.isInterruptive());
+ }
+
+ @Test
+ public void testNoInterruptionForMin() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setSystemImportance(NotificationManager.IMPORTANCE_MIN);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ verifyNeverVibrate();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoInterruptionForIntercepted() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ r.setIntercepted(true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ verifyNeverVibrate();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testBeepTwice() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mRingtonePlayer);
+
+ // update should beep
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyBeepUnlooped();
+ verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt());
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testHonorAlertOnlyOnceForBeep() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getBeepyOnceNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mRingtonePlayer);
+
+ // update should not beep
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testNoisyUpdateDoesNotCancelAudio() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverStopAudio();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoisyOnceUpdateDoesNotCancelAudio() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getBeepyOnceNotification();
+ s.isUpdate = true;
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+
+ verifyNeverStopAudio();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ /**
+ * Tests the case where the user re-posts a {@link Notification} with looping sound where
+ * {@link Notification.Builder#setOnlyAlertOnce(true)} has been called. This should silence
+ * the sound associated with the notification.
+ * @throws Exception
+ */
+ @Test
+ public void testNoisyOnceUpdateDoesCancelAudio() throws Exception {
+ NotificationRecord r = getInsistentBeepyNotification();
+ NotificationRecord s = getInsistentBeepyOnceNotification();
+ s.isUpdate = true;
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+
+ verifyStopAudio();
+ }
+
+ @Test
+ public void testQuietUpdateDoesNotCancelAudioFromOther() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getQuietNotification();
+ s.isUpdate = true;
+ NotificationRecord other = getNoisyOtherNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ mAttentionHelper.buzzBeepBlinkLocked(other, DEFAULT_SIGNALS); // this takes the audio stream
+ Mockito.reset(mRingtonePlayer);
+
+ // should not stop noise, since we no longer own it
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS); // this no longer owns the stream
+ verifyNeverStopAudio();
+ assertTrue(other.isInterruptive());
+ assertNotEquals(-1, other.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietInterloperDoesNotCancelAudio() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord other = getQuietOtherNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mRingtonePlayer);
+
+ // should not stop noise, since it does not own it
+ mAttentionHelper.buzzBeepBlinkLocked(other, DEFAULT_SIGNALS);
+ verifyNeverStopAudio();
+ }
+
+ @Test
+ public void testQuietUpdateCancelsAudio() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getQuietNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // quiet update should stop making noise
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyStopAudio();
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietOnceUpdateCancelsAudio() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getQuietOnceNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // stop making noise - this is a weird corner case, but quiet should override once
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyStopAudio();
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testInCallNotification() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper = spy(mAttentionHelper);
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mRingtonePlayer);
+
+ mAttentionHelper.setInCallStateOffHook(true);
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verify(mAttentionHelper, times(1)).playInCallNotification();
+ verifyNeverBeep(); // doesn't play normal beep
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoDemoteSoundToVibrateIfVibrateGiven() throws Exception {
+ NotificationRecord r = getBuzzyBeepyNotification();
+ assertTrue(r.getSound() != null);
+
+ // the phone is quiet
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyDelayedVibrate(r.getVibration());
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoDemoteSoundToVibrateIfNonNotificationStream() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ assertTrue(r.getSound() != null);
+ assertNull(r.getVibration());
+
+ // the phone is quiet
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverVibrate();
+ verifyBeepUnlooped();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testDemoteSoundToVibrate() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ assertTrue(r.getSound() != null);
+ assertNull(r.getVibration());
+
+ // the phone is quiet
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyDelayedVibrate(
+ mAttentionHelper.getVibratorHelper().createFallbackVibration(
+ /* insistent= */ false));
+ verify(mRingtonePlayer, never()).playAsync(anyObject(), anyObject(), anyBoolean(),
+ anyObject());
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testDemoteInsistentSoundToVibrate() throws Exception {
+ NotificationRecord r = getInsistentBeepyNotification();
+ assertTrue(r.getSound() != null);
+ assertNull(r.getVibration());
+
+ // the phone is quiet
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyDelayedVibrateLooped();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testVibrate() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ verifyVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testInsistentVibrate() {
+ NotificationRecord r = getInsistentBuzzyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyVibrateLooped();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testVibrateTwice() {
+ NotificationRecord r = getBuzzyNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mVibrator);
+
+ // update should vibrate
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testPostSilently() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ r.setPostSilently(true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummarySilenceChild() throws Exception {
+ NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(child.isInterruptive());
+ assertEquals(-1, child.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummaryNoSilenceSummary() throws Exception {
+ NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ // summaries are never interruptive for notification counts
+ assertFalse(summary.isInterruptive());
+ assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummaryNoSilenceNonGroupChild() throws Exception {
+ NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_SUMMARY);
+
+ mAttentionHelper.buzzBeepBlinkLocked(nonGroup, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ assertTrue(nonGroup.isInterruptive());
+ assertNotEquals(-1, nonGroup.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildSilenceSummary() throws Exception {
+ NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ assertFalse(summary.isInterruptive());
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildNoSilenceChild() throws Exception {
+ NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ assertTrue(child.isInterruptive());
+ assertNotEquals(-1, child.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildNoSilenceNonGroupSummary() throws Exception {
+ NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_CHILDREN);
+
+ mAttentionHelper.buzzBeepBlinkLocked(nonGroup, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ assertTrue(nonGroup.isInterruptive());
+ assertNotEquals(-1, nonGroup.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertAllNoSilenceGroup() throws Exception {
+ NotificationRecord group = getBeepyNotificationRecord("a", GROUP_ALERT_ALL);
+
+ mAttentionHelper.buzzBeepBlinkLocked(group, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ assertTrue(group.isInterruptive());
+ assertNotEquals(-1, group.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testHonorAlertOnlyOnceForBuzz() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord s = getBuzzyOnceNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mVibrator);
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+
+ // update should not beep
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyNeverVibrate();
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoisyUpdateDoesNotCancelVibrate() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testNoisyOnceUpdateDoesNotCancelVibrate() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord s = getBuzzyOnceNotification();
+ s.isUpdate = true;
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+
+ verifyNeverStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietUpdateDoesNotCancelVibrateFromOther() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord s = getQuietNotification();
+ s.isUpdate = true;
+ NotificationRecord other = getNoisyOtherNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ // this takes the vibrate stream
+ mAttentionHelper.buzzBeepBlinkLocked(other, DEFAULT_SIGNALS);
+ Mockito.reset(mVibrator);
+
+ // should not stop vibrate, since we no longer own it
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS); // this no longer owns the stream
+ verifyNeverStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertTrue(other.isInterruptive());
+ assertNotEquals(-1, other.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietInterloperDoesNotCancelVibrate() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord other = getQuietOtherNotification();
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ Mockito.reset(mVibrator);
+
+ // should not stop noise, since it does not own it
+ mAttentionHelper.buzzBeepBlinkLocked(other, DEFAULT_SIGNALS);
+ verifyNeverStopVibrate();
+ assertFalse(other.isInterruptive());
+ assertEquals(-1, other.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietUpdateCancelsVibrate() {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord s = getQuietNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyVibrate();
+
+ // quiet update should stop making noise
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietOnceUpdateCancelVibrate() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+ NotificationRecord s = getQuietOnceNotification();
+ s.isUpdate = true;
+
+ // set up internal state
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyVibrate();
+
+ // stop making noise - this is a weird corner case, but quiet should override once
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testQuietUpdateCancelsDemotedVibrate() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+ NotificationRecord s = getQuietNotification();
+
+ // the phone is quiet
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyDelayedVibrate(mAttentionHelper.getVibratorHelper().createFallbackVibration(false));
+
+ // quiet update should stop making noise
+ mAttentionHelper.buzzBeepBlinkLocked(s, DEFAULT_SIGNALS);
+ verifyStopVibrate();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ assertFalse(s.isInterruptive());
+ assertEquals(-1, s.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testEmptyUriSoundTreatedAsNoSound() throws Exception {
+ NotificationChannel channel = new NotificationChannel("test", "test", IMPORTANCE_HIGH);
+ channel.setSound(Uri.EMPTY, null);
+ final Notification n = new Builder(getContext(), "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+ mPid, n, mUser, null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(getContext(), sbn, channel);
+ mService.addNotification(r);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testRepeatedSoundOverLimitMuted() throws Exception {
+ when(mUsageStats.isAlertRateLimited(any())).thenReturn(true);
+
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testPostingSilentNotificationDoesNotAffectRateLimiting() throws Exception {
+ NotificationRecord r = getQuietNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verify(mUsageStats, never()).isAlertRateLimited(any());
+ }
+
+ @Test
+ public void testPostingGroupSuppressedDoesNotAffectRateLimiting() throws Exception {
+ NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verify(mUsageStats, never()).isAlertRateLimited(any());
+ }
+
+ @Test
+ public void testGroupSuppressionFailureDoesNotAffectRateLimiting() {
+ NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verify(mUsageStats, times(1)).isAlertRateLimited(any());
+ }
+
+ @Test
+ public void testCrossUserSoundMuted() throws Exception {
+ final Notification n = new Builder(getContext(), "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+
+ int userId = mUser.getIdentifier() + 1;
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+ mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(getContext(), sbn,
+ new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testA11yMinInitialPost() throws Exception {
+ NotificationRecord r = getQuietNotification();
+ r.setSystemImportance(IMPORTANCE_MIN);
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testA11yQuietInitialPost() throws Exception {
+ NotificationRecord r = getQuietNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testA11yQuietUpdate() throws Exception {
+ NotificationRecord r = getQuietNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testA11yCrossUserEventNotSent() throws Exception {
+ final Notification n = new Builder(getContext(), "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ int userId = mUser.getIdentifier() + 1;
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+ mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(getContext(), sbn,
+ new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testLightsScreenOn() {
+ mAttentionHelper.setScreenOn(true);
+ NotificationRecord r = getLightsNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertTrue(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsInCall() {
+ mAttentionHelper.setInCallStateOffHook(true);
+ NotificationRecord r = getLightsNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsSilentUpdate() {
+ NotificationRecord r = getLightsOnceNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyLights();
+ assertTrue(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+
+ r = getLightsOnceNotification();
+ r.isUpdate = true;
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ // checks that lights happened once, i.e. this new call didn't trigger them again
+ verifyLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsUnimportant() {
+ NotificationRecord r = getLightsNotification();
+ r.setSystemImportance(IMPORTANCE_LOW);
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsNoLights() {
+ NotificationRecord r = getQuietNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsNoLightOnDevice() {
+ mAttentionHelper.mHasLight = false;
+ NotificationRecord r = getLightsNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsLightsOffGlobally() {
+ mAttentionHelper.mNotificationPulseEnabled = false;
+ NotificationRecord r = getLightsNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsDndIntercepted() {
+ NotificationRecord r = getLightsNotification();
+ r.setSuppressedVisualEffects(SUPPRESSED_EFFECT_LIGHTS);
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummaryNoLightsChild() {
+ NotificationRecord child = getLightsNotificationRecord("a", GROUP_ALERT_SUMMARY);
+
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+
+ verifyNeverLights();
+ assertFalse(child.isInterruptive());
+ assertEquals(-1, child.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummaryLightsSummary() {
+ NotificationRecord summary = getLightsNotificationRecord("a", GROUP_ALERT_SUMMARY);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+
+ verifyLights();
+ // summaries should never count for interruptiveness counts
+ assertFalse(summary.isInterruptive());
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertSummaryLightsNonGroupChild() {
+ NotificationRecord nonGroup = getLightsNotificationRecord(null, GROUP_ALERT_SUMMARY);
+
+ mAttentionHelper.buzzBeepBlinkLocked(nonGroup, DEFAULT_SIGNALS);
+
+ verifyLights();
+ assertTrue(nonGroup.isInterruptive());
+ assertEquals(-1, nonGroup.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildNoLightsSummary() {
+ NotificationRecord summary = getLightsNotificationRecord("a", GROUP_ALERT_CHILDREN);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+
+ verifyNeverLights();
+ assertFalse(summary.isInterruptive());
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildLightsChild() {
+ NotificationRecord child = getLightsNotificationRecord("a", GROUP_ALERT_CHILDREN);
+
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+
+ verifyLights();
+ assertTrue(child.isInterruptive());
+ assertEquals(-1, child.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertChildLightsNonGroupSummary() {
+ NotificationRecord nonGroup = getLightsNotificationRecord(null, GROUP_ALERT_CHILDREN);
+
+ mAttentionHelper.buzzBeepBlinkLocked(nonGroup, DEFAULT_SIGNALS);
+
+ verifyLights();
+ assertTrue(nonGroup.isInterruptive());
+ assertEquals(-1, nonGroup.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testGroupAlertAllLightsGroup() {
+ NotificationRecord group = getLightsNotificationRecord("a", GROUP_ALERT_ALL);
+
+ mAttentionHelper.buzzBeepBlinkLocked(group, DEFAULT_SIGNALS);
+
+ verifyLights();
+ assertTrue(group.isInterruptive());
+ assertEquals(-1, group.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testLightsCheckCurrentUser() {
+ final Notification n = new Builder(getContext(), "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ int userId = mUser.getIdentifier() + 10;
+ StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+ mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(getContext(), sbn,
+ new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyNeverLights();
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testListenerHintCall() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord r = getCallRecord(1, ringtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS));
+
+ verifyNeverBeep();
+ }
+
+ @Test
+ public void testListenerHintCall_notificationSound() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS));
+
+ verifyBeepUnlooped();
+ }
+
+ @Test
+ public void testListenerHintNotification() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS));
+
+ verifyNeverBeep();
+ }
+
+ @Test
+ public void testListenerHintBoth() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord r = getCallRecord(1, ringtoneChannel, true);
+ NotificationRecord s = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS
+ | NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS));
+ mAttentionHelper.buzzBeepBlinkLocked(s, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS
+ | NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS));
+
+ verifyNeverBeep();
+ }
+
+ @Test
+ public void testListenerHintNotification_callSound() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord r = getCallRecord(1, ringtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals(false,
+ NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS));
+
+ verifyBeepLooped();
+ }
+
+ @Test
+ public void testCannotInterruptRingtoneInsistentBeep() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+ mService.addNotification(ringtoneNotification);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+
+ NotificationRecord interrupter = getBeepyOtherNotification();
+ assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyBeep(1);
+
+ assertFalse(interrupter.isInterruptive());
+ assertEquals(-1, interrupter.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testRingtoneInsistentBeep_canUpdate() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"),
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+ mService.addNotification(ringtoneNotification);
+ assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+ DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+ verifyDelayedVibrateLooped();
+ Mockito.reset(mVibrator);
+ Mockito.reset(mRingtonePlayer);
+
+ assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+ DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+
+ // beep wasn't reset
+ verifyNeverBeep();
+ verifyNeverVibrate();
+ verifyNeverStopAudio();
+ verifyNeverStopVibrate();
+ }
+
+ @Test
+ public void testRingtoneInsistentBeep_clearEffectsStopsSoundAndVibration() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"),
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+ mService.addNotification(ringtoneNotification);
+ assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+ DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+ verifyDelayedVibrateLooped();
+
+ mAttentionHelper.clearSoundLocked();
+ mAttentionHelper.clearVibrateLocked();
+
+ verifyStopAudio();
+ verifyStopVibrate();
+ }
+
+ @Test
+ public void testRingtoneInsistentBeep_neverVibratesWhenEffectsClearedBeforeDelay()
+ throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"),
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+ mService.addNotification(ringtoneNotification);
+ assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+ DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+ verifyNeverVibrate();
+
+ mAttentionHelper.clearSoundLocked();
+ mAttentionHelper.clearVibrateLocked();
+
+ verifyStopAudio();
+ verifyDelayedNeverVibrate();
+ }
+
+ @Test
+ public void testCannotInterruptRingtoneInsistentBuzz() {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Uri.EMPTY,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+ assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+ DEFAULT_SIGNALS));
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyVibrateLooped();
+
+ NotificationRecord interrupter = getBuzzyOtherNotification();
+ assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS));
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyVibrate(1);
+
+ assertFalse(interrupter.isInterruptive());
+ assertEquals(-1, interrupter.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testCanInterruptRingtoneNonInsistentBeep() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, false);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepUnlooped();
+
+ NotificationRecord interrupter = getBeepyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyBeep(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testCanInterruptRingtoneNonInsistentBuzz() {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(null,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, false);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyVibrate();
+
+ NotificationRecord interrupter = getBuzzyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyVibrate(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testRingtoneInsistentBeep_doesNotBlockFutureSoundsOnceStopped() throws Exception {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+
+ mAttentionHelper.clearSoundLocked();
+
+ NotificationRecord interrupter = getBeepyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyBeep(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testRingtoneInsistentBuzz_doesNotBlockFutureSoundsOnceStopped() {
+ NotificationChannel ringtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ ringtoneChannel.setSound(null,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
+ ringtoneChannel.enableVibration(true);
+ NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyVibrateLooped();
+
+ mAttentionHelper.clearVibrateLocked();
+
+ NotificationRecord interrupter = getBuzzyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyVibrate(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testCanInterruptNonRingtoneInsistentBeep() throws Exception {
+ NotificationChannel fakeRingtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ NotificationRecord ringtoneNotification = getCallRecord(1, fakeRingtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+ verifyBeepLooped();
+
+ NotificationRecord interrupter = getBeepyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyBeep(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testCanInterruptNonRingtoneInsistentBuzz() {
+ NotificationChannel fakeRingtoneChannel =
+ new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
+ fakeRingtoneChannel.enableVibration(true);
+ fakeRingtoneChannel.setSound(null,
+ new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION).build());
+ NotificationRecord ringtoneNotification = getCallRecord(1, fakeRingtoneChannel, true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
+
+ NotificationRecord interrupter = getBuzzyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+
+ verifyVibrate(2);
+
+ assertTrue(interrupter.isInterruptive());
+ }
+
+ @Test
+ public void testBubbleSuppressedNotificationDoesntMakeSound() {
+ Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
+ mock(PendingIntent.class), mock(Icon.class))
+ .build();
+
+ NotificationRecord record = getBuzzyNotification();
+ metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+ record.getNotification().setBubbleMetadata(metadata);
+ record.setAllowBubble(true);
+ record.getNotification().flags |= FLAG_BUBBLE;
+ record.isUpdate = true;
+ record.setInterruptive(false);
+
+ mAttentionHelper.buzzBeepBlinkLocked(record, DEFAULT_SIGNALS);
+ verifyNeverVibrate();
+ }
+
+ @Test
+ public void testOverflowBubbleSuppressedNotificationDoesntMakeSound() {
+ Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
+ mock(PendingIntent.class), mock(Icon.class))
+ .build();
+
+ NotificationRecord record = getBuzzyNotification();
+ metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+ record.getNotification().setBubbleMetadata(metadata);
+ record.setFlagBubbleRemoved(true);
+ record.setAllowBubble(true);
+ record.isUpdate = true;
+ record.setInterruptive(false);
+
+ mAttentionHelper.buzzBeepBlinkLocked(record, DEFAULT_SIGNALS);
+ verifyNeverVibrate();
+ }
+
+ @Test
+ public void testBubbleUpdateMakesSound() {
+ Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
+ mock(PendingIntent.class), mock(Icon.class))
+ .build();
+
+ NotificationRecord record = getBuzzyNotification();
+ record.getNotification().setBubbleMetadata(metadata);
+ record.setAllowBubble(true);
+ record.getNotification().flags |= FLAG_BUBBLE;
+ record.isUpdate = true;
+ record.setInterruptive(true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(record, DEFAULT_SIGNALS);
+ verifyVibrate(1);
+ }
+
+ @Test
+ public void testNewBubbleSuppressedNotifMakesSound() {
+ Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
+ mock(PendingIntent.class), mock(Icon.class))
+ .build();
+
+ NotificationRecord record = getBuzzyNotification();
+ metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+ record.getNotification().setBubbleMetadata(metadata);
+ record.setAllowBubble(true);
+ record.getNotification().flags |= FLAG_BUBBLE;
+ record.isUpdate = false;
+ record.setInterruptive(true);
+
+ mAttentionHelper.buzzBeepBlinkLocked(record, DEFAULT_SIGNALS);
+ verifyVibrate(1);
+ }
+
+ @Test
+ public void testStartFlashNotificationEvent_receiveBeepyNotification() throws Exception {
+ NotificationRecord r = getBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ verifyNeverVibrate();
+ verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(),
+ eq(r.getSbn().getPackageName()));
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testStartFlashNotificationEvent_receiveBuzzyNotification() throws Exception {
+ NotificationRecord r = getBuzzyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ verifyVibrate();
+ verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(),
+ eq(r.getSbn().getPackageName()));
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testStartFlashNotificationEvent_receiveBuzzyBeepyNotification() throws Exception {
+ NotificationRecord r = getBuzzyBeepyNotification();
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyBeepUnlooped();
+ verifyDelayedVibrate(r.getVibration());
+ verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(),
+ eq(r.getSbn().getPackageName()));
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ @Test
+ public void testStartFlashNotificationEvent_receiveBuzzyBeepyNotification_ringerModeSilent()
+ throws Exception {
+ NotificationRecord r = getBuzzyBeepyNotification();
+ when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
+ when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+
+ verifyNeverBeep();
+ verifyNeverVibrate();
+ verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(),
+ eq(r.getSbn().getPackageName()));
+ assertFalse(r.isInterruptive());
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ }
+
+ static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> {
+ private final int mRepeatIndex;
+
+ VibrateRepeatMatcher(int repeatIndex) {
+ mRepeatIndex = repeatIndex;
+ }
+
+ @Override
+ public boolean matches(VibrationEffect actual) {
+ if (actual instanceof VibrationEffect.Composed
+ && ((VibrationEffect.Composed) actual).getRepeatIndex() == mRepeatIndex) {
+ return true;
+ }
+ // All non-waveform effects are essentially one shots.
+ return mRepeatIndex == -1;
+ }
+
+ @Override
+ public String toString() {
+ return "repeatIndex=" + mRepeatIndex;
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 4576e9be07ad..9543a2de1e13 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -84,6 +84,7 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
@@ -272,6 +273,7 @@ import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -279,6 +281,7 @@ import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@@ -372,6 +375,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
private DevicePolicyManagerInternal mDevicePolicyManager;
@Mock
private PowerManager mPowerManager;
+ @Mock
+ private LightsManager mLightsManager;
private final ArrayList<WakeLock> mAcquiredWakeLocks = new ArrayList<>();
private final TestPostNotificationTrackerFactory mPostNotificationTrackerFactory =
new TestPostNotificationTrackerFactory();
@@ -503,9 +508,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
setDpmAppOppsExemptFromDismissal(false);
- mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger,
- mNotificationInstanceIdSequence);
-
// Use this testable looper.
mTestableLooper = TestableLooper.get(this);
// MockPackageManager - default returns ApplicationInfo with matching calling UID
@@ -527,8 +529,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
Object[] args = invocation.getArguments();
return (int) args[1] == mUid;
});
- final LightsManager mockLightsManager = mock(LightsManager.class);
- when(mockLightsManager.getLight(anyInt())).thenReturn(mock(LogicalLight.class));
+ when(mLightsManager.getLight(anyInt())).thenReturn(mock(LogicalLight.class));
when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
when(mPackageManagerClient.hasSystemFeature(FEATURE_WATCH)).thenReturn(false);
when(mUgmInternal.newUriPermissionOwner(anyString())).thenReturn(mPermOwner);
@@ -601,6 +602,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
return wl;
});
+ // TODO (b/291907312): remove feature flag
+ mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, false);
+ initNMS();
+ }
+
+ private void initNMS() throws Exception {
+ mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger,
+ mNotificationInstanceIdSequence);
+
// apps allowed as convos
mService.setStringArrayResourceValue(PKG_O);
@@ -617,7 +627,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
- mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
+ mLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAtm,
mAppUsageStats, mDevicePolicyManager, mUgm, mUgmInternal,
mAppOpsManager, mUm, mHistoryManager, mStatsManager,
@@ -628,11 +638,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
// Return first true for RoleObserver main-thread check
when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false);
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper);
+ Mockito.reset(mHistoryManager);
verify(mHistoryManager, never()).onBootPhaseAppsCanStart();
mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START, mMainLooper);
verify(mHistoryManager).onBootPhaseAppsCanStart();
- mService.setAudioManager(mAudioManager);
+ // TODO b/291907312: remove feature flag
+ if (mTestFlagResolver.isEnabled(ENABLE_ATTENTION_HELPER_REFACTOR)) {
+ mService.mAttentionHelper.setAudioManager(mAudioManager);
+ } else {
+ mService.setAudioManager(mAudioManager);
+ }
mStrongAuthTracker = mService.new StrongAuthTrackerFake(mContext);
mService.setStrongAuthTracker(mStrongAuthTracker);
@@ -1653,6 +1669,23 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testEnqueueNotificationWithTag_WritesExpectedLogs_NAHRefactor() throws Exception {
+ // TODO b/291907312: remove feature flag
+ mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, true);
+ // Cleanup NMS before re-initializing
+ if (mService != null) {
+ try {
+ mService.onDestroy();
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ // can throw if a broadcast receiver was never registered
+ }
+ }
+ initNMS();
+
+ testEnqueueNotificationWithTag_WritesExpectedLogs();
+ }
+
+ @Test
public void testEnqueueNotificationWithTag_LogsOnMajorUpdates() throws Exception {
final String tag = "testEnqueueNotificationWithTag_LogsOnMajorUpdates";
Notification original = new Notification.Builder(mContext,
@@ -9015,6 +9048,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped_NAHRefactor()
+ throws Exception {
+ // TODO b/291907312: remove feature flag
+ mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, true);
+ // Cleanup NMS before re-initializing
+ if (mService != null) {
+ try {
+ mService.onDestroy();
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ // can throw if a broadcast receiver was never registered
+ }
+ }
+ initNMS();
+
+ testOnBubbleMetadataChangedToSuppressNotification_soundStopped();
+ }
+
+ @Test
public void testGrantInlineReplyUriPermission_recordExists() throws Exception {
int userId = UserManager.isHeadlessSystemUserMode()
? UserHandle.getUserId(UID_HEADLESS)