Post notification on abusive background current drain

Tapping on the notification will bring up the individual abusive
app's battery settings page. It won't be re-posted after being
dismissed until next day, in order to avoid from spamming the user.

Bug: 200326767
Bug: 203105544
Test: FrameworksMockingServicesTests:BackgroundRestrictionTest
Test: Manual - adb shell am set-bg-abusive-uids & verify notification
Change-Id: I0b30014d748863c66c3845b5f310948a9493e302
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 3b6f8f6..b79c0be 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -62,6 +62,7 @@
     public static String DO_NOT_DISTURB = "DO_NOT_DISTURB";
     public static String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
     public static String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
+    public static String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
 
     public static void createAll(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
@@ -209,6 +210,12 @@
                 NotificationManager.IMPORTANCE_LOW);
         channelsList.add(accessibilitySecurityPolicyChannel);
 
+        final NotificationChannel abusiveBackgroundAppsChannel = new NotificationChannel(
+                ABUSIVE_BACKGROUND_APPS,
+                context.getString(R.string.notification_channel_abusive_bg_apps),
+                NotificationManager.IMPORTANCE_LOW);
+        channelsList.add(abusiveBackgroundAppsChannel);
+
         nm.createNotificationChannels(channelsList);
     }
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 1a5d8b7..fe5bafd 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6199,4 +6199,13 @@
     <string name="ui_translation_accessibility_translated_text"><xliff:g id="message" example="Hello">%1$s</xliff:g> Translated.</string>
     <!-- Accessibility message announced to notify the user when the system has finished translating the content displayed on the screen to a different language after the user requested translation. [CHAR LIMIT=NONE] -->
     <string name="ui_translation_accessibility_translation_finished">Message translated from <xliff:g id="from_language" example="English">%1$s</xliff:g> to <xliff:g id="to_language" example="French">%2$s</xliff:g>.</string>
+
+    <!-- Title for the notification channel notifying user of abusive background apps. [CHAR LIMIT=NONE] -->
+    <string name="notification_channel_abusive_bg_apps">Background Activity</string>
+    <!-- Title of notification indicating abusive background apps. [CHAR LIMIT=NONE] -->
+    <string name="notification_title_abusive_bg_apps">Background Activity</string>
+    <!-- Content of notification indicating abusive background apps. [CHAR LIMIT=NONE] -->
+    <string name="notification_content_abusive_bg_apps">
+        <xliff:g id="app" example="Gmail">%1$s</xliff:g> is running in the background and draining battery. Tap to review.
+    </string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bcc3a6d..3ceaa2a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4669,4 +4669,8 @@
   <java-symbol type="string" name="config_deviceManagerUpdater" />
 
   <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
+
+  <java-symbol type="string" name="notification_channel_abusive_bg_apps"/>
+  <java-symbol type="string" name="notification_title_abusive_bg_apps"/>
+  <java-symbol type="string" name="notification_content_abusive_bg_apps"/>
 </resources>
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 196c6aa..e89dda9 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -362,5 +362,11 @@
     // Notify the user that some accessibility service has view and control permissions.
     // package: android
     NOTE_A11Y_VIEW_AND_CONTROL_ACCESS = 1005;
+
+    // Notify the user an abusive background app has been detected.
+    // Package: android
+    // Note: this is a base ID, multiple notifications will be posted for each
+    // abusive apps, with notification ID based off this ID.
+    NOTE_ABUSIVE_BG_APPS_BASE = 0xc1b2508; // 203105544
   }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index c062365..8eba1a2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -336,6 +336,8 @@
                     return runGetIsolatedProcesses(pw);
                 case "set-stop-user-on-switch":
                     return runSetStopUserOnSwitch(pw);
+                case "set-bg-abusive-uids":
+                    return runSetBgAbusiveUids(pw);
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -3215,6 +3217,43 @@
         return 0;
     }
 
+    // TODO(b/203105544) STOPSHIP - For debugging only, to be removed before shipping.
+    private int runSetBgAbusiveUids(PrintWriter pw) throws RemoteException {
+        final String arg = getNextArg();
+        final AppBatteryTracker batteryTracker =
+                mInternal.mAppRestrictionController.getAppStateTracker(AppBatteryTracker.class);
+        if (batteryTracker == null) {
+            getErrPrintWriter().println("Unable to get bg battery tracker");
+            return -1;
+        }
+        if (arg == null) {
+            batteryTracker.mDebugUidPercentages.clear();
+            return 0;
+        }
+        String[] pairs = arg.split(",");
+        int[] uids = new int[pairs.length];
+        double[] values = new double[pairs.length];
+        try {
+            for (int i = 0; i < pairs.length; i++) {
+                String[] pair = pairs[i].split("=");
+                if (pair.length != 2) {
+                    getErrPrintWriter().println("Malformed input");
+                    return -1;
+                }
+                uids[i] = Integer.parseInt(pair[0]);
+                values[i] = Double.parseDouble(pair[1]);
+            }
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Malformed input");
+            return -1;
+        }
+        batteryTracker.mDebugUidPercentages.clear();
+        for (int i = 0; i < pairs.length; i++) {
+            batteryTracker.mDebugUidPercentages.put(uids[i], values[i]);
+        }
+        return 0;
+    }
+
     private Resources getResources(PrintWriter pw) throws RemoteException {
         // system resources does not contain all the device configuration, construct it manually.
         Configuration config = mInterface.getConfiguration();
@@ -3551,6 +3590,8 @@
             pw.println("         Sets whether the current user (and its profiles) should be stopped"
                     + " when switching to a different user.");
             pw.println("         Without arguments, it resets to the value defined by platform.");
+            pw.println("  set-bg-abusive-uids [uid=percentage][,uid=percentage...]");
+            pw.println("         Force setting the battery usage of the given UID.");
             pw.println();
             Intent.printIntentArgsHelp(pw, "");
         }
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index 49a22d6..24ec0869 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -69,7 +69,7 @@
 final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
     static final String TAG = TAG_WITH_CLASS_NAME ? "AppBatteryTracker" : TAG_AM;
 
-    private static final boolean DEBUG_BACKGROUND_BATTERY_TRACKER = false;
+    static final boolean DEBUG_BACKGROUND_BATTERY_TRACKER = false;
 
     // As we don't support realtime per-UID battery usage stats yet, we're polling the stats
     // in a regular time basis.
@@ -103,6 +103,9 @@
 
     private BatteryUsageStatsQuery mBatteryUsageStatsQuery;
 
+    // For debug only.
+    final SparseArray<Double> mDebugUidPercentages = new SparseArray<>();
+
     AppBatteryTracker(Context context, AppRestrictionController controller) {
         this(context, controller, null, null);
     }
@@ -112,7 +115,9 @@
             Object outerContext) {
         super(context, controller, injector, outerContext);
         if (injector == null) {
-            mBatteryUsageStatsPollingIntervalMs = BATTERY_USAGE_STATS_POLLING_INTERVAL_MS_LONG;
+            mBatteryUsageStatsPollingIntervalMs = DEBUG_BACKGROUND_BATTERY_TRACKER
+                    ? BATTERY_USAGE_STATS_POLLING_INTERVAL_MS_DEBUG
+                    : BATTERY_USAGE_STATS_POLLING_INTERVAL_MS_LONG;
         } else {
             mBatteryUsageStatsPollingIntervalMs = BATTERY_USAGE_STATS_POLLING_INTERVAL_MS_DEBUG;
         }
@@ -186,6 +191,11 @@
                 }
                 bgPolicy.handleUidBatteryConsumption(uid, percentage);
             }
+            // For debugging only.
+            for (int i = 0, size = mDebugUidPercentages.size(); i < size; i++) {
+                bgPolicy.handleUidBatteryConsumption(mDebugUidPercentages.keyAt(i),
+                        mDebugUidPercentages.valueAt(i));
+            }
         } finally {
             scheduleBatteryUsageStatsUpdateIfNecessary();
         }
@@ -422,7 +432,8 @@
             mBgCurrentDrainWindowMs = DeviceConfig.getLong(
                     DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     KEY_BG_CURRENT_DRAIN_WINDOW,
-                    DEFAULT_BG_CURRENT_DRAIN_WINDOW_MS);
+                    mBgCurrentDrainWindowMs != DEFAULT_BG_CURRENT_DRAIN_WINDOW_MS
+                    ? mBgCurrentDrainWindowMs : DEFAULT_BG_CURRENT_DRAIN_WINDOW_MS);
         }
 
         @Override
@@ -490,16 +501,19 @@
                         // it's actually back to normal, but we don't untrack it until
                         // explicit user interactions.
                         notifyController = true;
-                    } else if (percentage >= mBgCurrentDrainBgRestrictedThreshold) {
-                        // If we're in the restricted standby bucket but still seeing high
-                        // current drains, tell the controller again.
-                        if (curLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET
-                                && ts[TIME_STAMP_INDEX_BG_RESTRICTED] == 0) {
-                            final long now = SystemClock.elapsedRealtime();
-                            if (now > ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
-                                    + mBgCurrentDrainWindowMs) {
-                                ts[TIME_STAMP_INDEX_BG_RESTRICTED] = now;
-                                notifyController = excessive = true;
+                    } else {
+                        excessive = true;
+                        if (percentage >= mBgCurrentDrainBgRestrictedThreshold) {
+                            // If we're in the restricted standby bucket but still seeing high
+                            // current drains, tell the controller again.
+                            if (curLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET
+                                    && ts[TIME_STAMP_INDEX_BG_RESTRICTED] == 0) {
+                                final long now = SystemClock.elapsedRealtime();
+                                if (now > ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
+                                        + mBgCurrentDrainWindowMs) {
+                                    ts[TIME_STAMP_INDEX_BG_RESTRICTED] = now;
+                                    notifyController = true;
+                                }
                             }
                         }
                     }
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index aa24a34..f1d48d4 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -47,7 +47,9 @@
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+import static android.os.Process.SYSTEM_UID;
 
+import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
@@ -61,6 +63,9 @@
 import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.UsageStatsManager;
 import android.content.BroadcastReceiver;
@@ -68,9 +73,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -82,6 +89,7 @@
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
+import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.util.Slog;
 import android.util.SparseArrayMap;
@@ -89,6 +97,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.function.TriConsumer;
 import com.android.server.AppStateTracker;
@@ -147,6 +156,7 @@
 
     private final Object mLock = new Object();
     private final Injector mInjector;
+    private final NotificationHelper mNotificationHelper;
 
     /**
      * The restriction levels that each package is on, the levels here are defined in
@@ -165,6 +175,9 @@
             private @ElapsedRealtimeLong long mLevelChangeTimeElapsed;
             private int mReason;
 
+            @ElapsedRealtimeLong long mLastNotificationShownTimeElapsed;
+            int mNotificationId;
+
             PkgSettings(String packageName, int uid) {
                 mPackageName = packageName;
                 mUid = uid;
@@ -207,8 +220,12 @@
                     pw.print('/');
                     pw.print(ActivityManager.restrictionLevelToName(mLastRestrictionLevel));
                 }
-                pw.print(' ');
+                pw.print(" levelChange=");
                 TimeUtils.formatDuration(mLevelChangeTimeElapsed - nowElapsed, pw);
+                if (mLastNotificationShownTimeElapsed > 0) {
+                    pw.print(" lastNoti=");
+                    TimeUtils.formatDuration(mLastNotificationShownTimeElapsed - nowElapsed, pw);
+                }
             }
 
             String getPackageName() {
@@ -240,7 +257,7 @@
         @RestrictionLevel int update(String packageName, int uid, @RestrictionLevel int level,
                 int reason, int subReason) {
             synchronized (mLock) {
-                PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+                PkgSettings settings = getRestrictionSettingsLocked(uid, packageName);
                 if (settings == null) {
                     settings = new PkgSettings(packageName, uid);
                     mRestrictionLevels.add(uid, packageName, settings);
@@ -284,7 +301,7 @@
 
         @RestrictionLevel int getRestrictionLevel(int uid, String packageName) {
             synchronized (mLock) {
-                final PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+                final PkgSettings settings = getRestrictionSettingsLocked(uid, packageName);
                 return settings == null
                         ? getRestrictionLevel(uid) : settings.getCurrentRestrictionLevel();
             }
@@ -325,6 +342,11 @@
             }
         }
 
+        @GuardedBy("mLock")
+        PkgSettings getRestrictionSettingsLocked(int uid, String packageName) {
+            return mRestrictionLevels.get(uid, packageName);
+        }
+
         void removeUser(@UserIdInt int userId) {
             synchronized (mLock) {
                 for (int i = mRestrictionLevels.numMaps() - 1; i >= 0; i--) {
@@ -373,14 +395,24 @@
          * when it's background-restricted.
          */
         static final String KEY_BG_AUTO_RESTRICTED_BUCKET_ON_BG_RESTRICTION =
-                    DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "auto_restricted_bucket_on_bg_restricted";
+                DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "auto_restricted_bucket_on_bg_restricted";
+
+        /**
+         * The minimal interval in ms before posting a notification again on abusive behaviors
+         * of a certain package.
+         */
+        static final String KEY_BG_ABUSIVE_NOTIFICATION_MINIMAL_INTERVAL =
+                DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "abusive_notification_minimal_interval";
 
         static final boolean DEFAULT_BG_AUTO_RESTRICTED_BUCKET_ON_BG_RESTRICTION = true;
+        static final long DEFAULT_BG_ABUSIVE_NOTIFICATION_MINIMAL_INTERVAL_MS = 24 * 60 * 60 * 1000;
 
         volatile boolean mBgAutoRestrictedBucket;
 
         volatile boolean mRestrictedBucketEnabled;
 
+        volatile long mBgNotificationMinIntervalMs;
+
         ConstantsObserver(Handler handler) {
             super(handler);
         }
@@ -395,6 +427,9 @@
                     case KEY_BG_AUTO_RESTRICTED_BUCKET_ON_BG_RESTRICTION:
                         updateBgAutoRestrictedBucketChanged();
                         break;
+                    case KEY_BG_ABUSIVE_NOTIFICATION_MINIMAL_INTERVAL:
+                        updateBgAbusiveNotificationMinimalInterval();
+                        break;
                 }
                 AppRestrictionController.this.onPropertiesChanged(name);
             }
@@ -425,6 +460,7 @@
 
         void updateDeviceConfig() {
             updateBgAutoRestrictedBucketChanged();
+            updateBgAbusiveNotificationMinimalInterval();
         }
 
         private void updateBgAutoRestrictedBucketChanged() {
@@ -437,6 +473,13 @@
                 dispatchAutoRestrictedBucketFeatureFlagChanged(mBgAutoRestrictedBucket);
             }
         }
+
+        private void updateBgAbusiveNotificationMinimalInterval() {
+            mBgNotificationMinIntervalMs = DeviceConfig.getLong(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    KEY_BG_ABUSIVE_NOTIFICATION_MINIMAL_INTERVAL,
+                    DEFAULT_BG_ABUSIVE_NOTIFICATION_MINIMAL_INTERVAL_MS);
+        }
     }
 
     private final ConstantsObserver mConstantsObserver;
@@ -515,6 +558,7 @@
         mBgHandlerThread.start();
         mBgHandler = new BgHandler(mBgHandlerThread.getLooper(), injector);
         mConstantsObserver = new ConstantsObserver(mBgHandler);
+        mNotificationHelper = new NotificationHelper(this);
         injector.initAppStateTrackers(this);
     }
 
@@ -793,6 +837,8 @@
             applyRestrictionLevel(pkgName, uid, RESTRICTION_LEVEL_BACKGROUND_RESTRICTED,
                     curBucket, true, REASON_MAIN_FORCED_BY_USER,
                     REASON_SUB_FORCED_USER_FLAG_INTERACTION);
+            mBgHandler.obtainMessage(BgHandler.MSG_CANCEL_REQUEST_BG_RESTRICTED, uid, 0, pkgName)
+                    .sendToTarget();
         } else {
             // Moved out of the background-restricted state, we'd need to check if it should
             // stay in the restricted standby bucket.
@@ -857,7 +903,135 @@
             Slog.i(TAG, "Requesting background restricted " + packageName + " "
                     + UserHandle.formatUid(uid));
         }
-        // TODO: b/200326767 - show the request notification.
+        mNotificationHelper.postRequestBgRestrictedIfNecessary(packageName, uid);
+    }
+
+    void handleCancelRequestBgRestricted(String packageName, int uid) {
+        if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+            Slog.i(TAG, "Cancelling requesting background restricted " + packageName + " "
+                    + UserHandle.formatUid(uid));
+        }
+        mNotificationHelper.cancelRequestBgRestrictedIfNecessary(packageName, uid);
+    }
+
+    static class NotificationHelper {
+        static final String PACKAGE_SCHEME = "package";
+        static final String GROUP_KEY = "com.android.app.abusive_bg_apps";
+
+        static final int SUMMARY_NOTIFICATION_ID = SystemMessage.NOTE_ABUSIVE_BG_APPS_BASE;
+
+        private final AppRestrictionController mBgController;
+        private final NotificationManager mNotificationManager;
+        private final Injector mInjector;
+        private final Object mLock;
+        private final Context mContext;
+
+        @GuardedBy("mLock")
+        private int mNotificationIDStepper = SUMMARY_NOTIFICATION_ID + 1;
+
+        NotificationHelper(AppRestrictionController controller) {
+            mBgController = controller;
+            mInjector = controller.mInjector;
+            mNotificationManager = mInjector.getNotificationManager();
+            mLock = controller.mLock;
+            mContext = mInjector.getContext();
+        }
+
+        void postRequestBgRestrictedIfNecessary(String packageName, int uid) {
+            int notificationId;
+            synchronized (mLock) {
+                final RestrictionSettings.PkgSettings settings = mBgController.mRestrictionSettings
+                        .getRestrictionSettingsLocked(uid, packageName);
+
+                final long now = SystemClock.elapsedRealtime();
+                if (settings.mLastNotificationShownTimeElapsed != 0
+                        && (settings.mLastNotificationShownTimeElapsed
+                        + mBgController.mConstantsObserver.mBgNotificationMinIntervalMs > now)) {
+                    if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+                        Slog.i(TAG, "Not showing notification as last notification was shown "
+                                + TimeUtils.formatDuration(
+                                        now - settings.mLastNotificationShownTimeElapsed)
+                                + " ago");
+                    }
+                    return;
+                }
+                if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+                    Slog.i(TAG, "Showing notification for " + packageName
+                            + "/" + UserHandle.formatUid(uid)
+                            + ", now=" + now
+                            + ", lastShown=" + settings.mLastNotificationShownTimeElapsed);
+                }
+                settings.mLastNotificationShownTimeElapsed = now;
+                if (settings.mNotificationId == 0) {
+                    settings.mNotificationId = mNotificationIDStepper++;
+                }
+                notificationId = settings.mNotificationId;
+            }
+
+            final UserHandle targetUser = UserHandle.of(UserHandle.getUserId(uid));
+
+            postSummaryNotification(targetUser);
+
+            final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
+            final ApplicationInfo ai = pm.getApplicationInfo(packageName, STOCK_PM_FLAGS,
+                    SYSTEM_UID, UserHandle.getUserId(uid));
+            final String title = mContext.getString(
+                    com.android.internal.R.string.notification_title_abusive_bg_apps);
+            final String message = mContext.getString(
+                    com.android.internal.R.string.notification_content_abusive_bg_apps,
+                    ai != null ? mInjector.getPackageManager()
+                    .getText(packageName, ai.labelRes, ai) : packageName);
+
+            final Intent intent = new Intent(Settings.ACTION_VIEW_ADVANCED_POWER_USAGE_DETAIL);
+            intent.setData(Uri.fromParts(PACKAGE_SCHEME, packageName, null));
+            final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(mContext, 0,
+                    intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, null,
+                    targetUser);
+
+            final Notification.Builder notificationBuilder = new Notification.Builder(mContext,
+                    ABUSIVE_BACKGROUND_APPS)
+                    .setAutoCancel(true)
+                    .setGroup(GROUP_KEY)
+                    .setWhen(System.currentTimeMillis())
+                    .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
+                    .setColor(mContext.getColor(
+                            com.android.internal.R.color.system_notification_accent_color))
+                    .setContentTitle(title)
+                    .setContentText(message)
+                    .setContentIntent(pendingIntent);
+            if (ai != null) {
+                notificationBuilder.setLargeIcon(Icon.createWithResource(packageName, ai.icon));
+            }
+
+            final Notification notification = notificationBuilder.build();
+            // Remember the package name for testing.
+            notification.extras.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
+
+            mNotificationManager.notifyAsUser(null, notificationId, notification, targetUser);
+        }
+
+        private void postSummaryNotification(@NonNull UserHandle targetUser) {
+            final Notification summary = new Notification.Builder(mContext,
+                    ABUSIVE_BACKGROUND_APPS)
+                    .setGroup(GROUP_KEY)
+                    .setGroupSummary(true)
+                    .setStyle(new Notification.BigTextStyle())
+                    .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
+                    .setColor(mContext.getColor(
+                            com.android.internal.R.color.system_notification_accent_color))
+                    .build();
+            mNotificationManager.notifyAsUser(null, SUMMARY_NOTIFICATION_ID, summary, targetUser);
+        }
+
+        void cancelRequestBgRestrictedIfNecessary(String packageName, int uid) {
+            synchronized (mLock) {
+                final RestrictionSettings.PkgSettings settings = mBgController.mRestrictionSettings
+                        .getRestrictionSettingsLocked(uid, packageName);
+                if (settings.mNotificationId > 0) {
+                    mNotificationManager.cancel(settings.mNotificationId);
+                }
+            }
+        }
     }
 
     void handleUidInactive(int uid, boolean disabled) {
@@ -924,6 +1098,18 @@
         mAppStateTrackers.add(tracker);
     }
 
+    /**
+     * @return The tracker instance of the given class.
+     */
+    <T extends BaseAppStateTracker> T getAppStateTracker(Class<T> trackerClass) {
+        for (BaseAppStateTracker tracker : mAppStateTrackers) {
+            if (trackerClass.isAssignableFrom(tracker.getClass())) {
+                return (T) tracker;
+            }
+        }
+        return null;
+    }
+
     static class BgHandler extends Handler {
         static final int MSG_BACKGROUND_RESTRICTION_CHANGED = 0;
         static final int MSG_APP_RESTRICTION_LEVEL_CHANGED = 1;
@@ -932,6 +1118,7 @@
         static final int MSG_REQUEST_BG_RESTRICTED = 4;
         static final int MSG_UID_INACTIVE = 5;
         static final int MSG_UID_ACTIVE = 6;
+        static final int MSG_CANCEL_REQUEST_BG_RESTRICTED = 7;
 
         private final Injector mInjector;
 
@@ -966,6 +1153,9 @@
                 case MSG_UID_ACTIVE: {
                     c.handleUidActive(msg.arg1);
                 } break;
+                case MSG_CANCEL_REQUEST_BG_RESTRICTED: {
+                    c.handleCancelRequestBgRestricted((String) msg.obj, msg.arg1);
+                } break;
             }
         }
     }
@@ -980,6 +1170,7 @@
         private IActivityManager mIActivityManager;
         private UserManagerInternal mUserManagerInternal;
         private PackageManagerInternal mPackageManagerInternal;
+        private NotificationManager mNotificationManager;
 
         Injector(Context context) {
             mContext = context;
@@ -1048,6 +1239,13 @@
         PackageManager getPackageManager() {
             return getContext().getPackageManager();
         }
+
+        NotificationManager getNotificationManager() {
+            if (mNotificationManager == null) {
+                mNotificationManager = getContext().getSystemService(NotificationManager.class);
+            }
+            return mNotificationManager;
+        }
     }
 
     private void registerForSystemBroadcasts() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
index 1d031e1..5e92322 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -36,13 +36,16 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
 import static com.android.server.am.AppBatteryTracker.BATT_DIMEN_BG;
 import static com.android.server.am.AppBatteryTracker.BATT_DIMEN_FG;
 import static com.android.server.am.AppBatteryTracker.BATT_DIMEN_FGS;
 import static com.android.server.am.AppRestrictionController.STOCK_PM_FLAGS;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -64,8 +67,11 @@
 import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.usage.AppStandbyInfo;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.os.BatteryManagerInternal;
@@ -85,6 +91,7 @@
 import com.android.server.AppStateTracker;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.am.AppBatteryTracker.AppBatteryPolicy;
+import com.android.server.am.AppRestrictionController.NotificationHelper;
 import com.android.server.apphibernation.AppHibernationManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal;
@@ -168,6 +175,7 @@
     @Mock private UserManagerInternal mUserManagerInternal;
     @Mock private PackageManager mPackageManager;
     @Mock private PackageManagerInternal mPackageManagerInternal;
+    @Mock private NotificationManager mNotificationManager;
 
     private long mCurrentTimeMillis;
 
@@ -619,6 +627,8 @@
                         verify(mBgRestrictionController, times(1)).handleRequestBgRestricted(
                                 eq(testPkgName),
                                 eq(testUid));
+                        // Verify we have the notification posted.
+                        checkNotification(testPkgName);
                     });
 
             // Turn ON the FAS for real.
@@ -658,6 +668,21 @@
         }
     }
 
+    private void checkNotification(String packageName) throws Exception {
+        final NotificationManager nm = mInjector.getNotificationManager();
+        final ArgumentCaptor<Integer> notificationIdCaptor =
+                ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Notification> notificationCaptor =
+                ArgumentCaptor.forClass(Notification.class);
+        verify(mInjector.getNotificationManager(), atLeast(1)).notifyAsUser(any(),
+                notificationIdCaptor.capture(), notificationCaptor.capture(), any());
+        final Notification n = notificationCaptor.getValue();
+        assertTrue(NotificationHelper.SUMMARY_NOTIFICATION_ID < notificationIdCaptor.getValue());
+        assertEquals(NotificationHelper.GROUP_KEY, n.getGroup());
+        assertEquals(ABUSIVE_BACKGROUND_APPS, n.getChannelId());
+        assertEquals(packageName, n.extras.getString(Intent.EXTRA_PACKAGE_NAME));
+    }
+
     private void closeIfNotNull(DeviceConfigSession<?> config) throws Exception {
         if (config != null) {
             config.close();
@@ -812,6 +837,11 @@
         PackageManager getPackageManager() {
             return mPackageManager;
         }
+
+        @Override
+        NotificationManager getNotificationManager() {
+            return mNotificationManager;
+        }
     }
 
     private class TestBaseTrackerInjector<T extends BaseAppStatePolicy>