summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/ActivityManagerInternal.java21
-rw-r--r--core/java/android/content/Intent.java11
-rw-r--r--core/res/AndroidManifest.xml1
-rw-r--r--core/res/res/values/strings.xml4
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java26
-rw-r--r--services/core/java/com/android/server/am/AppFGSTracker.java539
-rw-r--r--services/core/java/com/android/server/am/AppRestrictionController.java209
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java199
9 files changed, 963 insertions, 48 deletions
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 70e95a11782f..d3df3ff8bc21 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -755,4 +755,25 @@ public abstract class ActivityManagerInternal {
*/
public abstract void addAppBackgroundRestrictionListener(
@NonNull AppBackgroundRestrictionListener listener);
+
+ /**
+ * A listener interface, which will be notified on foreground service state changes.
+ */
+ public interface ForegroundServiceStateListener {
+ /**
+ * Call when the given process's foreground service state changes.
+ *
+ * @param packageName The package name of the process.
+ * @param uid The UID of the process.
+ * @param pid The pid of the process.
+ * @param started {@code true} if the process transits from non-FGS state to FGS state.
+ */
+ void onForegroundServiceStateChanged(String packageName, int uid, int pid, boolean started);
+ }
+
+ /**
+ * Register the foreground service state change listener callback.
+ */
+ public abstract void addForegroundServiceStateListener(
+ @NonNull ForegroundServiceStateListener listener);
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 7f00bcb1dccb..b7de8dd0a0a8 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5053,6 +5053,17 @@ public class Intent implements Parcelable, Cloneable {
public static final String ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION =
"android.intent.action.PACKAGE_NEEDS_INTEGRITY_VERIFICATION";
+ /**
+ * Broadcast Action: Start the foreground service manager.
+ *
+ * <p class="note">
+ * This is a protected intent that can only be sent by the system.
+ * </p>
+ *
+ * @hide
+ */
+ public static final String ACTION_SHOW_FOREGROUND_SERVICE_MANAGER =
+ "android.intent.action.SHOW_FOREGROUND_SERVICE_MANAGER";
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3a842ee6e7f3..14d6a5c942c8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -717,6 +717,7 @@
<!-- Added in T -->
<protected-broadcast android:name="android.intent.action.REFRESH_SAFETY_SOURCES" />
<protected-broadcast android:name="android.app.action.DEVICE_POLICY_RESOURCE_UPDATED" />
+ <protected-broadcast android:name="android.intent.action.SHOW_FOREGROUND_SERVICE_MANAGER" />
<!-- ====================================================================== -->
<!-- RUNTIME PERMISSIONS -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index fe5bafd491bb..de31fe87c0c4 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6208,4 +6208,8 @@ ul.</string>
<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>
+ <!-- Content of notification indicating long running foreground service. [CHAR LIMIT=NONE] -->
+ <string name="notification_content_long_running_fgs">
+ <xliff:g id="app" example="Gmail">%1$s</xliff:g> is running in the background for a long time. Tap to review.
+ </string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3ceaa2aaf9fc..a118ab1864cb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4673,4 +4673,5 @@
<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"/>
+ <java-symbol type="string" name="notification_content_long_running_fgs"/>
</resources>
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 57b5aab58f6b..ccdf4945df80 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -156,6 +156,7 @@ import android.app.ActivityManager.ProcessCapability;
import android.app.ActivityManager.RestrictionLevel;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ForegroundServiceStateListener;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.app.ActivityThread;
import android.app.AnrController;
@@ -1373,6 +1374,13 @@ public class ActivityManagerService extends IActivityManager.Stub
= new ProcessMap<ArrayList<ProcessRecord>>();
/**
+ * The list of foreground service state change listeners.
+ */
+ @GuardedBy("this")
+ final ArrayList<ForegroundServiceStateListener> mForegroundServiceStateListeners =
+ new ArrayList<>();
+
+ /**
* Set if the systemServer made a call to enterSafeMode.
*/
@GuardedBy("this")
@@ -14841,8 +14849,16 @@ public class ActivityManagerService extends IActivityManager.Stub
final void updateProcessForegroundLocked(ProcessRecord proc, boolean isForeground,
int fgServiceTypes, boolean oomAdj) {
final ProcessServiceRecord psr = proc.mServices;
- if (isForeground != psr.hasForegroundServices()
+ final boolean foregroundStateChanged = isForeground != psr.hasForegroundServices();
+ if (foregroundStateChanged
|| psr.getForegroundServiceTypes() != fgServiceTypes) {
+ if (foregroundStateChanged) {
+ // Notify internal listeners.
+ for (int i = mForegroundServiceStateListeners.size() - 1; i >= 0; i--) {
+ mForegroundServiceStateListeners.get(i).onForegroundServiceStateChanged(
+ proc.info.packageName, proc.info.uid, proc.getPid(), isForeground);
+ }
+ }
psr.setHasForegroundServices(isForeground, fgServiceTypes);
ArrayList<ProcessRecord> curProcs = mForegroundPackages.get(proc.info.packageName,
proc.info.uid);
@@ -16849,6 +16865,14 @@ public class ActivityManagerService extends IActivityManager.Stub
@NonNull ActivityManagerInternal.AppBackgroundRestrictionListener listener) {
mAppRestrictionController.addAppBackgroundRestrictionListener(listener);
}
+
+ @Override
+ public void addForegroundServiceStateListener(
+ @NonNull ForegroundServiceStateListener listener) {
+ synchronized (ActivityManagerService.this) {
+ mForegroundServiceStateListeners.add(listener);
+ }
+ }
}
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java
new file mode 100644
index 000000000000..80c8fec1533d
--- /dev/null
+++ b/services/core/java/com/android/server/am/AppFGSTracker.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.app.ActivityManager.RESTRICTION_LEVEL_UNKNOWN;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.am.AppRestrictionController.DEVICE_CONFIG_SUBNAMESPACE_PREFIX;
+import static com.android.server.am.BaseAppStateTracker.ONE_DAY;
+import static com.android.server.am.BaseAppStateTracker.ONE_HOUR;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager.RestrictionLevel;
+import android.app.ActivityManagerInternal.ForegroundServiceStateListener;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ProcessMap;
+import com.android.server.am.AppFGSTracker.AppFGSPolicy;
+import com.android.server.am.BaseAppStateTracker.Injector;
+
+import java.lang.reflect.Constructor;
+import java.util.LinkedList;
+
+/**
+ * The tracker for monitoring abusive (long-running) FGS.
+ */
+final class AppFGSTracker extends BaseAppStateTracker<AppFGSPolicy>
+ implements ForegroundServiceStateListener {
+ static final String TAG = TAG_WITH_CLASS_NAME ? "AppFGSTracker" : TAG_AM;
+
+ static final boolean DEBUG_BACKGROUND_FGS_TRACKER = false;
+
+ private final MyHandler mHandler;
+
+ @GuardedBy("mLock")
+ private final ProcessMap<PackageDurations> mPkgFgsDurations = new ProcessMap<>();
+
+ // Unlocked since it's only accessed in single thread.
+ private final ArraySet<PackageDurations> mTmpPkgDurations = new ArraySet<>();
+
+ @Override
+ public void onForegroundServiceStateChanged(String packageName,
+ int uid, int pid, boolean started) {
+ mHandler.obtainMessage(started ? MyHandler.MSG_FOREGROUND_SERVICES_STARTED
+ : MyHandler.MSG_FOREGROUND_SERVICES_STOPPED, pid, uid, packageName).sendToTarget();
+ }
+
+ private static class MyHandler extends Handler {
+ static final int MSG_FOREGROUND_SERVICES_STARTED = 0;
+ static final int MSG_FOREGROUND_SERVICES_STOPPED = 1;
+ static final int MSG_CHECK_LONG_RUNNING_FGS = 2;
+
+ private final AppFGSTracker mTracker;
+
+ MyHandler(AppFGSTracker tracker) {
+ super(tracker.mBgHandler.getLooper());
+ mTracker = tracker;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_FOREGROUND_SERVICES_STARTED:
+ mTracker.handleForegroundServicesChanged(
+ (String) msg.obj, msg.arg1, msg.arg2, true);
+ break;
+ case MSG_FOREGROUND_SERVICES_STOPPED:
+ mTracker.handleForegroundServicesChanged(
+ (String) msg.obj, msg.arg1, msg.arg2, false);
+ break;
+ case MSG_CHECK_LONG_RUNNING_FGS:
+ mTracker.checkLongRunningFgs();
+ break;
+ }
+ }
+ }
+
+ AppFGSTracker(Context context, AppRestrictionController controller) {
+ this(context, controller, null, null);
+ }
+
+ AppFGSTracker(Context context, AppRestrictionController controller,
+ Constructor<? extends Injector<AppFGSPolicy>> injector, Object outerContext) {
+ super(context, controller, injector, outerContext);
+ mHandler = new MyHandler(this);
+ mInjector.setPolicy(new AppFGSPolicy(mInjector, this));
+ }
+
+ @Override
+ void onSystemReady() {
+ super.onSystemReady();
+ mInjector.getActivityManagerInternal().addForegroundServiceStateListener(this);
+ }
+
+ @VisibleForTesting
+ void reset() {
+ mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS);
+ synchronized (mLock) {
+ mPkgFgsDurations.clear();
+ }
+ }
+
+ private void handleForegroundServicesChanged(String packageName, int pid, int uid,
+ boolean started) {
+ if (!mInjector.getPolicy().isEnabled() || mInjector.getPolicy().shouldExemptUid(uid)) {
+ return;
+ }
+ final long now = SystemClock.elapsedRealtime();
+ boolean longRunningFGSGone = false;
+ synchronized (mLock) {
+ PackageDurations pkg = mPkgFgsDurations.get(packageName, uid);
+ if (pkg == null) {
+ pkg = new PackageDurations(uid, packageName);
+ mPkgFgsDurations.put(packageName, uid, pkg);
+ }
+ final boolean wasLongRunning = pkg.isLongRunning();
+ pkg.addEvent(started, now);
+ longRunningFGSGone = wasLongRunning && !pkg.hasForegroundServices();
+ if (longRunningFGSGone) {
+ pkg.setIsLongRunning(false);
+ }
+ // Reschedule the checks.
+ scheduleDurationCheckLocked(now);
+ }
+ if (longRunningFGSGone) {
+ // The long-running FGS is gone, cancel the notification.
+ mInjector.getPolicy().onLongRunningFgsGone(packageName, uid);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleDurationCheckLocked(long now) {
+ // Look for the active FGS with longest running time till now.
+ final ArrayMap<String, SparseArray<PackageDurations>> map = mPkgFgsDurations.getMap();
+ long longest = -1;
+ for (int i = map.size() - 1; i >= 0; i--) {
+ final SparseArray<PackageDurations> val = map.valueAt(i);
+ for (int j = val.size() - 1; j >= 0; j--) {
+ final PackageDurations pkg = val.valueAt(j);
+ if (!pkg.hasForegroundServices() || pkg.isLongRunning()) {
+ // No FGS or it's a known long-running FGS, ignore it.
+ continue;
+ }
+ longest = Math.max(pkg.getTotalDurations(now), longest);
+ }
+ }
+ // Schedule a check in the future.
+ mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS);
+ if (longest >= 0) {
+ final long future = Math.max(0,
+ mInjector.getPolicy().getFgsLongRunningThreshold() - longest);
+ if (DEBUG_BACKGROUND_FGS_TRACKER) {
+ Slog.i(TAG, "Scheduling a FGS duration check at "
+ + TimeUtils.formatDuration(future));
+ }
+ mHandler.sendEmptyMessageDelayed(MyHandler.MSG_CHECK_LONG_RUNNING_FGS, future);
+ } else if (DEBUG_BACKGROUND_FGS_TRACKER) {
+ Slog.i(TAG, "Not scheduling FGS duration check");
+ }
+ }
+
+ private void checkLongRunningFgs() {
+ final AppFGSPolicy policy = mInjector.getPolicy();
+ final ArraySet<PackageDurations> pkgWithLongFgs = mTmpPkgDurations;
+ final long now = SystemClock.elapsedRealtime();
+ final long threshold = policy.getFgsLongRunningThreshold();
+ final long windowSize = policy.getFgsLongRunningWindowSize();
+ final long trimTo = Math.max(0, now - windowSize);
+
+ synchronized (mLock) {
+ final ArrayMap<String, SparseArray<PackageDurations>> map = mPkgFgsDurations.getMap();
+ for (int i = map.size() - 1; i >= 0; i--) {
+ final SparseArray<PackageDurations> val = map.valueAt(i);
+ for (int j = val.size() - 1; j >= 0; j--) {
+ final PackageDurations pkg = val.valueAt(j);
+ if (pkg.hasForegroundServices() && !pkg.isLongRunning()) {
+ final long totalDuration = pkg.getTotalDurations(now);
+ if (totalDuration >= threshold) {
+ pkgWithLongFgs.add(pkg);
+ pkg.setIsLongRunning(true);
+ if (DEBUG_BACKGROUND_FGS_TRACKER) {
+ Slog.i(TAG, pkg.mPackageName
+ + "/" + UserHandle.formatUid(pkg.mUid)
+ + " has FGS running for "
+ + TimeUtils.formatDuration(totalDuration)
+ + " over " + TimeUtils.formatDuration(windowSize));
+ }
+ }
+ }
+ // Trim the duration list, we don't need to keep track of all old records.
+ pkg.trim(trimTo);
+ }
+ }
+ }
+
+ for (int i = pkgWithLongFgs.size() - 1; i >= 0; i--) {
+ final PackageDurations pkg = pkgWithLongFgs.valueAt(i);
+ policy.onLongRunningFgs(pkg.mPackageName, pkg.mUid);
+ }
+ pkgWithLongFgs.clear();
+
+ synchronized (mLock) {
+ scheduleDurationCheckLocked(now);
+ }
+ }
+
+ private void onBgFgsMonitorEnabled(boolean enabled) {
+ if (enabled) {
+ synchronized (mLock) {
+ scheduleDurationCheckLocked(SystemClock.elapsedRealtime());
+ }
+ } else {
+ mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS);
+ synchronized (mLock) {
+ mPkgFgsDurations.clear();
+ }
+ }
+ }
+
+ private void onBgFgsLongRunningThresholdChanged() {
+ synchronized (mLock) {
+ if (mInjector.getPolicy().isEnabled()) {
+ scheduleDurationCheckLocked(SystemClock.elapsedRealtime());
+ }
+ }
+ }
+
+ @Override
+ void onUidRemoved(final int uid) {
+ synchronized (mLock) {
+ final ArrayMap<String, SparseArray<PackageDurations>> map = mPkgFgsDurations.getMap();
+ for (int i = map.size() - 1; i >= 0; i--) {
+ final SparseArray<PackageDurations> val = map.valueAt(i);
+ final int index = val.indexOfKey(uid);
+ if (index >= 0) {
+ val.removeAt(index);
+ if (val.size() == 0) {
+ map.removeAt(i);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ void onUserRemoved(final @UserIdInt int userId) {
+ synchronized (mLock) {
+ final ArrayMap<String, SparseArray<PackageDurations>> map = mPkgFgsDurations.getMap();
+ for (int i = map.size() - 1; i >= 0; i--) {
+ final SparseArray<PackageDurations> val = map.valueAt(i);
+ for (int j = val.size() - 1; j >= 0; j--) {
+ final int uid = val.keyAt(j);
+ if (UserHandle.getUserId(uid) == userId) {
+ val.removeAt(j);
+ }
+ }
+ if (val.size() == 0) {
+ map.removeAt(i);
+ }
+ }
+ }
+ }
+
+ /**
+ * Tracks the durations with active FGS for a given package.
+ */
+ static class PackageDurations {
+ final int mUid;
+ final String mPackageName;
+
+ /**
+ * A list of timestamps when the FGS start/stop occurred, we may trim/modify the start time
+ * in this list, so don't use this timestamp anywhere else.
+ */
+ final LinkedList<Long> mStartStopTime = new LinkedList<>();
+
+ private long mKnownDuration;
+ private int mNest; // A counter to track in case that the package gets multiple FGS starts.
+ private boolean mIsLongRunning;
+
+ PackageDurations(int uid, String packageName) {
+ mUid = uid;
+ mPackageName = packageName;
+ }
+
+ void addEvent(boolean startFgs, long now) {
+ final int size = mStartStopTime.size();
+ final boolean hasForegroundServices = hasForegroundServices();
+
+ if (startFgs) {
+ mNest++;
+ } else {
+ if (DEBUG_BACKGROUND_FGS_TRACKER && mNest <= 0) {
+ Slog.wtf(TAG, "Under-counted FGS start event mNest=" + mNest);
+ return;
+ }
+ mNest--;
+ if (mNest == 0) {
+ mKnownDuration += now - mStartStopTime.getLast();
+ mIsLongRunning = false;
+ }
+ }
+ if (startFgs == hasForegroundServices) {
+ // It's actually the same state as we have now, don't record the event time.
+ return;
+ }
+ if (DEBUG_BACKGROUND_FGS_TRACKER) {
+ if (startFgs != ((mStartStopTime.size() & 1) == 0)) {
+ Slog.wtf(TAG, "Unmatched start/stop event, current=" + mStartStopTime.size());
+ return;
+ }
+ }
+ mStartStopTime.add(now);
+ }
+
+ void setIsLongRunning(boolean isLongRunning) {
+ mIsLongRunning = isLongRunning;
+ }
+
+ boolean isLongRunning() {
+ return mIsLongRunning;
+ }
+
+ /**
+ * Remove/trim earlier durations with start time older than the given timestamp.
+ */
+ void trim(long earliest) {
+ while (mStartStopTime.size() > 1) {
+ final long current = mStartStopTime.peek();
+ if (current >= earliest) {
+ break; // All we have are newer than the given timestamp.
+ }
+ // Check the timestamp of FGS stop event.
+ if (mStartStopTime.get(1) > earliest) {
+ // Trim the duration by moving the start time.
+ mStartStopTime.set(0, earliest);
+ break;
+ }
+ // Discard the 1st duration as it's older than the given timestamp.
+ mStartStopTime.pop();
+ mStartStopTime.pop();
+ }
+ mKnownDuration = 0;
+ if (mStartStopTime.size() == 1) {
+ // Trim the duration by moving the start time.
+ mStartStopTime.set(0, Math.max(earliest, mStartStopTime.peek()));
+ return;
+ }
+ // Update the known durations.
+ int index = 0;
+ long last = 0;
+ for (long timestamp : mStartStopTime) {
+ if ((index & 1) == 1) {
+ mKnownDuration += timestamp - last;
+ } else {
+ last = timestamp;
+ }
+ index++;
+ }
+ }
+
+ long getTotalDurations(long now) {
+ return hasForegroundServices()
+ ? mKnownDuration + (now - mStartStopTime.getLast()) : mKnownDuration;
+ }
+
+ boolean hasForegroundServices() {
+ return mNest > 0;
+ }
+
+ @Override
+ public String toString() {
+ return mPackageName + "/" + UserHandle.formatUid(mUid)
+ + " hasForegroundServices=" + hasForegroundServices()
+ + " totalDurations=" + getTotalDurations(SystemClock.elapsedRealtime());
+ }
+ }
+
+ static final class AppFGSPolicy extends BaseAppStatePolicy<AppFGSTracker> {
+ /**
+ * Whether or not we should enable the monitoring on abusive FGS.
+ */
+ static final String KEY_BG_FGS_MONITOR_ENABLED =
+ DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_monitor_enabled";
+
+ /**
+ * The size of the sliding window in which the accumulated FGS durations are checked
+ * against the threshold.
+ */
+ static final String KEY_BG_FGS_LONG_RUNNING_WINDOW =
+ DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_long_running_window";
+
+ /**
+ * The threshold at where the accumulated FGS durations are considered as "long-running"
+ * within the given window.
+ */
+ static final String KEY_BG_FGS_LONG_RUNNING_THRESHOLD =
+ DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_long_running_threshold";
+
+ /**
+ * Default value to {@link #mBgFgsMonitorEnabled}.
+ */
+ static final boolean DEFAULT_BG_FGS_MONITOR_ENABLED = true;
+
+ /**
+ * Default value to {@link #mBgFgsLongRunningWindowMs}.
+ */
+ static final long DEFAULT_BG_FGS_LONG_RUNNING_WINDOW = ONE_DAY;
+
+ /**
+ * Default value to {@link #mBgFgsLongRunningThresholdMs}.
+ */
+ static final long DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD = 20 * ONE_HOUR;
+
+ /**
+ * @see #KEY_BG_FGS_MONITOR_ENABLED.
+ */
+ private volatile boolean mBgFgsMonitorEnabled = DEFAULT_BG_FGS_MONITOR_ENABLED;
+
+ /**
+ * @see #KEY_BG_FGS_LONG_RUNNING_WINDOW.
+ */
+ private volatile long mBgFgsLongRunningWindowMs = DEFAULT_BG_FGS_LONG_RUNNING_WINDOW;
+
+ /**
+ * @see #KEY_BG_FGS_LONG_RUNNING_THRESHOLD.
+ */
+ private volatile long mBgFgsLongRunningThresholdMs = DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD;
+
+ @NonNull
+ private final Object mLock;
+
+ AppFGSPolicy(@NonNull Injector injector, @NonNull AppFGSTracker tracker) {
+ super(injector, tracker);
+ mLock = tracker.mLock;
+ }
+
+ @Override
+ public void onSystemReady() {
+ updateBgFgsMonitorEnabled();
+ updateBgFgsLongRunningThreshold();
+ }
+
+ @Override
+ public void onPropertiesChanged(String name) {
+ switch (name) {
+ case KEY_BG_FGS_MONITOR_ENABLED:
+ updateBgFgsMonitorEnabled();
+ break;
+ case KEY_BG_FGS_LONG_RUNNING_WINDOW:
+ case KEY_BG_FGS_LONG_RUNNING_THRESHOLD:
+ updateBgFgsLongRunningThreshold();
+ break;
+ }
+ }
+
+ private void updateBgFgsMonitorEnabled() {
+ final boolean enabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_BG_FGS_MONITOR_ENABLED,
+ DEFAULT_BG_FGS_MONITOR_ENABLED);
+ if (enabled != mBgFgsMonitorEnabled) {
+ mBgFgsMonitorEnabled = enabled;
+ mTracker.onBgFgsMonitorEnabled(enabled);
+ }
+ }
+
+ private void updateBgFgsLongRunningThreshold() {
+ final long window = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_BG_FGS_LONG_RUNNING_WINDOW,
+ DEFAULT_BG_FGS_LONG_RUNNING_WINDOW);
+ final long threshold = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_BG_FGS_LONG_RUNNING_THRESHOLD,
+ DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD);
+ if (threshold != mBgFgsLongRunningThresholdMs || window != mBgFgsLongRunningWindowMs) {
+ mBgFgsLongRunningWindowMs = window;
+ mBgFgsLongRunningThresholdMs = threshold;
+ mTracker.onBgFgsLongRunningThresholdChanged();
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mBgFgsMonitorEnabled;
+ }
+
+ long getFgsLongRunningThreshold() {
+ return mBgFgsLongRunningThresholdMs;
+ }
+
+ long getFgsLongRunningWindowSize() {
+ return mBgFgsLongRunningWindowMs;
+ }
+
+ void onLongRunningFgs(String packageName, int uid) {
+ mTracker.mAppRestrictionController.postLongRunningFgsIfNecessary(packageName, uid);
+ }
+
+ void onLongRunningFgsGone(String packageName, int uid) {
+ mTracker.mAppRestrictionController
+ .cancelLongRunningFGSNotificationIfNecessary(packageName, uid);
+ }
+
+ @Override
+ public @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid) {
+ return RESTRICTION_LEVEL_UNKNOWN;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index f1d48d4e02c3..72f2c91ab304 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -44,6 +44,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
import static android.app.usage.UsageStatsManager.reasonToString;
+import static android.content.Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER;
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;
@@ -54,6 +55,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -108,6 +110,8 @@ import com.android.server.usage.AppStandbyInternal;
import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -175,8 +179,8 @@ public final class AppRestrictionController {
private @ElapsedRealtimeLong long mLevelChangeTimeElapsed;
private int mReason;
- @ElapsedRealtimeLong long mLastNotificationShownTimeElapsed;
- int mNotificationId;
+ private @ElapsedRealtimeLong long[] mLastNotificationShownTimeElapsed;
+ private int[] mNotificationId;
PkgSettings(String packageName, int uid) {
mPackageName = packageName;
@@ -222,9 +226,16 @@ public final class AppRestrictionController {
}
pw.print(" levelChange=");
TimeUtils.formatDuration(mLevelChangeTimeElapsed - nowElapsed, pw);
- if (mLastNotificationShownTimeElapsed > 0) {
- pw.print(" lastNoti=");
- TimeUtils.formatDuration(mLastNotificationShownTimeElapsed - nowElapsed, pw);
+ if (mLastNotificationShownTimeElapsed != null) {
+ for (int i = 0; i < mLastNotificationShownTimeElapsed.length; i++) {
+ if (mLastNotificationShownTimeElapsed[i] > 0) {
+ pw.print(" lastNoti(");
+ pw.print(mNotificationHelper.notificationTypeToString(i));
+ pw.print(")=");
+ TimeUtils.formatDuration(
+ mLastNotificationShownTimeElapsed[i] - nowElapsed, pw);
+ }
+ }
}
}
@@ -247,6 +258,38 @@ public final class AppRestrictionController {
int getReason() {
return mReason;
}
+
+ @ElapsedRealtimeLong long getLastNotificationTime(
+ @NotificationHelper.NotificationType int notificationType) {
+ if (mLastNotificationShownTimeElapsed == null) {
+ return 0;
+ }
+ return mLastNotificationShownTimeElapsed[notificationType];
+ }
+
+ void setLastNotificationTime(@NotificationHelper.NotificationType int notificationType,
+ @ElapsedRealtimeLong long timestamp) {
+ if (mLastNotificationShownTimeElapsed == null) {
+ mLastNotificationShownTimeElapsed =
+ new long[NotificationHelper.NOTIFICATION_TYPE_LAST];
+ }
+ mLastNotificationShownTimeElapsed[notificationType] = timestamp;
+ }
+
+ int getNotificationId(@NotificationHelper.NotificationType int notificationType) {
+ if (mNotificationId == null) {
+ return 0;
+ }
+ return mNotificationId[notificationType];
+ }
+
+ void setNotificationId(@NotificationHelper.NotificationType int notificationType,
+ int notificationId) {
+ if (mNotificationId == null) {
+ mNotificationId = new int[NotificationHelper.NOTIFICATION_TYPE_LAST];
+ }
+ mNotificationId[notificationType] = notificationId;
+ }
}
/**
@@ -371,6 +414,13 @@ public final class AppRestrictionController {
}
}
+ @VisibleForTesting
+ void reset() {
+ synchronized (mLock) {
+ mRestrictionLevels.clear();
+ }
+ }
+
@GuardedBy("mLock")
void dumpLocked(PrintWriter pw, String prefix) {
final ArrayList<PkgSettings> settings = new ArrayList<>();
@@ -551,7 +601,7 @@ public final class AppRestrictionController {
this(new Injector(context));
}
- AppRestrictionController(Injector injector) {
+ AppRestrictionController(final Injector injector) {
mInjector = injector;
mContext = injector.getContext();
mBgHandlerThread = new HandlerThread("bgres-controller");
@@ -577,6 +627,12 @@ public final class AppRestrictionController {
}
}
+ @VisibleForTesting
+ void resetRestrictionSettings() {
+ mRestrictionSettings.reset();
+ initRestrictionStates();
+ }
+
private void initRestrictionStates() {
final int[] allUsers = mInjector.getUserManagerInternal().getUserIds();
for (int userId : allUsers) {
@@ -920,6 +976,26 @@ public final class AppRestrictionController {
static final int SUMMARY_NOTIFICATION_ID = SystemMessage.NOTE_ABUSIVE_BG_APPS_BASE;
+ static final int NOTIFICATION_TYPE_ABUSIVE_CURRENT_DRAIN = 0;
+ static final int NOTIFICATION_TYPE_LONG_RUNNING_FGS = 1;
+ static final int NOTIFICATION_TYPE_LAST = 2;
+
+ @IntDef(prefix = { "NOTIFICATION_TYPE_"}, value = {
+ NOTIFICATION_TYPE_ABUSIVE_CURRENT_DRAIN,
+ NOTIFICATION_TYPE_LONG_RUNNING_FGS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ static @interface NotificationType{}
+
+ static final String[] NOTIFICATION_TYPE_STRINGS = {
+ "Abusive current drain",
+ "Long-running FGS",
+ };
+
+ static String notificationTypeToString(@NotificationType int notificationType) {
+ return NOTIFICATION_TYPE_STRINGS[notificationType];
+ }
+
private final AppRestrictionController mBgController;
private final NotificationManager mNotificationManager;
private final Injector mInjector;
@@ -938,55 +1014,89 @@ public final class AppRestrictionController {
}
void postRequestBgRestrictedIfNecessary(String packageName, int uid) {
- int notificationId;
+ 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,
+ UserHandle.of(UserHandle.getUserId(uid)));
+ postNotificationIfNecessary(NOTIFICATION_TYPE_ABUSIVE_CURRENT_DRAIN,
+ com.android.internal.R.string.notification_title_abusive_bg_apps,
+ com.android.internal.R.string.notification_content_abusive_bg_apps,
+ pendingIntent, packageName, uid);
+ }
+
+ void postLongRunningFgsIfNecessary(String packageName, int uid) {
+ final Intent intent = new Intent(ACTION_SHOW_FOREGROUND_SERVICE_MANAGER);
+ intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+
+ final PendingIntent pendingIntent = PendingIntent.getBroadcastAsUser(mContext, 0,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
+ UserHandle.of(UserHandle.getUserId(uid)));
+ postNotificationIfNecessary(NOTIFICATION_TYPE_LONG_RUNNING_FGS,
+ com.android.internal.R.string.notification_title_abusive_bg_apps,
+ com.android.internal.R.string.notification_content_long_running_fgs,
+ pendingIntent, packageName, uid);
+ }
+
+ int getNotificationIdIfNecessary(@NotificationType int notificationType,
+ String packageName, int uid) {
synchronized (mLock) {
final RestrictionSettings.PkgSettings settings = mBgController.mRestrictionSettings
.getRestrictionSettingsLocked(uid, packageName);
final long now = SystemClock.elapsedRealtime();
- if (settings.mLastNotificationShownTimeElapsed != 0
- && (settings.mLastNotificationShownTimeElapsed
+ final long lastNotificationShownTimeElapsed =
+ settings.getLastNotificationTime(notificationType);
+ if (lastNotificationShownTimeElapsed != 0 && (lastNotificationShownTimeElapsed
+ 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)
+ + TimeUtils.formatDuration(now - lastNotificationShownTimeElapsed)
+ " ago");
}
- return;
+ return 0;
+ }
+ settings.setLastNotificationTime(notificationType, now);
+ int notificationId = settings.getNotificationId(notificationType);
+ if (notificationId <= 0) {
+ notificationId = mNotificationIDStepper++;
+ settings.setNotificationId(notificationType, notificationId);
}
if (DEBUG_BG_RESTRICTION_CONTROLLER) {
Slog.i(TAG, "Showing notification for " + packageName
+ "/" + UserHandle.formatUid(uid)
+ + ", id=" + notificationId
+ ", now=" + now
- + ", lastShown=" + settings.mLastNotificationShownTimeElapsed);
- }
- settings.mLastNotificationShownTimeElapsed = now;
- if (settings.mNotificationId == 0) {
- settings.mNotificationId = mNotificationIDStepper++;
+ + ", lastShown=" + lastNotificationShownTimeElapsed);
}
- notificationId = settings.mNotificationId;
+ return notificationId;
}
+ }
- final UserHandle targetUser = UserHandle.of(UserHandle.getUserId(uid));
-
- postSummaryNotification(targetUser);
+ void postNotificationIfNecessary(@NotificationType int notificationType, int titleRes,
+ int messageRes, PendingIntent pendingIntent, String packageName, int uid) {
+ int notificationId = getNotificationIdIfNecessary(notificationType, packageName, uid);
+ if (notificationId <= 0) {
+ return;
+ }
- final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
- final ApplicationInfo ai = pm.getApplicationInfo(packageName, STOCK_PM_FLAGS,
+ final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
+ final PackageManager pm = mInjector.getPackageManager();
+ final ApplicationInfo ai = pmi.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 String title = mContext.getString(titleRes);
+ final String message = mContext.getString(messageRes,
+ ai != null ? pm.getText(packageName, ai.labelRes, ai) : packageName);
+ final Icon icon = ai != null ? Icon.createWithResource(packageName, ai.icon) : null;
- 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);
+ postNotification(notificationId, packageName, uid, title, message, icon, pendingIntent);
+ }
+
+ void postNotification(int notificationId, String packageName, int uid, String title,
+ String message, Icon icon, PendingIntent pendingIntent) {
+ final UserHandle targetUser = UserHandle.of(UserHandle.getUserId(uid));
+ postSummaryNotification(targetUser);
final Notification.Builder notificationBuilder = new Notification.Builder(mContext,
ABUSIVE_BACKGROUND_APPS)
@@ -999,8 +1109,8 @@ public final class AppRestrictionController {
.setContentTitle(title)
.setContentText(message)
.setContentIntent(pendingIntent);
- if (ai != null) {
- notificationBuilder.setLargeIcon(Icon.createWithResource(packageName, ai.icon));
+ if (icon != null) {
+ notificationBuilder.setLargeIcon(icon);
}
final Notification notification = notificationBuilder.build();
@@ -1027,8 +1137,22 @@ public final class AppRestrictionController {
synchronized (mLock) {
final RestrictionSettings.PkgSettings settings = mBgController.mRestrictionSettings
.getRestrictionSettingsLocked(uid, packageName);
- if (settings.mNotificationId > 0) {
- mNotificationManager.cancel(settings.mNotificationId);
+ final int notificationId =
+ settings.getNotificationId(NOTIFICATION_TYPE_ABUSIVE_CURRENT_DRAIN);
+ if (notificationId > 0) {
+ mNotificationManager.cancel(notificationId);
+ }
+ }
+ }
+
+ void cancelLongRunningFGSNotificationIfNecessary(String packageName, int uid) {
+ synchronized (mLock) {
+ final RestrictionSettings.PkgSettings settings = mBgController.mRestrictionSettings
+ .getRestrictionSettingsLocked(uid, packageName);
+ final int notificationId =
+ settings.getNotificationId(NOTIFICATION_TYPE_LONG_RUNNING_FGS);
+ if (notificationId > 0) {
+ mNotificationManager.cancel(notificationId);
}
}
}
@@ -1110,6 +1234,14 @@ public final class AppRestrictionController {
return null;
}
+ void postLongRunningFgsIfNecessary(String packageName, int uid) {
+ mNotificationHelper.postLongRunningFgsIfNecessary(packageName, uid);
+ }
+
+ void cancelLongRunningFGSNotificationIfNecessary(String packageName, int uid) {
+ mNotificationHelper.cancelLongRunningFGSNotificationIfNecessary(packageName, uid);
+ }
+
static class BgHandler extends Handler {
static final int MSG_BACKGROUND_RESTRICTION_CHANGED = 0;
static final int MSG_APP_RESTRICTION_LEVEL_CHANGED = 1;
@@ -1183,6 +1315,7 @@ public final class AppRestrictionController {
void initAppStateTrackers(AppRestrictionController controller) {
mAppRestrictionController = controller;
controller.mAppStateTrackers.add(new AppBatteryTracker(mContext, controller));
+ controller.mAppStateTrackers.add(new AppFGSTracker(mContext, controller));
}
AppRestrictionController getAppRestrictionController() {
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 5e9232298df0..cd7f65fd8b95 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -91,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.AppFGSTracker.AppFGSPolicy;
import com.android.server.am.AppRestrictionController.NotificationHelper;
import com.android.server.apphibernation.AppHibernationManagerInternal;
import com.android.server.pm.UserManagerInternal;
@@ -105,6 +106,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
import java.time.Duration;
import java.util.ArrayList;
@@ -192,6 +194,7 @@ public final class BackgroundRestrictionTest {
private TestBgRestrictionInjector mInjector;
private AppRestrictionController mBgRestrictionController;
private AppBatteryTracker mAppBatteryTracker;
+ private AppFGSTracker mAppFGSTracker;
@Before
public void setUp() throws Exception {
@@ -628,7 +631,7 @@ public final class BackgroundRestrictionTest {
eq(testPkgName),
eq(testUid));
// Verify we have the notification posted.
- checkNotification(testPkgName);
+ checkNotificationShown(new String[] {testPkgName}, atLeast(1), true);
});
// Turn ON the FAS for real.
@@ -668,19 +671,189 @@ public final class BackgroundRestrictionTest {
}
}
- private void checkNotification(String packageName) throws Exception {
- final NotificationManager nm = mInjector.getNotificationManager();
+ @Test
+ public void testLongFGSMonitor() throws Exception {
+ final int testPkgIndex1 = 1;
+ final String testPkgName1 = TEST_PACKAGE_BASE + testPkgIndex1;
+ final int testUser1 = TEST_USER0;
+ final int testUid1 = UserHandle.getUid(testUser1, TEST_PACKAGE_APPID_BASE + testPkgIndex1);
+ final int testPid1 = 1234;
+
+ final int testPkgIndex2 = 2;
+ final String testPkgName2 = TEST_PACKAGE_BASE + testPkgIndex2;
+ final int testUser2 = TEST_USER0;
+ final int testUid2 = UserHandle.getUid(testUser2, TEST_PACKAGE_APPID_BASE + testPkgIndex2);
+ final int testPid2 = 1235;
+
+ final long windowMs = 2_000;
+ final long thresholdMs = 1_000;
+ final long shortMs = 100;
+
+ DeviceConfigSession<Boolean> longRunningFGSMonitor = null;
+ DeviceConfigSession<Long> longRunningFGSWindow = null;
+ DeviceConfigSession<Long> longRunningFGSThreshold = null;
+
+ try {
+ longRunningFGSMonitor = new DeviceConfigSession<>(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ AppFGSPolicy.KEY_BG_FGS_MONITOR_ENABLED,
+ DeviceConfig::getBoolean,
+ AppFGSPolicy.DEFAULT_BG_FGS_MONITOR_ENABLED);
+ longRunningFGSMonitor.set(true);
+
+ longRunningFGSWindow = new DeviceConfigSession<>(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ AppFGSPolicy.KEY_BG_FGS_LONG_RUNNING_WINDOW,
+ DeviceConfig::getLong,
+ AppFGSPolicy.DEFAULT_BG_FGS_LONG_RUNNING_WINDOW);
+ longRunningFGSWindow.set(windowMs);
+
+ longRunningFGSThreshold = new DeviceConfigSession<>(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ AppFGSPolicy.KEY_BG_FGS_LONG_RUNNING_THRESHOLD,
+ DeviceConfig::getLong,
+ AppFGSPolicy.DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD);
+ longRunningFGSThreshold.set(thresholdMs);
+
+ // Basic case
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, true);
+ // Verify we have the notification, it'll include the summary notification though.
+ int notificationId = checkNotificationShown(
+ new String[] {testPkgName1}, timeout(windowMs * 2).times(2), true)[0];
+
+ clearInvocations(mInjector.getNotificationManager());
+ // Sleep a while, verify it won't show another notification.
+ Thread.sleep(windowMs * 2);
+ checkNotificationShown(
+ new String[] {testPkgName1}, timeout(windowMs * 2).times(0), false);
+
+ // Stop this FGS
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, false);
+ checkNotificationGone(testPkgName1, timeout(windowMs), notificationId);
+
+ clearInvocations(mInjector.getNotificationManager());
+ // Start another one and stop it.
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, true);
+ Thread.sleep(shortMs);
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, false);
+
+ // Not long enough, it shouldn't show notification in this case.
+ checkNotificationShown(
+ new String[] {testPkgName2}, timeout(windowMs * 2).times(0), false);
+
+ clearInvocations(mInjector.getNotificationManager());
+ // Start the FGS again.
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, true);
+ // Verify we have the notification.
+ notificationId = checkNotificationShown(
+ new String[] {testPkgName2}, timeout(windowMs * 2).times(2), true)[0];
+
+ // Stop this FGS
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, false);
+ checkNotificationGone(testPkgName2, timeout(windowMs), notificationId);
+
+ // Start over with concurrent cases.
+ clearInvocations(mInjector.getNotificationManager());
+ mBgRestrictionController.resetRestrictionSettings();
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, true);
+ Thread.sleep(shortMs);
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, true);
+
+ // Verify we've seen both notifications, and test pkg2 should be shown before test pkg1.
+ int[] notificationIds = checkNotificationShown(
+ new String[] {testPkgName2, testPkgName1},
+ timeout(windowMs * 2).times(4), true);
+
+ // Stop both of them.
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, false);
+ checkNotificationGone(testPkgName1, timeout(windowMs), notificationIds[1]);
+ clearInvocations(mInjector.getNotificationManager());
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, false);
+ checkNotificationGone(testPkgName2, timeout(windowMs), notificationIds[0]);
+
+ // Test the interlaced case.
+ clearInvocations(mInjector.getNotificationManager());
+ mBgRestrictionController.resetRestrictionSettings();
+ mAppFGSTracker.reset();
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, true);
+
+ final long initialWaitMs = thresholdMs / 2;
+ Thread.sleep(initialWaitMs);
+
+ for (long remaining = thresholdMs - initialWaitMs; remaining > 0;) {
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, false);
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, true);
+ Thread.sleep(shortMs);
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, true);
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName2, testUid2,
+ testPid2, false);
+ Thread.sleep(shortMs);
+ remaining -= shortMs;
+ }
+
+ // Verify test pkg1 got the notification, but not test pkg2.
+ notificationId = checkNotificationShown(
+ new String[] {testPkgName1}, timeout(windowMs).times(2), true)[0];
+
+ clearInvocations(mInjector.getNotificationManager());
+ // Stop the FGS.
+ mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
+ testPid1, false);
+ checkNotificationGone(testPkgName1, timeout(windowMs), notificationId);
+ } finally {
+ closeIfNotNull(longRunningFGSMonitor);
+ closeIfNotNull(longRunningFGSWindow);
+ closeIfNotNull(longRunningFGSThreshold);
+ }
+ }
+
+ private int[] checkNotificationShown(String[] packageName, VerificationMode mode,
+ boolean verifyNotification) throws Exception {
final ArgumentCaptor<Integer> notificationIdCaptor =
ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Notification> notificationCaptor =
ArgumentCaptor.forClass(Notification.class);
- verify(mInjector.getNotificationManager(), atLeast(1)).notifyAsUser(any(),
+ verify(mInjector.getNotificationManager(), mode).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));
+ final int[] notificationId = new int[packageName.length];
+ if (verifyNotification) {
+ for (int i = 0, j = 0; i < packageName.length; j++) {
+ final int id = notificationIdCaptor.getAllValues().get(j);
+ if (id == NotificationHelper.SUMMARY_NOTIFICATION_ID) {
+ continue;
+ }
+ final Notification n = notificationCaptor.getAllValues().get(j);
+ notificationId[i] = id;
+ assertTrue(NotificationHelper.SUMMARY_NOTIFICATION_ID < notificationId[i]);
+ assertEquals(NotificationHelper.GROUP_KEY, n.getGroup());
+ assertEquals(ABUSIVE_BACKGROUND_APPS, n.getChannelId());
+ assertEquals(packageName[i], n.extras.getString(Intent.EXTRA_PACKAGE_NAME));
+ i++;
+ }
+ }
+ return notificationId;
+ }
+
+ private void checkNotificationGone(String packageName, VerificationMode mode,
+ int notificationId) throws Exception {
+ final ArgumentCaptor<Integer> notificationIdCaptor =
+ ArgumentCaptor.forClass(Integer.class);
+ verify(mInjector.getNotificationManager(), mode).cancel(notificationIdCaptor.capture());
+ assertEquals(notificationId, notificationIdCaptor.getValue().intValue());
}
private void closeIfNotNull(DeviceConfigSession<?> config) throws Exception {
@@ -788,6 +961,11 @@ public final class BackgroundRestrictionTest {
BackgroundRestrictionTest.class),
BackgroundRestrictionTest.this);
controller.addAppStateTracker(mAppBatteryTracker);
+ mAppFGSTracker = new AppFGSTracker(mContext, controller,
+ TestAppFGSTrackerInjector.class.getDeclaredConstructor(
+ BackgroundRestrictionTest.class),
+ BackgroundRestrictionTest.this);
+ controller.addAppStateTracker(mAppFGSTracker);
} catch (NoSuchMethodException e) {
// Won't happen.
}
@@ -884,4 +1062,7 @@ public final class BackgroundRestrictionTest {
private class TestAppBatteryTrackerInjector extends TestBaseTrackerInjector<AppBatteryPolicy> {
}
+
+ private class TestAppFGSTrackerInjector extends TestBaseTrackerInjector<AppFGSPolicy> {
+ }
}