Toast on broadcast receiver notification trampolines

Show a warning toast when an app starts an activity inside a broadcast
receiver in response to a notification or notification action click.
This is to gather dogfood feedback in preparation for blocking such
activity starts. See go/notification-trampolines for more info. For now,
only taking care of broadcast receivers, the services case will be in a
future CL.

The toast is only shown once per package. This is to avoid annoying
dogfooders.

The actual block will be gated on targetSdk, however since very few
apps (any?) will be targeting the latest SDK on dogfood and we are still
allowing the activity start, we're going to show a toast regardless of
targetSdk to collect more feedback.

A side-effect of the way we implemented the rule that allowed
notification trampolines in BAL is that in the case that the user clicks
on a broadcast/service-trampoline notification of an app that also
happens to be in the foreground (on top for example) we can't
differentiate if an activity start came from the broadcast/service or
from the activity itself (or some other part of the app). Because of
this, in this case, we won't show the warning toast (and I'd say we
might not block the launch in the future, but this discussion can wait).

Implementation:

We determine at PendingIntent fire time if the receiver is allowed to
start activities. However, we pass a boolean to represent the grant to
start activities and this boolean also takes into account another case
that allows such starts. To correctly identify at activity start time
that it was allowed due to notification-click scenario we pass in the
token that was embeded in the PendingIntent (and was validated in the
PendingIntentRecord) by notification manager.

The set of components in ProcessRecord becomes a map to contain the
originating tokens. Since now WindowProcessController needs to have the
token to trace back the grant, I've replaced the boolean flag with the
map from ProcessRecord. We move the grant rule to the bottom of the
method areBackgroundActivityStartsAllowed() to make sure the start was
allowed exclusively due to notification-click interaction.

We then let notification manager register a callback with activity task
manager that will be called whenever a background activity start is
allowed exclusively due to a token provided in that callback. In that
callback we show the warning toast.

Test: atest BroadcastRecordTest BackgroundActivityLaunchTest
      ActivityStartControllerTests ActivityStarterTests
      RecentsAnimationTest WindowProcessControllerMapTests
      WindowProcessControllerMapTests
Test: Posted a broadcast-trampoline notification, clicked on it and
      observed the toast was shown.
Change-Id: Ica479d7d2f6b5f2ddaac4c59e12d0b25cd637717
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 9e15c1f..ef30cb4 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -318,12 +318,14 @@
             int uid, int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
             IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
             String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-            @UserIdInt int userId, boolean allowBackgroundActivityStarts);
+            @UserIdInt int userId, boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken);
 
     public abstract ComponentName startServiceInPackage(int uid, Intent service,
             String resolvedType, boolean fgRequired, String callingPackage,
             @Nullable String callingFeatureId, @UserIdInt int userId,
-            boolean allowBackgroundActivityStarts) throws TransactionTooLargeException;
+            boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken) throws TransactionTooLargeException;
 
     public abstract void disconnectActivityFromServices(Object connectionHolder);
     public abstract void cleanUpServices(@UserIdInt int userId, ComponentName component,
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index bd51c7a..33a92e6 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4881,12 +4881,9 @@
             if (instr != null && instr.mHasBackgroundActivityStartsPermission) {
                 return true;
             }
-        }
-
-        final boolean hasAllowBackgroundActivityStartsToken = r.app != null
-                ? !r.app.mAllowBackgroundActivityStartsTokens.isEmpty() : false;
-        if (hasAllowBackgroundActivityStartsToken) {
-            return true;
+            if (r.app.areBackgroundActivityStartsAllowedByToken()) {
+                return true;
+            }
         }
 
         if (mAm.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2b6da4c..32f7d1d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4303,11 +4303,11 @@
                         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
                         broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
                                 null, null, 0, null, null, permission.ACCESS_INSTANT_APPS, null,
-                                false, false, resolvedUserId, false);
+                                false, false, resolvedUserId, false, null);
                     } else {
                         broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
                                 null, null, 0, null, null, null, null, false, false, resolvedUserId,
-                                false);
+                                false, null);
                     }
 
                     if (observer != null) {
@@ -15691,7 +15691,7 @@
                     BroadcastQueue queue = broadcastQueueForIntent(intent);
                     BroadcastRecord r = new BroadcastRecord(queue, intent, null,
                             null, null, -1, -1, false, null, null, OP_NONE, null, receivers,
-                            null, 0, null, null, false, true, true, -1, false,
+                            null, 0, null, null, false, true, true, -1, false, null,
                             false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */);
                     queue.enqueueParallelBroadcastLocked(r);
                     queue.scheduleBroadcastsLocked();
@@ -15938,7 +15938,7 @@
                 resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions,
                 appOp, bOptions, ordered, sticky, callingPid, callingUid, realCallingUid,
                 realCallingPid, userId, false /* allowBackgroundActivityStarts */,
-                null /*broadcastWhitelist*/);
+                null /* tokenNeededForBackgroundActivityStarts */, null /* broadcastWhitelist */);
     }
 
     @GuardedBy("this")
@@ -15948,6 +15948,7 @@
             Bundle resultExtras, String[] requiredPermissions, int appOp, Bundle bOptions,
             boolean ordered, boolean sticky, int callingPid, int callingUid, int realCallingUid,
             int realCallingPid, int userId, boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken,
             @Nullable int[] broadcastWhitelist) {
         intent = new Intent(intent);
 
@@ -16038,6 +16039,8 @@
                     throw new SecurityException(msg);
                 } else {
                     allowBackgroundActivityStarts = true;
+                    // We set the token to null since if it wasn't for it we'd allow anyway here
+                    backgroundActivityStartsToken = null;
                 }
             }
         }
@@ -16506,7 +16509,8 @@
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, appOp, brOptions, registeredReceivers, resultTo,
                     resultCode, resultData, resultExtras, ordered, sticky, false, userId,
-                    allowBackgroundActivityStarts, timeoutExempt);
+                    allowBackgroundActivityStarts, backgroundActivityStartsToken,
+                    timeoutExempt);
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r);
             final boolean replaced = replacePending
                     && (queue.replaceParallelBroadcastLocked(r) != null);
@@ -16603,7 +16607,8 @@
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, appOp, brOptions, receivers, resultTo, resultCode,
                     resultData, resultExtras, ordered, sticky, false, userId,
-                    allowBackgroundActivityStarts, timeoutExempt);
+                    allowBackgroundActivityStarts, backgroundActivityStartsToken,
+                    timeoutExempt);
 
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
 
@@ -16763,7 +16768,8 @@
             int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
             IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
             String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-            int userId, boolean allowBackgroundActivityStarts) {
+            int userId, boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken) {
         synchronized(this) {
             intent = verifyBroadcastLocked(intent);
 
@@ -16775,6 +16781,7 @@
                         resultTo, resultCode, resultData, resultExtras, requiredPermissions,
                         OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid,
                         realCallingPid, userId, allowBackgroundActivityStarts,
+                        backgroundActivityStartsToken,
                         null /*broadcastWhitelist*/);
             } finally {
                 Binder.restoreCallingIdentity(origId);
@@ -19444,12 +19451,14 @@
                 int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
                 IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
                 String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-                int userId, boolean allowBackgroundActivityStarts) {
+                int userId, boolean allowBackgroundActivityStarts,
+                @Nullable IBinder backgroundActivityStartsToken) {
             synchronized (ActivityManagerService.this) {
                 return ActivityManagerService.this.broadcastIntentInPackage(packageName, featureId,
                         uid, realCallingUid, realCallingPid, intent, resolvedType, resultTo,
                         resultCode, resultData, resultExtras, requiredPermission, bOptions,
-                        serialized, sticky, userId, allowBackgroundActivityStarts);
+                        serialized, sticky, userId, allowBackgroundActivityStarts,
+                        backgroundActivityStartsToken);
             }
         }
 
@@ -19471,6 +19480,7 @@
                             null /*resultExtras*/, requiredPermissions, AppOpsManager.OP_NONE,
                             null /*options*/, serialized, false /*sticky*/, callingPid, callingUid,
                             callingUid, callingPid, userId, false /*allowBackgroundStarts*/,
+                            null /*tokenNeededForBackgroundActivityStarts*/,
                             appIdWhitelist);
                 } finally {
                     Binder.restoreCallingIdentity(origId);
@@ -19482,7 +19492,8 @@
         @Override
         public ComponentName startServiceInPackage(int uid, Intent service, String resolvedType,
                 boolean fgRequired, String callingPackage, @Nullable String callingFeatureId,
-                int userId, boolean allowBackgroundActivityStarts)
+                int userId, boolean allowBackgroundActivityStarts,
+                @Nullable IBinder backgroundActivityStartsToken)
                 throws TransactionTooLargeException {
             synchronized(ActivityManagerService.this) {
                 if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 1fa62c6..12937b9 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -1677,7 +1677,7 @@
         // that request - we don't want the token to be swept from under our feet...
         mHandler.removeCallbacksAndMessages(msgToken);
         // ...then add the token
-        proc.addAllowBackgroundActivityStartsToken(r);
+        proc.addAllowBackgroundActivityStartsToken(r, r.mBackgroundActivityStartsToken);
     }
 
     final void setBroadcastTimeoutLocked(long timeoutTime) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 40743b8..198ba34 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -92,6 +92,9 @@
     // if set to true, app's process will be temporarily allowed to start activities from background
     // for the duration of the broadcast dispatch
     final boolean allowBackgroundActivityStarts;
+    // token used to trace back the grant for activity starts, optional
+    @Nullable
+    final IBinder mBackgroundActivityStartsToken;
 
     static final int IDLE = 0;
     static final int APP_RECEIVE = 1;
@@ -240,7 +243,8 @@
             String[] _requiredPermissions, int _appOp, BroadcastOptions _options, List _receivers,
             IIntentReceiver _resultTo, int _resultCode, String _resultData, Bundle _resultExtras,
             boolean _serialized, boolean _sticky, boolean _initialSticky, int _userId,
-            boolean _allowBackgroundActivityStarts, boolean _timeoutExempt) {
+            boolean allowBackgroundActivityStarts, @Nullable IBinder backgroundActivityStartsToken,
+            boolean timeoutExempt) {
         if (_intent == null) {
             throw new NullPointerException("Can't construct with a null intent");
         }
@@ -270,8 +274,9 @@
         userId = _userId;
         nextReceiver = 0;
         state = IDLE;
-        allowBackgroundActivityStarts = _allowBackgroundActivityStarts;
-        timeoutExempt = _timeoutExempt;
+        this.allowBackgroundActivityStarts = allowBackgroundActivityStarts;
+        mBackgroundActivityStartsToken = backgroundActivityStartsToken;
+        this.timeoutExempt = timeoutExempt;
     }
 
     /**
@@ -317,6 +322,7 @@
         manifestSkipCount = from.manifestSkipCount;
         queue = from.queue;
         allowBackgroundActivityStarts = from.allowBackgroundActivityStarts;
+        mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken;
         timeoutExempt = from.timeoutExempt;
     }
 
@@ -352,7 +358,7 @@
                 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                 requiredPermissions, appOp, options, splitReceivers, resultTo, resultCode,
                 resultData, resultExtras, ordered, sticky, initialSticky, userId,
-                allowBackgroundActivityStarts, timeoutExempt);
+                allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt);
 
         split.splitToken = this.splitToken;
         return split;
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 1997dbd..fbfed34 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -437,15 +437,17 @@
                     break;
                 case ActivityManager.INTENT_SENDER_BROADCAST:
                     try {
+                        final boolean allowedByToken =
+                                mAllowBgActivityStartsForBroadcastSender.contains(whitelistToken);
+                        final IBinder bgStartsToken = (allowedByToken) ? whitelistToken : null;
+
                         // If a completion callback has been requested, require
                         // that the broadcast be delivered synchronously
                         int sent = controller.mAmInternal.broadcastIntentInPackage(key.packageName,
                                 key.featureId, uid, callingUid, callingPid, finalIntent,
                                 resolvedType, finishedReceiver, code, null, null,
                                 requiredPermission, options, (finishedReceiver != null), false,
-                                userId,
-                                mAllowBgActivityStartsForBroadcastSender.contains(whitelistToken)
-                                        || allowTrampoline);
+                                userId, allowedByToken || allowTrampoline, bgStartsToken);
                         if (sent == ActivityManager.BROADCAST_SUCCESS) {
                             sendFinish = false;
                         }
@@ -456,11 +458,14 @@
                 case ActivityManager.INTENT_SENDER_SERVICE:
                 case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE:
                     try {
+                        final boolean allowedByToken =
+                                mAllowBgActivityStartsForServiceSender.contains(whitelistToken);
+                        final IBinder bgStartsToken = (allowedByToken) ? whitelistToken : null;
+
                         controller.mAmInternal.startServiceInPackage(uid, finalIntent, resolvedType,
                                 key.type == ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE,
                                 key.packageName, key.featureId, userId,
-                                mAllowBgActivityStartsForServiceSender.contains(whitelistToken)
-                                || allowTrampoline);
+                                allowedByToken || allowTrampoline, bgStartsToken);
                     } catch (RuntimeException e) {
                         Slog.w(TAG, "Unable to send startService intent", e);
                     } catch (TransactionTooLargeException e) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 42e3061..598b70e7 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -25,6 +25,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.am.ActivityManagerService.MY_PID;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ApplicationErrorReport;
 import android.app.ApplicationExitInfo;
@@ -274,9 +275,6 @@
     final ArrayMap<String, ContentProviderRecord> pubProviders = new ArrayMap<>();
     // All ContentProviderRecord process is using
     final ArrayList<ContentProviderConnection> conProviders = new ArrayList<>();
-    // A set of tokens that currently contribute to this process being temporarily allowed
-    // to start activities even if it's not in the foreground
-    final ArraySet<Binder> mAllowBackgroundActivityStartsTokens = new ArraySet<>();
     // a set of UIDs of all bound clients
     private ArraySet<Integer> mBoundClientUids = new ArraySet<>();
 
@@ -625,13 +623,6 @@
                 pw.print(prefix); pw.print("  - "); pw.println(receivers.valueAt(i));
             }
         }
-        if (mAllowBackgroundActivityStartsTokens.size() > 0) {
-            pw.print(prefix); pw.println("Background activity start tokens:");
-            for (int i = 0; i < mAllowBackgroundActivityStartsTokens.size(); i++) {
-                pw.print(prefix); pw.print("  - ");
-                pw.println(mAllowBackgroundActivityStartsTokens.valueAt(i));
-            }
-        }
     }
 
     ProcessRecord(ActivityManagerService _service, ApplicationInfo _info, String _processName,
@@ -1328,17 +1319,23 @@
         return mUsingWrapper;
     }
 
-    void addAllowBackgroundActivityStartsToken(Binder entity) {
+    /**
+     * Allows background activity starts using token {@param entity}. Optionally, you can provide
+     * {@param originatingToken} if you have one such originating token, this is useful for tracing
+     * back the grant in the case of the notification token.
+     */
+    void addAllowBackgroundActivityStartsToken(Binder entity, @Nullable IBinder originatingToken) {
         if (entity == null) return;
-        mAllowBackgroundActivityStartsTokens.add(entity);
-        mWindowProcessController.setAllowBackgroundActivityStarts(true);
+        mWindowProcessController.addAllowBackgroundActivityStartsToken(entity, originatingToken);
     }
 
     void removeAllowBackgroundActivityStartsToken(Binder entity) {
         if (entity == null) return;
-        mAllowBackgroundActivityStartsTokens.remove(entity);
-        mWindowProcessController.setAllowBackgroundActivityStarts(
-                !mAllowBackgroundActivityStartsTokens.isEmpty());
+        mWindowProcessController.removeAllowBackgroundActivityStartsToken(entity);
+    }
+
+    boolean areBackgroundActivityStartsAllowedByToken() {
+        return mWindowProcessController.areBackgroundActivityStartsAllowedByToken();
     }
 
     void addBoundClientUid(int clientUid) {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index db05d65..022b04d 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -575,7 +575,7 @@
                     ? _proc : null;
             if (mIsAllowedBgActivityStartsByStart
                     || mIsAllowedBgActivityStartsByBinding) {
-                _proc.addAllowBackgroundActivityStartsToken(this);
+                _proc.addAllowBackgroundActivityStartsToken(this, null);
             } else {
                 _proc.removeAllowBackgroundActivityStartsToken(this);
             }
@@ -723,7 +723,9 @@
      * {@code mIsAllowedBgActivityStartsByBinding}. If either is true, this ServiceRecord
      * should be contributing as a token in parent ProcessRecord.
      *
-     * @see com.android.server.am.ProcessRecord#mAllowBackgroundActivityStartsTokens
+     * @see com.android.server.am.ProcessRecord#addAllowBackgroundActivityStartsToken(Binder,
+     * IBinder)
+     * @see com.android.server.am.ProcessRecord#removeAllowBackgroundActivityStartsToken(Binder)
      */
     private void updateParentProcessBgActivityStartsToken() {
         if (app == null) {
@@ -732,7 +734,7 @@
         if (mIsAllowedBgActivityStartsByStart || mIsAllowedBgActivityStartsByBinding) {
             // if the token is already there it's safe to "re-add it" - we're dealing with
             // a set of Binder objects
-            app.addAllowBackgroundActivityStartsToken(this);
+            app.addAllowBackgroundActivityStartsToken(this, null);
         } else {
             app.removeAllowBackgroundActivityStartsToken(this);
         }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 88964e0..deea589 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -274,6 +274,7 @@
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.BackgroundActivityStartCallback;
 import com.android.server.wm.WindowManagerInternal;
 
 import libcore.io.IoUtils;
@@ -1894,6 +1895,7 @@
                 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
         mAm = am;
         mAtm = atm;
+        mAtm.setBackgroundActivityStartCallback(new NotificationTrampolineCallback());
         mUgm = ugm;
         mUgmInternal = ugmInternal;
         mPackageManager = packageManager;
@@ -9918,4 +9920,36 @@
         if (TextUtils.isEmpty(val)) return defValue;
         return Boolean.parseBoolean(val);
     }
+
+    /**
+     * Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too
+     * aggressive and annoying the user.
+     *
+     * TODO(b/161957908): Remove dogfooder toast.
+     */
+    private class NotificationTrampolineCallback implements BackgroundActivityStartCallback {
+        private Set<String> mPackagesShown = new ArraySet<>();
+
+        @Override
+        public IBinder getToken() {
+            return WHITELIST_TOKEN;
+        }
+
+        @Override
+        public void onExclusiveTokenActivityStart(String packageName) {
+            Slog.w(TAG, "Indirect notification activity start from " + packageName);
+            boolean isFirstOccurrence = mPackagesShown.add(packageName);
+            if (!isFirstOccurrence) {
+                return;
+            }
+
+            mUiHandler.post(() ->
+                    Toast.makeText(getUiContext(),
+                            "Indirect activity start from "
+                                    + packageName + ". "
+                                    + "This will be blocked in S.\n"
+                                    + "See go/s-trampolines.",
+                            Toast.LENGTH_LONG).show());
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index a903bcd..777ddda 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -221,6 +221,14 @@
             boolean allowBackgroundActivityStart);
 
     /**
+     * Callback to be called on certain activity start scenarios.
+     *
+     * @see BackgroundActivityStartCallback
+     */
+    public abstract void setBackgroundActivityStartCallback(
+            @Nullable BackgroundActivityStartCallback callback);
+
+    /**
      * Start activity {@code intent} without calling user-id check.
      *
      * - DO NOT call it with the calling UID cleared.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 029b554..d766928 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -675,6 +675,9 @@
     WindowOrganizerController mWindowOrganizerController;
     TaskOrganizerController mTaskOrganizerController;
 
+    @Nullable
+    private BackgroundActivityStartCallback mBackgroundActivityStartCallback;
+
     private int mDeviceOwnerUid = Process.INVALID_UID;
 
     private final class FontScaleSettingObserver extends ContentObserver {
@@ -1001,6 +1004,11 @@
         return config;
     }
 
+    @Nullable
+    public BackgroundActivityStartCallback getBackgroundActivityStartCallback() {
+        return mBackgroundActivityStartCallback;
+    }
+
     private void start() {
         LocalServices.addService(ActivityTaskManagerInternal.class, mInternal);
     }
@@ -6142,6 +6150,11 @@
             return false;
         }
 
+        public void setBackgroundActivityStartCallback(
+                @Nullable BackgroundActivityStartCallback backgroundActivityStartCallback) {
+            mBackgroundActivityStartCallback = backgroundActivityStartCallback;
+        }
+
         @Override
         public int startActivitiesAsPackage(String packageName, @Nullable String featureId,
                 int userId, Intent[] intents, Bundle bOptions) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java
new file mode 100644
index 0000000..4e742b9
--- /dev/null
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.os.IBinder;
+
+/**
+ * Callback to be called when a background activity start is allowed exclusively because of the
+ * token provided in {@link #getToken()}.
+ */
+public interface BackgroundActivityStartCallback {
+    /**
+     * The token that allowed the activity start that triggered {@link
+     * #onExclusiveTokenActivityStart()}.
+     *
+     * Ideally this should just return a final variable, don't do anything costly here (don't hold
+     * any locks).
+     */
+    IBinder getToken();
+
+    /**
+     * Called when the background activity start happens.
+     */
+    void onExclusiveTokenActivityStart(String packageName);
+}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index da9c7f3..6ba8769 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -55,11 +55,14 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
+import android.os.Binder;
 import android.os.Build;
+import android.os.IBinder;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
@@ -98,6 +101,11 @@
     final ApplicationInfo mInfo;
     final String mName;
     final int mUid;
+
+    // A set of tokens that currently contribute to this process being temporarily allowed
+    // to start activities even if it's not in the foreground. The values of this map are optional
+    // (can be null) and are used to trace back the grant to the notification token mechanism.
+    private final ArrayMap<Binder, IBinder> mBackgroundActivityStartTokens = new ArrayMap<>();
     // The process of this application; 0 if none
     private volatile int mPid;
     // user of process.
@@ -160,9 +168,6 @@
     private volatile boolean mPerceptible;
     // Set to true when process was launched with a wrapper attached
     private volatile boolean mUsingWrapper;
-    // Set to true if this process is currently temporarily allowed to start activities even if
-    // it's not in the foreground
-    private volatile boolean mAllowBackgroundActivityStarts;
     // Set of UIDs of clients currently bound to this process
     private volatile ArraySet<Integer> mBoundClientUids = new ArraySet<Integer>();
 
@@ -208,6 +213,9 @@
     /** Whether our process is currently running a {@link IRemoteAnimationRunner} */
     private boolean mRunningRemoteAnimation;
 
+    @Nullable
+    private BackgroundActivityStartCallback mBackgroundActivityStartCallback;
+
     public WindowProcessController(@NonNull ActivityTaskManagerService atm, ApplicationInfo info,
             String name, int uid, int userId, Object owner, WindowProcessListener listener) {
         mInfo = info;
@@ -218,6 +226,7 @@
         mListener = listener;
         mAtm = atm;
         mDisplayId = INVALID_DISPLAY;
+        mBackgroundActivityStartCallback = mAtm.getBackgroundActivityStartCallback();
 
         boolean isSysUiPackage = info.packageName.equals(
                 mAtm.getSysUiServiceComponentLocked().getPackageName());
@@ -449,19 +458,39 @@
         mLastActivityFinishTime = finishTime;
     }
 
-    public void setAllowBackgroundActivityStarts(boolean allowBackgroundActivityStarts) {
-        mAllowBackgroundActivityStarts = allowBackgroundActivityStarts;
+    /**
+     * Allows background activity starts using token {@code entity}. Optionally, you can provide
+     * {@code originatingToken} if you have one such originating token, this is useful for tracing
+     * back the grant in the case of the notification token.
+     */
+    public void addAllowBackgroundActivityStartsToken(Binder entity,
+            @Nullable IBinder originatingToken) {
+        synchronized (mAtm.mGlobalLock) {
+            mBackgroundActivityStartTokens.put(entity, originatingToken);
+        }
+    }
+
+    /**
+     * Removes token {@code entity} that allowed background activity starts added via {@link
+     * #addAllowBackgroundActivityStartsToken(Binder, IBinder)}.
+     */
+    public void removeAllowBackgroundActivityStartsToken(Binder entity) {
+        synchronized (mAtm.mGlobalLock) {
+            mBackgroundActivityStartTokens.remove(entity);
+        }
+    }
+
+    /**
+     * Returns true if background activity starts are allowed by any token added via {@link
+     * #addAllowBackgroundActivityStartsToken(Binder, IBinder)}.
+     */
+    public boolean areBackgroundActivityStartsAllowedByToken() {
+        synchronized (mAtm.mGlobalLock) {
+            return !mBackgroundActivityStartTokens.isEmpty();
+        }
     }
 
     boolean areBackgroundActivityStartsAllowed() {
-        // allow if the flag was explicitly set
-        if (mAllowBackgroundActivityStarts) {
-            if (DEBUG_ACTIVITY_STARTS) {
-                Slog.d(TAG, "[WindowProcessController(" + mPid
-                        + ")] Activity start allowed: mAllowBackgroundActivityStarts = true");
-            }
-            return true;
-        }
         // allow if any activity in the caller has either started or finished very recently, and
         // it must be started or finished after last stop app switches time.
         final long now = SystemClock.uptimeMillis();
@@ -510,9 +539,32 @@
             }
             return true;
         }
+        // allow if the flag was explicitly set
+        if (!mBackgroundActivityStartTokens.isEmpty()) {
+            onBackgroundStartAllowedByToken();
+            if (DEBUG_ACTIVITY_STARTS) {
+                Slog.d(TAG, "[WindowProcessController(" + mPid
+                        + ")] Activity start allowed: process allowed by token");
+            }
+            return true;
+        }
         return false;
     }
 
+    private void onBackgroundStartAllowedByToken() {
+        if (mBackgroundActivityStartCallback == null) {
+            return;
+        }
+        IBinder callbackToken = mBackgroundActivityStartCallback.getToken();
+        for (IBinder token : mBackgroundActivityStartTokens.values()) {
+            if (token != callbackToken) {
+                return;
+            }
+        }
+        mAtm.mH.post(() ->
+                mBackgroundActivityStartCallback.onExclusiveTokenActivityStart(mInfo.packageName));
+    }
+
     private boolean isBoundByForegroundUid() {
         for (int i = mBoundClientUids.size() - 1; i >= 0; --i) {
             if (mAtm.isUidForeground(mBoundClientUids.valueAt(i))) {
@@ -1434,6 +1486,13 @@
             if (mVrThreadTid != 0) {
                 pw.print(prefix); pw.print("mVrThreadTid="); pw.println(mVrThreadTid);
             }
+            if (mBackgroundActivityStartTokens.size() > 0) {
+                pw.print(prefix); pw.println("Background activity start tokens:");
+                for (int i = 0; i < mBackgroundActivityStartTokens.size(); i++) {
+                    pw.print(prefix); pw.print("  - ");
+                    pw.println(mBackgroundActivityStartTokens.keyAt(i));
+                }
+            }
         }
         pw.println(prefix + " Configuration=" + getConfiguration());
         pw.println(prefix + " OverrideConfiguration=" + getRequestedOverrideConfiguration());
diff --git a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
index a871ec6..5bef877 100644
--- a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -195,7 +195,8 @@
                 false /* sticky */,
                 false /* initialSticky */,
                 userId,
-                false, /* allowBackgroundActivityStarts */
+                false /* allowBackgroundActivityStarts */,
+                null /* activityStartsToken */,
                 false /* timeoutExempt */ );
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 3772e25..d07000f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -74,6 +74,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
@@ -665,7 +666,9 @@
         mService.mStackSupervisor.setRecentTasks(recentTasks);
         doReturn(callerIsRecents).when(recentTasks).isCallerRecents(callingUid);
         // caller is temp allowed
-        callerApp.setAllowBackgroundActivityStarts(callerIsTempAllowed);
+        if (callerIsTempAllowed) {
+            callerApp.addAllowBackgroundActivityStartsToken(new Binder(), null);
+        }
         // caller is instrumenting with background activity starts privileges
         callerApp.setInstrumenting(callerIsInstrumentingWithBackgroundActivityStartPrivileges,
                 callerIsInstrumentingWithBackgroundActivityStartPrivileges);