diff options
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> { + } } |