From 10a3ccad5369da34230933630e6887bb2f946465 Mon Sep 17 00:00:00 2001 From: Christopher Tate Date: Wed, 28 Apr 2021 10:12:09 -0700 Subject: [PATCH] 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 --- .../android/app/ActivityManagerInternal.java | 35 +++ .../com/android/server/am/ActiveServices.java | 255 ++++++++++++++---- .../server/am/ActivityManagerConstants.java | 2 +- .../server/am/ActivityManagerService.java | 18 ++ .../com/android/server/am/ServiceRecord.java | 1 + .../NotificationManagerInternal.java | 3 + .../NotificationManagerService.java | 63 ++++- .../notification/NotificationRecord.java | 4 + 8 files changed, 315 insertions(+), 66 deletions(-) diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index ab610e4e71c6b..d962fa3bc316a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -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. diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 04083093d1015..8a891c24c4d38 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -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 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) { - // Did the app have another FGS notification deferred recently? - showNow = withinFgsDeferRateLimit(uid, now); + if (!mAm.mConstants.mFlagFgsNotificationDeferralEnabled) { + return true; } - if (!showNow) { - // Legacy apps' FGS notifications are not deferred unless the relevant + + // 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? + if (withinFgsDeferRateLimit(r, now)) { + return true; + } + + 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; + } } - 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) { + + // did we already show it? + if (r.mFgsNotificationShown) { + return true; + } + + // has the app forced deferral? + if (!r.foregroundNoti.isForegroundDisplayForceDeferred()) { + // is the notification such that it should show right away? + if (r.foregroundNoti.shouldShowForegroundImmediately()) { + if (DEBUG_FOREGROUND_SERVICE) { Slog.d(TAG_SERVICE, "FGS " + r + " notification policy says show immediately"); } - // 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; - } - } - } else { + return true; + } + + // or is this an type of FGS that always shows immediately? + if ((r.foregroundServiceType & FGS_IMMEDIATE_DISPLAY_MASK) != 0) { if (DEBUG_FOREGROUND_SERVICE) { - Slog.d(TAG_SERVICE, "FGS " + r + " notification is app deferred"); + Slog.d(TAG_SERVICE, "FGS " + r + + " type gets immediate display"); } + return true; } - } - if (showNow) { + // fall through to return false: no policy dictates immediate display + } else { if (DEBUG_FOREGROUND_SERVICE) { - Slog.d(TAG_SERVICE, "FGS " + r + " non-deferred notification"); + Slog.d(TAG_SERVICE, "FGS " + r + " notification is app deferred"); } - r.postNotification(); - r.mFgsNotificationDeferred = false; - r.mFgsNotificationShown = true; - return; + // fall through to return false } - r.mFgsNotificationDeferred = true; - r.mFgsNotificationShown = false; + return 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,9 +2177,16 @@ public final class ActiveServices { if (r.isForeground && r.app != null) { r.postNotification(); r.mFgsNotificationShown = true; - } else if (DEBUG_FOREGROUND_SERVICE) { - Slog.d(TAG_SERVICE, " - service no longer running/fg, ignoring"); + } 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) { @@ -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; diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 0fff8be705741..0d19efc207852 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -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; /** diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b44fe9fdaad14..321b8349f0175 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -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) { diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index fd59e852ddd7c..5a9491f9d7cb5 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 @@ 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 diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java index dc9839c6da0ef..0528b95d1a6e6 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java +++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java @@ -30,6 +30,9 @@ public interface NotificationManagerInternal { void cancelNotification(String pkg, String basePkg, int callingUid, int callingPid, String tag, int id, int userId); + /** is the given notification currently showing? */ + boolean isNotificationShown(String pkg, String tag, int notificationId, int userId); + void removeForegroundServiceFlagFromNotification(String pkg, int notificationId, int userId); void onConversationRemoved(String pkg, int uid, Set shortcuts); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index f0e693976faad..d7ccde8722736 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -130,6 +130,7 @@ import android.annotation.UserIdInt; import android.annotation.WorkerThread; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityManagerInternal.ServiceNotificationPolicy; import android.app.AlarmManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -3078,6 +3079,20 @@ public class NotificationManagerService extends SystemService { } } + protected void maybeReportForegroundServiceUpdate(final NotificationRecord r) { + if (r.isForegroundService()) { + // snapshot live state for the asynchronous operation + final StatusBarNotification sbn = r.getSbn(); + final Notification notification = sbn.getNotification(); + final int id = sbn.getId(); + final String pkg = sbn.getPackageName(); + final int userId = sbn.getUser().getIdentifier(); + mHandler.post(() -> { + mAmi.onForegroundServiceNotificationUpdate(notification, id, pkg, userId); + }); + } + } + private String getHistoryTitle(Notification n) { CharSequence title = null; if (n.extras != null) { @@ -5064,7 +5079,7 @@ public class NotificationManagerService extends SystemService { } @Override - public boolean isNotificationPolicyAccessGrantedForPackage(String pkg) {; + public boolean isNotificationPolicyAccessGrantedForPackage(String pkg) { enforceSystemOrSystemUIOrSamePackage(pkg, "request policy access status for another package"); return checkPolicyAccess(pkg); @@ -5719,9 +5734,7 @@ public class NotificationManagerService extends SystemService { summaryNotification.extras.putAll(extras); Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg); if (appIntent != null) { - final ActivityManagerInternal ami = LocalServices - .getService(ActivityManagerInternal.class); - summaryNotification.contentIntent = ami.getPendingIntentActivityAsApp( + summaryNotification.contentIntent = mAmi.getPendingIntentActivityAsApp( 0, appIntent, PendingIntent.FLAG_IMMUTABLE, null, pkg, appInfo.uid); } @@ -5772,7 +5785,7 @@ public class NotificationManagerService extends SystemService { return "callState"; } return null; - }; + } private void dumpJson(PrintWriter pw, @NonNull DumpFilter filter) { JSONObject dump = new JSONObject(); @@ -6052,6 +6065,11 @@ public class NotificationManagerService extends SystemService { cancelNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, userId); } + @Override + public boolean isNotificationShown(String pkg, String tag, int notificationId, int userId) { + return isNotificationShownInternal(pkg, tag, notificationId, userId); + } + @Override public void removeForegroundServiceFlagFromNotification(String pkg, int notificationId, int userId) { @@ -6135,16 +6153,22 @@ public class NotificationManagerService extends SystemService { mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null); } + boolean isNotificationShownInternal(String pkg, String tag, int notificationId, int userId) { + synchronized (mNotificationLock) { + return findNotificationLocked(pkg, tag, notificationId, userId) != null; + } + } + void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int incomingUserId) { enqueueNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, notification, - incomingUserId, false); + incomingUserId, false); } void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, - final int callingPid, final String tag, final int id, final Notification notification, - int incomingUserId, boolean postSilently) { + final int callingPid, final String tag, final int id, final Notification notification, + int incomingUserId, boolean postSilently) { if (DBG) { Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); @@ -6179,6 +6203,22 @@ public class NotificationManagerService extends SystemService { return; } + // Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE, + // but it's also possible that the app has called notify() with an update to an + // FGS notification that hasn't yet been displayed. Make sure we check for any + // FGS-related situation up front, outside of any locks so it's safe to call into + // the Activity Manager. + final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification( + notification, id, pkg, userId); + if (policy == ServiceNotificationPolicy.UPDATE_ONLY) { + // Proceed if the notification is already showing/known, otherwise ignore + // because the service lifecycle logic has retained responsibility for its + // handling. + if (!isNotificationShownInternal(pkg, tag, id, userId)) { + return; + } + } + mUsageStats.registerEnqueuedByApp(pkg); final StatusBarNotification n = new StatusBarNotification( @@ -6281,19 +6321,17 @@ public class NotificationManagerService extends SystemService { if (notification.allPendingIntents != null) { final int intentCount = notification.allPendingIntents.size(); if (intentCount > 0) { - final ActivityManagerInternal am = LocalServices - .getService(ActivityManagerInternal.class); final long duration = LocalServices.getService( DeviceIdleInternal.class).getNotificationAllowlistDuration(); for (int i = 0; i < intentCount; i++) { PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i); if (pendingIntent != null) { - am.setPendingIntentAllowlistDuration(pendingIntent.getTarget(), + mAmi.setPendingIntentAllowlistDuration(pendingIntent.getTarget(), ALLOWLIST_TOKEN, duration, TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_NOTIFICATION_SERVICE, "NotificationManagerService"); - am.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(), + mAmi.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(), ALLOWLIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER)); } @@ -7102,6 +7140,7 @@ public class NotificationManagerService extends SystemService { maybeRecordInterruptionLocked(r); maybeRegisterMessageSent(r); + maybeReportForegroundServiceUpdate(r); // Log event to statsd mNotificationRecordLogger.maybeLogNotificationPosted(r, old, position, diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index e875065441889..6504ca683eb3f 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -877,6 +877,10 @@ public final class NotificationRecord { return mHidden; } + public boolean isForegroundService() { + return 0 != (getFlags() & Notification.FLAG_FOREGROUND_SERVICE); + } + /** * Override of all alerting information on the channel and notification. Used when notifications * are reposted in response to direct user action and thus don't need to alert.