Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f2c18bbb authored by Christopher Tate's avatar Christopher Tate
Browse files

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
parent 4a14b48d
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -5778,6 +5778,7 @@ package android.app {
    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);
+48 −1
Original line number Diff line number Diff line
@@ -630,10 +630,16 @@ public class Notification implements Parcelable
     */
    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{};

@@ -4380,6 +4386,18 @@ public class Notification implements Parcelable
            return this;
        }

        /**
         * 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.
         *
@@ -6382,6 +6400,35 @@ public class Notification implements Parcelable
        return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
    }

    /**
     * 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
+108 −1
Original line number Diff line number Diff line
@@ -217,6 +217,11 @@ public final class ActiveServices {
     */
    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 @@ public final class ActiveServices {
                        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 @@ public final class ActiveServices {
                    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 @@ public final class ActiveServices {
        }
    }

    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) {
+64 −0
Original line number Diff line number Diff line
@@ -173,6 +173,27 @@ final class ActivityManagerConstants extends ContentObserver {
    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 @@ final class ActivityManagerConstants extends ContentObserver {
    // 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 @@ final class ActivityManagerConstants extends ContentObserver {
                            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 @@ final class ActivityManagerConstants extends ContentObserver {
                /*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,
+1 −0
Original line number Diff line number Diff line
@@ -109,6 +109,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
    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.