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

Commit 10a3ccad authored by Christopher Tate's avatar Christopher Tate
Browse files

Fix various FGS notification deferral issues

A number of scenarios involving both FGS and non-FGS notification
operations were not yet working as intended.  This CL lands the
following behaviors:

* notify() to update the notification content during an FGS's
notification deferral period now respects the new Notification's
deferral policy rather than forcing immediate display.  An app can now
choose whether to force immediate display, or to have the new content be
what eventually appears at the end of the deferral period, by using the
same builder API as controls the deferral policy for startForeground().

* If an app posts a notification via notify() then adopts that
notification with new content (i.e. uses the same notification ID) in a
call to startForeground(), there is now no deferral: the existing
notification is always updated immediately, regardless of the
notification's or the service's configuration viz deferral policy.

* Fixed a latent bug when the app called notify() to update the
notification after startForeground(), then the service was killed for
memory and relaunched.  The notification might have been reposted with
stale content.

* Adjusted the metrics handling to more accurately express "was display
of the notification content for this transition to the FGS mode
deferred?"

Bug: 178406514
Bug: 185771298
Bug: 187860135
Test: atest CtsAppTestCases:android.app.cts.ServiceTest
Test: atest CtsAppTestCases:NotificationManagerTest
Change-Id: Ieb62f7195eb619c1769e3c1bd518cb6a7b6e4b7c
parent 7e5a53a0
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
@@ -48,6 +48,23 @@ import java.util.Set;
 */
public abstract class ActivityManagerInternal {

    public enum ServiceNotificationPolicy {
        /**
         * The Notification is not associated with any foreground service.
         */
        NOT_FOREGROUND_SERVICE,
        /**
         * The Notification is associated with a foreground service, but the
         * notification system should handle it just like non-FGS notifications.
         */
        SHOW_IMMEDIATELY,
        /**
         * The Notification is associated with a foreground service, and the
         * notification system should ignore it unless it has already been shown (in
         * which case it should be used to update the currently displayed UI).
         */
        UPDATE_ONLY
    }

    // Access modes for handleIncomingUser.
    public static final int ALLOW_NON_FULL = 0;
@@ -457,6 +474,24 @@ public abstract class ActivityManagerInternal {
    public abstract boolean hasForegroundServiceNotification(String pkg, @UserIdInt int userId,
            String channelId);

    /**
     * Tell the service lifecycle logic that the given Notification content is now
     * canonical for any foreground-service visibility policy purposes.
     *
     * Returns a description of any FGs-related policy around the given Notification:
     * not associated with an FGS; ensure display; or only update if already displayed.
     */
    public abstract ServiceNotificationPolicy applyForegroundServiceNotification(
            Notification notification, int id, String pkg, @UserIdInt int userId);

    /**
     * Callback from the notification subsystem that the given FGS notification has
     * been shown or updated.  This can happen after either Service.startForeground()
     * or NotificationManager.notify().
     */
    public abstract void onForegroundServiceNotificationUpdate(Notification notification,
            int id, String pkg, @UserIdInt int userId);

    /**
     * If the given app has any FGSs whose notifications are in the given channel,
     * stop them.
+203 −54
Original line number Diff line number Diff line
@@ -79,8 +79,10 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UptimeMillisLong;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerInternal.ServiceNotificationPolicy;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.AppOpsManager;
@@ -213,6 +215,9 @@ public final class ActiveServices {
    // at the same time.
    final int mMaxStartingBackground;

    /**
     * Master service bookkeeping, keyed by user number.
     */
    final SparseArray<ServiceMap> mServiceMap = new SparseArray<>();

    /**
@@ -1811,7 +1816,7 @@ public final class ActiveServices {
                        showFgsBgRestrictedNotificationLocked(r);
                        updateServiceForegroundLocked(psr, true);
                        ignoreForeground = true;
                        logForegroundServiceStateChanged(r,
                        logFGSStateChangeLocked(r,
                                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
                                0);
                        if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID,
@@ -1859,6 +1864,7 @@ public final class ActiveServices {
                            active.mNumActive++;
                        }
                        r.isForeground = true;
                        r.mLogEntering = true;
                        enterForeground = true;
                        r.mStartForegroundCount++;
                        r.mFgsEnterTime = SystemClock.uptimeMillis();
@@ -1881,14 +1887,7 @@ public final class ActiveServices {
                    }
                    // Even if the service is already a FGS, we need to update the notification,
                    // so we need to call it again.
                    postFgsNotificationLocked(r);
                    if (enterForeground) {
                        // Because we want to log what's updated in postFgsNotificationLocked(),
                        // this must be called after postFgsNotificationLocked().
                        logForegroundServiceStateChanged(r,
                                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
                                0);
                    }
                    r.postNotification();
                    if (r.app != null) {
                        updateServiceForegroundLocked(psr, true);
                    }
@@ -1937,7 +1936,7 @@ public final class ActiveServices {
                        AppOpsManager.getToken(mAm.mAppOpsService),
                        AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null);
                unregisterAppOpCallbackLocked(r);
                logForegroundServiceStateChanged(r,
                logFGSStateChangeLocked(r,
                        FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                        r.mFgsExitTime > r.mFgsEnterTime
                                ? (int)(r.mFgsExitTime - r.mFgsEnterTime) : 0);
@@ -1964,7 +1963,18 @@ public final class ActiveServices {
        }
    }

    private boolean withinFgsDeferRateLimit(final int uid, final long now) {
    private boolean withinFgsDeferRateLimit(ServiceRecord sr, final long now) {
        // If we're still within the service's deferral period, then by definition
        // deferral is not rate limited.
        if (now < sr.fgDisplayTime) {
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG_SERVICE, "FGS transition for " + sr
                        + " within deferral period, no rate limit applied");
            }
            return false;
        }

        final int uid = sr.appInfo.uid;
        final long eligible = mFgsDeferralEligible.get(uid, 0L);
        if (DEBUG_FOREGROUND_SERVICE) {
            if (now < eligible) {
@@ -1975,62 +1985,137 @@ public final class ActiveServices {
        return now < eligible;
    }

    // TODO: remove as part of fixing b/173627642
    ServiceNotificationPolicy applyForegroundServiceNotificationLocked(Notification notification,
            final int id, final String pkg, final int userId) {
        if (DEBUG_FOREGROUND_SERVICE) {
            Slog.d(TAG_SERVICE, "Evaluating FGS policy for id=" + id
                    + " pkg=" + pkg + " not=" + notification);
        }
        // Is there an FGS using this notification?
        final ServiceMap smap = mServiceMap.get(userId);
        if (smap == null) {
            // No services in this user at all
            return ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE;
        }

        for (int i = 0; i < smap.mServicesByInstanceName.size(); i++) {
            final ServiceRecord sr = smap.mServicesByInstanceName.valueAt(i);
            if (id != sr.foregroundId || !pkg.equals(sr.appInfo.packageName)) {
                // Not this one; keep looking
                continue;
            }

            // Found; it is associated with an FGS.  Make sure that it's flagged:
            // it may have entered the bookkeeping outside of Service-related
            // APIs.  We also make sure to take this latest Notification as
            // the content to be shown (immediately or eventually).
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG_SERVICE, "   FOUND: notification is for " + sr);
            }
            notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
            sr.foregroundNoti = notification;

            // ...and determine immediate vs deferred display policy for it
            final boolean showNow = shouldShowFgsNotificationLocked(sr);
            if (showNow) {
                if (DEBUG_FOREGROUND_SERVICE) {
                    Slog.d(TAG_SERVICE, "   Showing immediately due to policy");
                }
                sr.mFgsNotificationDeferred = false;
                return ServiceNotificationPolicy.SHOW_IMMEDIATELY;
            }

            // Deferring - kick off the timer if necessary, and tell the caller
            // that it's to be shown only if it's an update to already-
            // visible content (e.g. if it's an FGS adopting a
            // previously-posted Notification).
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG_SERVICE, "   Deferring / update-only");
            }
            startFgsDeferralTimerLocked(sr);
            return ServiceNotificationPolicy.UPDATE_ONLY;
        }

        // None of the services in this user are FGSs
        return ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE;
    }

    // No legacy-app behavior skew intended but there's a runtime E-stop if a need
    // arises, so note that
    @SuppressWarnings("AndroidFrameworkCompatChange")
    private void postFgsNotificationLocked(ServiceRecord r) {
        final int uid = r.appInfo.uid;
    private boolean shouldShowFgsNotificationLocked(ServiceRecord r) {
        final long now = SystemClock.uptimeMillis();
        final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);

        // Is the behavior enabled at all?
        boolean showNow = !mAm.mConstants.mFlagFgsNotificationDeferralEnabled;
        if (!showNow) {
        if (!mAm.mConstants.mFlagFgsNotificationDeferralEnabled) {
            return true;
        }

        // Has this service's deferral timer expired?
        if (r.mFgsNotificationDeferred && now >= r.fgDisplayTime) {
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG, "FGS reached end of deferral period: " + r);
            }
            return true;
        }

        // Did the app have another FGS notification deferred recently?
            showNow = withinFgsDeferRateLimit(uid, now);
        if (withinFgsDeferRateLimit(r, now)) {
            return true;
        }
        if (!showNow) {
            // Legacy apps' FGS notifications are not deferred unless the relevant

        if (mAm.mConstants.mFlagFgsNotificationDeferralApiGated) {
            // Legacy apps' FGS notifications are also deferred unless the relevant
            // DeviceConfig element has been set
            showNow = isLegacyApp && mAm.mConstants.mFlagFgsNotificationDeferralApiGated;
            final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);
            if (isLegacyApp) {
                return true;
            }
        }

        // did we already show it?
        if (r.mFgsNotificationShown) {
            return true;
        }
        if (!showNow) {

        // has the app forced deferral?
        if (!r.foregroundNoti.isForegroundDisplayForceDeferred()) {
            // is the notification such that it should show right away?
                showNow = r.foregroundNoti.shouldShowForegroundImmediately();
                if (DEBUG_FOREGROUND_SERVICE && showNow) {
            if (r.foregroundNoti.shouldShowForegroundImmediately()) {
                if (DEBUG_FOREGROUND_SERVICE) {
                    Slog.d(TAG_SERVICE, "FGS " + r
                            + " notification policy says show immediately");
                }
                return true;
            }

            // or is this an type of FGS that always shows immediately?
                if (!showNow) {
            if ((r.foregroundServiceType & FGS_IMMEDIATE_DISPLAY_MASK) != 0) {
                if (DEBUG_FOREGROUND_SERVICE) {
                    Slog.d(TAG_SERVICE, "FGS " + r
                            + " type gets immediate display");
                }
                        showNow = true;
                    }
                return true;
            }

            // fall through to return false: no policy dictates immediate display
        } else {
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG_SERVICE, "FGS " + r + " notification is app deferred");
            }
            }
            // fall through to return false
        }

        if (showNow) {
            if (DEBUG_FOREGROUND_SERVICE) {
                Slog.d(TAG_SERVICE, "FGS " + r + " non-deferred notification");
            }
            r.postNotification();
            r.mFgsNotificationDeferred = false;
            r.mFgsNotificationShown = true;
            return;
        return false;
    }

        r.mFgsNotificationDeferred = true;
        r.mFgsNotificationShown = false;
    // Target SDK consultation here is strictly for logging purposes, not
    // behavioral variation.
    @SuppressWarnings("AndroidFrameworkCompatChange")
    private void startFgsDeferralTimerLocked(ServiceRecord r) {
        final long now = SystemClock.uptimeMillis();
        final int uid = r.appInfo.uid;

        // schedule the actual notification post
        long when = now + mAm.mConstants.mFgsNotificationDeferralInterval;
        // If there are already deferred FGS notifications for this app,
@@ -2053,11 +2138,14 @@ public final class ActiveServices {
        final long nextEligible = when + mAm.mConstants.mFgsNotificationDeferralExclusionTime;
        mFgsDeferralEligible.put(uid, nextEligible);
        r.fgDisplayTime = when;
        r.mFgsNotificationDeferred = true;
        r.mFgsNotificationShown = false;
        mPendingFgsNotifications.add(r);
        if (DEBUG_FOREGROUND_SERVICE) {
            Slog.d(TAG_SERVICE, "FGS " + r
                    + " notification in " + (when - now) + " ms");
        }
        final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);
        if (isLegacyApp) {
            Slog.i(TAG_SERVICE, "Deferring FGS notification in legacy app "
                    + r.appInfo.packageName + "/" + UserHandle.formatUid(r.appInfo.uid)
@@ -2089,10 +2177,17 @@ public final class ActiveServices {
                        if (r.isForeground && r.app != null) {
                            r.postNotification();
                            r.mFgsNotificationShown = true;
                        } else if (DEBUG_FOREGROUND_SERVICE) {
                        } else {
                            if (DEBUG_FOREGROUND_SERVICE) {
                                Slog.d(TAG_SERVICE, "  - service no longer running/fg, ignoring");
                            }
                        }
                        // Regardless of whether we needed to post the notification or the
                        // service is no longer running, we may not have logged its FGS
                        // transition yet depending on the timing and API sequence that led
                        // to this point - so make sure to do so.
                        maybeLogFGSStateEnteredLocked(r);
                    }
                }
                if (DEBUG_FOREGROUND_SERVICE) {
                    Slog.d(TAG_SERVICE, "Done evaluating deferred FGS notifications; "
@@ -2102,6 +2197,60 @@ public final class ActiveServices {
        }
    };

    private void maybeLogFGSStateEnteredLocked(ServiceRecord r) {
        if (r.mLogEntering) {
            logFGSStateChangeLocked(r,
                    FrameworkStatsLog
                            .FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
                    0);
            r.mLogEntering = false;
        }
    }

    /**
     * Callback from NotificationManagerService whenever it posts a notification
     * associated with a foreground service.  This is the unified handling point
     * for the disjoint code flows that affect an FGS's notifiation content and
     * visibility, starting with both Service.startForeground() and
     * NotificationManager.notify().
     */
    public void onForegroundServiceNotificationUpdateLocked(Notification notification,
            final int id, final String pkg, @UserIdInt final int userId) {
        // If this happens to be a Notification for an FGS still in its deferral period,
        // drop the deferral and make sure our content bookkeeping is up to date.
        for (int i = mPendingFgsNotifications.size() - 1; i >= 0; i--) {
            final ServiceRecord sr = mPendingFgsNotifications.get(i);
            if (userId == sr.userId
                    && id == sr.foregroundId
                    && sr.appInfo.packageName.equals(pkg)) {
                if (DEBUG_FOREGROUND_SERVICE) {
                    Slog.d(TAG_SERVICE, "Notification shown; canceling deferral of "
                            + sr);
                }
                maybeLogFGSStateEnteredLocked(sr);
                sr.mFgsNotificationShown = true;
                sr.mFgsNotificationDeferred = false;
                mPendingFgsNotifications.remove(i);
            }
        }
        // And make sure to retain the latest notification content for the FGS
        ServiceMap smap = mServiceMap.get(userId);
        if (smap != null) {
            for (int i = 0; i < smap.mServicesByInstanceName.size(); i++) {
                final ServiceRecord sr = smap.mServicesByInstanceName.valueAt(i);
                if (sr.isForeground
                        && id == sr.foregroundId
                        && sr.appInfo.packageName.equals(pkg)) {
                    if (DEBUG_FOREGROUND_SERVICE) {
                        Slog.d(TAG_SERVICE, "Recording shown notification for "
                                + sr);
                    }
                    sr.foregroundNoti = notification;
                }
            }
        }
    }

    /** Registers an AppOpCallback for monitoring special AppOps for this foreground service. */
    private void registerAppOpCallbackLocked(@NonNull ServiceRecord r) {
        if (r.app == null) {
@@ -4020,7 +4169,7 @@ public final class ActiveServices {
                    AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null);
            unregisterAppOpCallbackLocked(r);
            r.mFgsExitTime = SystemClock.uptimeMillis();
            logForegroundServiceStateChanged(r,
            logFGSStateChangeLocked(r,
                    FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                    r.mFgsExitTime > r.mFgsEnterTime
                            ? (int)(r.mFgsExitTime - r.mFgsEnterTime) : 0);
@@ -6031,7 +6180,7 @@ public final class ActiveServices {
     * @param state one of ENTER/EXIT/DENIED event.
     * @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state.
     */
    private void logForegroundServiceStateChanged(ServiceRecord r, int state, int durationMs) {
    private void logFGSStateChangeLocked(ServiceRecord r, int state, int durationMs) {
        if (!ActivityManagerUtils.shouldSamplePackageForAtom(
                r.packageName, mAm.mConstants.mFgsAtomSampleRate)) {
            return;
+1 −1
Original line number Diff line number Diff line
@@ -453,7 +453,7 @@ final class ActivityManagerConstants extends ContentObserver {
    volatile long mFgsNotificationDeferralInterval = 10_000;

    // Rate limit: minimum time after an app's FGS notification is deferred
    // before another FGS notifiction from that app can be deferred.
    // before another FGS notification from that app can be deferred.
    volatile long mFgsNotificationDeferralExclusionTime = 2 * 60 * 1000L;

    /**
+18 −0
Original line number Diff line number Diff line
@@ -16054,6 +16054,24 @@ public class ActivityManagerService extends IActivityManager.Stub
            }
        }
        @Override
        public ServiceNotificationPolicy applyForegroundServiceNotification(
                Notification notification, int id, String pkg, int userId) {
            synchronized (ActivityManagerService.this) {
                return mServices.applyForegroundServiceNotificationLocked(notification,
                        id, pkg, userId);
            }
        }
        @Override
        public void onForegroundServiceNotificationUpdate(Notification notification,
                int id, String pkg, @UserIdInt int userId) {
            synchronized (ActivityManagerService.this) {
                mServices.onForegroundServiceNotificationUpdateLocked(notification,
                        id, pkg, userId);
            }
        }
        @Override
        public void stopForegroundServicesForChannel(String pkg, int userId,
                String channelId) {
+1 −0
Original line number Diff line number Diff line
@@ -109,6 +109,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
    boolean fgWaiting;      // is a timeout for going foreground already scheduled?
    boolean isNotAppComponentUsage; // is service binding not considered component/package usage?
    boolean isForeground;   // is service currently in foreground mode?
    boolean mLogEntering;    // need to report fgs transition once deferral policy is known
    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
Loading