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

Commit 8df95fcc authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Fix various FGS notification deferral issues" into sc-dev am: 2ee596d2 am: 83584688

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14736145

Change-Id: I1ec38feba3e9a4fca745b80ff3285ebe7b721abd
parents 64045859 83584688
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) {
@@ -4016,7 +4165,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);
@@ -6022,7 +6171,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
@@ -16066,6 +16066,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