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>