Defer FGS notification display for a few seconds
When a service is transitioned to the foreground state, we defer display
of its associated Notification for a short time, to reduce user
disturbance in situations where the FGS is short-lived. Apps can force
immediate display when they know it's relevant, and Notifications known
to correspond to contexts in which immediate display is appropriate -
such as media playback - are not deferred.
The behavior can be disabled or the deferral interval adjusted via
DeviceConfig.
Bug: 171499612
Test: ApiDemos
Test: atest CtsAppTestCases:ServiceTest
Test: atest CtsAppTestCases:NotificationManagerTest
Change-Id: I0cae3dc6f943e99873ed8c1687914ad08ec29b57
diff --git a/core/api/current.txt b/core/api/current.txt
index dfe80c7..25dfb70 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -5778,6 +5778,7 @@
method @NonNull public android.app.Notification.Builder setRemoteInputHistory(CharSequence[]);
method @NonNull public android.app.Notification.Builder setSettingsText(CharSequence);
method @NonNull public android.app.Notification.Builder setShortcutId(String);
+ method @NonNull public android.app.Notification.Builder setShowForegroundImmediately(boolean);
method @NonNull public android.app.Notification.Builder setShowWhen(boolean);
method @NonNull public android.app.Notification.Builder setSmallIcon(@DrawableRes int);
method @NonNull public android.app.Notification.Builder setSmallIcon(@DrawableRes int, int);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 4c08e75..e5d17d0 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -630,10 +630,16 @@
*/
public static final int FLAG_BUBBLE = 0x00001000;
+ /**
+ * @hide
+ */
+ public static final int FLAG_IMMEDIATE_FGS_DISPLAY = 0x00002000;
+
/** @hide */
@IntDef({FLAG_SHOW_LIGHTS, FLAG_ONGOING_EVENT, FLAG_INSISTENT, FLAG_ONLY_ALERT_ONCE,
FLAG_AUTO_CANCEL, FLAG_NO_CLEAR, FLAG_FOREGROUND_SERVICE, FLAG_HIGH_PRIORITY,
- FLAG_LOCAL_ONLY, FLAG_GROUP_SUMMARY, FLAG_AUTOGROUP_SUMMARY, FLAG_BUBBLE})
+ FLAG_LOCAL_ONLY, FLAG_GROUP_SUMMARY, FLAG_AUTOGROUP_SUMMARY, FLAG_BUBBLE,
+ FLAG_IMMEDIATE_FGS_DISPLAY})
@Retention(RetentionPolicy.SOURCE)
public @interface NotificationFlags{};
@@ -4381,6 +4387,18 @@
}
/**
+ * Set to {@code true} to require that the Notification associated with a
+ * foreground service is shown as soon as the service's {@code startForeground()}
+ * method is called, even if the system's UI policy might otherwise defer
+ * its visibility to a later time.
+ */
+ @NonNull
+ public Builder setShowForegroundImmediately(boolean showImmediately) {
+ setFlag(FLAG_IMMEDIATE_FGS_DISPLAY, showImmediately);
+ return this;
+ }
+
+ /**
* Make this notification automatically dismissed when the user touches it.
*
* @see Notification#FLAG_AUTO_CANCEL
@@ -6383,6 +6401,35 @@
}
/**
+ * Describe whether this notification's content such that it should always display
+ * immediately when tied to a foreground service, even if the system might generally
+ * avoid showing the notifications for short-lived foreground service lifetimes.
+ *
+ * Immediate visibility of the Notification is recommended when:
+ * <ul>
+ * <li>The app specifically indicated it with
+ * {@link Notification.Builder#setShowForegroundImmediately(boolean)
+ * setShowForegroundImmediately(true)}</li>
+ * <li>It is a media notification or has an associated media session</li>
+ * <li>It is a call or navigation notification</li>
+ * <li>It provides additional action affordances</li>
+ * </ul>
+ * @return whether this notification should always be displayed immediately when
+ * its associated service transitions to the foreground state
+ * @hide
+ */
+ public boolean shouldShowForegroundImmediately() {
+ if ((flags & Notification.FLAG_IMMEDIATE_FGS_DISPLAY) != 0
+ || isMediaNotification() || hasMediaSession()
+ || CATEGORY_CALL.equals(category)
+ || CATEGORY_NAVIGATION.equals(category)
+ || (actions != null && actions.length > 0)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* @return whether this notification has a media session attached
* @hide
*/
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 62392c9..01e77f8 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -217,6 +217,11 @@
*/
final ArrayList<ServiceRecord> mDestroyingServices = new ArrayList<>();
+ /**
+ * List of services for which display of the FGS notification has been deferred.
+ */
+ final ArrayList<ServiceRecord> mPendingFgsNotifications = new ArrayList<>();
+
/** Temporary list for holding the results of calls to {@link #collectPackageServicesLocked} */
private ArrayList<ServiceRecord> mTmpCollectionResults = null;
@@ -1551,7 +1556,7 @@
registerAppOpCallbackLocked(r);
mAm.updateForegroundServiceUsageStats(r.name, r.userId, true);
}
- r.postNotification();
+ postFgsNotificationLocked(r);
if (r.app != null) {
updateServiceForegroundLocked(r.app, true);
}
@@ -1608,6 +1613,9 @@
updateServiceForegroundLocked(r.app, true);
}
}
+ // Leave the time-to-display as already set: re-entering foreground mode will
+ // only resume the previous quiet timeout, or will display immediately if the
+ // deferral period had already passed.
if ((flags & Service.STOP_FOREGROUND_REMOVE) != 0) {
cancelForegroundNotificationLocked(r);
r.foregroundId = 0;
@@ -1622,6 +1630,105 @@
}
}
+ private void postFgsNotificationLocked(ServiceRecord r) {
+ boolean showNow = !mAm.mConstants.mFlagFgsNotificationDeferralEnabled;
+ if (!showNow) {
+ // Legacy apps' FGS notifications are not deferred unless the relevant
+ // DeviceConfig element has been set
+ showNow = mAm.mConstants.mFlagFgsNotificationDeferralApiGated
+ && r.appInfo.targetSdkVersion < Build.VERSION_CODES.S;
+ }
+ if (!showNow) {
+ // is the notification such that it should show right away?
+ showNow = r.foregroundNoti.shouldShowForegroundImmediately();
+ // or is this an type of FGS that always shows immediately?
+ if (!showNow) {
+ switch (r.foregroundServiceType) {
+ case ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK:
+ case ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL:
+ case ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE:
+ case ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION:
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "FGS " + r
+ + " type gets immediate display");
+ }
+ showNow = true;
+ }
+ }
+ }
+
+ if (showNow) {
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "FGS " + r + " non-deferred notification");
+ }
+ r.postNotification();
+ return;
+ }
+
+ // schedule the actual notification post
+ final int uid = r.appInfo.uid;
+ final long now = SystemClock.uptimeMillis();
+ long when = now + mAm.mConstants.mFgsNotificationDeferralInterval;
+ // If there are already deferred FGS notifications for this app,
+ // inherit that deferred-show timestamp
+ for (int i = 0; i < mPendingFgsNotifications.size(); i++) {
+ final ServiceRecord pending = mPendingFgsNotifications.get(i);
+ if (pending == r) {
+ // Already pending; no need to reschedule
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "FGS " + r
+ + " already pending notification display");
+ }
+ return;
+ }
+ if (uid == pending.appInfo.uid) {
+ when = Math.min(when, pending.fgDisplayTime);
+ }
+ }
+ r.fgDisplayTime = when;
+ mPendingFgsNotifications.add(r);
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "FGS " + r
+ + " notification in " + (when - now) + " ms");
+ }
+ mAm.mHandler.postAtTime(mPostDeferredFGSNotifications, when);
+ }
+
+ private final Runnable mPostDeferredFGSNotifications = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "+++ evaluating deferred FGS notifications +++");
+ }
+ final long now = SystemClock.uptimeMillis();
+ synchronized (mAm) {
+ // post all notifications whose time has come
+ for (int i = mPendingFgsNotifications.size() - 1; i >= 0; i--) {
+ final ServiceRecord r = mPendingFgsNotifications.get(i);
+ if (r.fgDisplayTime <= now) {
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "FGS " + r
+ + " handling deferred notification now");
+ }
+ mPendingFgsNotifications.remove(i);
+ // The service might have been stopped or exited foreground state
+ // in the interval, so we lazy check whether we still need to show
+ // the notification.
+ if (r.isForeground) {
+ r.postNotification();
+ } else if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, " - service no longer running/fg, ignoring");
+ }
+ }
+ }
+ if (DEBUG_FOREGROUND_SERVICE) {
+ Slog.d(TAG_SERVICE, "Done evaluating deferred FGS notifications; "
+ + mPendingFgsNotifications.size() + " remaining");
+ }
+ }
+ }
+ };
+
/** Registers an AppOpCallback for monitoring special AppOps for this foreground service. */
private void registerAppOpCallbackLocked(@NonNull ServiceRecord r) {
if (r.app == null) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 9c4a358..b4e856b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -173,6 +173,27 @@
private static final String KEY_DEFAULT_FGS_STARTS_TEMP_ALLOWLIST_ENABLED =
"default_fgs_starts_temp_allowlist_enabled";
+ /**
+ * Whether FGS notification display is deferred following the transition into
+ * the foreground state. Default behavior is {@code true} unless overridden.
+ */
+ private static final String KEY_DEFERRED_FGS_NOTIFICATIONS_ENABLED =
+ "deferred_fgs_notifications_enabled";
+
+ /** Whether FGS notification deferral applies only to those apps targeting
+ * API version S or higher. Default is {@code true} unless overidden.
+ */
+ private static final String KEY_DEFERRED_FGS_NOTIFICATIONS_API_GATED =
+ "deferred_fgs_notifications_api_gated";
+
+ /**
+ * Time in milliseconds to defer display of FGS notifications following the
+ * transition into the foreground state. Default is 10_000 (ten seconds)
+ * unless overridden.
+ */
+ private static final String KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL =
+ "deferred_fgs_notification_interval";
+
// Maximum number of cached processes we will allow.
public int MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES;
@@ -355,6 +376,19 @@
// DeviceIdleController's Temp AllowList is allowed to bypass the restriction.
volatile boolean mFlagFgsStartTempAllowListEnabled = false;
+ // Whether we defer FGS notifications a few seconds following their transition to
+ // the foreground state. Applies only to S+ apps; enabled by default.
+ volatile boolean mFlagFgsNotificationDeferralEnabled = true;
+
+ // Restrict FGS notification deferral policy to only those apps that target
+ // API version S or higher. Enabled by default; set to "false" to defer FGS
+ // notifications from legacy apps as well.
+ volatile boolean mFlagFgsNotificationDeferralApiGated = true;
+
+ // Time in milliseconds to defer FGS notifications after their transition to
+ // the foreground state.
+ volatile long mFgsNotificationDeferralInterval = 10_000;
+
private final ActivityManagerService mService;
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -509,6 +543,15 @@
case KEY_DEFAULT_FGS_STARTS_TEMP_ALLOWLIST_ENABLED:
updateFgsStartsTempAllowList();
break;
+ case KEY_DEFERRED_FGS_NOTIFICATIONS_ENABLED:
+ updateFgsNotificationDeferralEnable();
+ break;
+ case KEY_DEFERRED_FGS_NOTIFICATIONS_API_GATED:
+ updateFgsNotificationDeferralApiGated();
+ break;
+ case KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL:
+ updateFgsNotificationDeferralInterval();
+ break;
case KEY_OOMADJ_UPDATE_POLICY:
updateOomAdjUpdatePolicy();
break;
@@ -773,6 +816,27 @@
/*defaultValue*/ false);
}
+ private void updateFgsNotificationDeferralEnable() {
+ mFlagFgsNotificationDeferralEnabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_DEFERRED_FGS_NOTIFICATIONS_ENABLED,
+ /*default value*/ true);
+ }
+
+ private void updateFgsNotificationDeferralApiGated() {
+ mFlagFgsNotificationDeferralApiGated = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_DEFERRED_FGS_NOTIFICATIONS_API_GATED,
+ /*default value*/ true);
+ }
+
+ private void updateFgsNotificationDeferralInterval() {
+ mFgsNotificationDeferralInterval = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL,
+ /*default value*/ 10_000L);
+ }
+
private void updateOomAdjUpdatePolicy() {
OOMADJ_UPDATE_QUICK = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 4cdf661..057b007c 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -109,6 +109,7 @@
boolean isForeground; // is service currently in foreground mode?
int foregroundId; // Notification ID of last foreground req.
Notification foregroundNoti; // Notification record of foreground state.
+ long fgDisplayTime; // time at which the FGS notification should become visible
int foregroundServiceType; // foreground service types.
long lastActivity; // last time there was some activity on the service.
long startingBgTimeout; // time at which we scheduled this for a delayed start.