summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jing Ji <jji@google.com> 2021-12-07 14:51:43 -0800
committer Jing Ji <jji@google.com> 2022-01-25 14:33:21 -0800
commit0c6603973ad136a3e730d0fb8c571d9468db2f6c (patch)
tree5048b73312fe8d3f81f699043f37e7e5461a2d4c
parentb5f28a5ee6d1df3c7609ac8dc6a80fbb8d642303 (diff)
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
-rw-r--r--core/java/com/android/internal/notification/SystemNotificationChannels.java7
-rw-r--r--core/res/res/values/strings.xml9
-rw-r--r--core/res/res/values/symbols.xml4
-rw-r--r--proto/src/system_messages.proto6
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java41
-rw-r--r--services/core/java/com/android/server/am/AppBatteryTracker.java40
-rw-r--r--services/core/java/com/android/server/am/AppRestrictionController.java208
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java30
8 files changed, 327 insertions, 18 deletions
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 3b6f8f6187e1..b79c0bed4564 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -62,6 +62,7 @@ public class SystemNotificationChannels {
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 @@ public class SystemNotificationChannels {
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 1a5d8b7ca8fd..fe5bafd491bb 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6199,4 +6199,13 @@ ul.</string>
<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 bcc3a6df3180..3ceaa2aaf9fc 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 196c6aaa7fcb..e89dda90a2dd 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -362,5 +362,11 @@ message SystemMessage {
// 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 c062365944d7..8eba1a2101b1 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 @@ final class ActivityManagerShellCommand extends ShellCommand {
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 @@ final class ActivityManagerShellCommand extends ShellCommand {
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 @@ final class ActivityManagerShellCommand extends ShellCommand {
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 49a22d6060ea..24ec0869aaac 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 @@ import java.util.List;
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 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
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 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
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 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
}
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 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
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 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> {
// 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 aa24a3452194..f1d48d4e02c3 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.app.usage.UsageStatsManager.reasonToString;
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.ActivityThread;
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.ContentResolver;
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.os.UserHandle;
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 android.util.TimeUtils;
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
@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 @@ public final class AppRestrictionController {
@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 @@ public final class AppRestrictionController {
}
}
+ @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 @@ public final class AppRestrictionController {
* 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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
void updateDeviceConfig() {
updateBgAutoRestrictedBucketChanged();
+ updateBgAbusiveNotificationMinimalInterval();
}
private void updateBgAutoRestrictedBucketChanged() {
@@ -437,6 +473,13 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
mBgHandlerThread.start();
mBgHandler = new BgHandler(mBgHandlerThread.getLooper(), injector);
mConstantsObserver = new ConstantsObserver(mBgHandler);
+ mNotificationHelper = new NotificationHelper(this);
injector.initAppStateTrackers(this);
}
@@ -793,6 +837,8 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
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 @@ public final class AppRestrictionController {
private IActivityManager mIActivityManager;
private UserManagerInternal mUserManagerInternal;
private PackageManagerInternal mPackageManagerInternal;
+ private NotificationManager mNotificationManager;
Injector(Context context) {
mContext = context;
@@ -1048,6 +1239,13 @@ public final class AppRestrictionController {
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 1d031e19d6a8..5e9232298df0 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 android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
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.ActivityManagerInternal.AppBackgroundRestrictionListener;
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 androidx.test.runner.AndroidJUnit4;
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 @@ public final class BackgroundRestrictionTest {
@Mock private UserManagerInternal mUserManagerInternal;
@Mock private PackageManager mPackageManager;
@Mock private PackageManagerInternal mPackageManagerInternal;
+ @Mock private NotificationManager mNotificationManager;
private long mCurrentTimeMillis;
@@ -619,6 +627,8 @@ public final class BackgroundRestrictionTest {
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 @@ public final class BackgroundRestrictionTest {
}
}
+ 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 @@ public final class BackgroundRestrictionTest {
PackageManager getPackageManager() {
return mPackageManager;
}
+
+ @Override
+ NotificationManager getNotificationManager() {
+ return mNotificationManager;
+ }
}
private class TestBaseTrackerInjector<T extends BaseAppStatePolicy>