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.