Merge "Decouple the thresholds of the background restrictions" into tm-dev

This commit is contained in:
Jing Ji
2022-03-30 19:18:16 +00:00
committed by Android (Google) Code Review
5 changed files with 241 additions and 47 deletions

View File

@@ -20,6 +20,7 @@ import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.app.ActivityManager.RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_UNKNOWN;
import static android.app.ActivityManager.isLowRamDeviceStatic;
import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
@@ -1118,6 +1119,13 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
static final String KEY_BG_CURRENT_DRAIN_HIGH_THRESHOLD_BY_BG_LOCATION =
DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "current_drain_high_threshold_by_bg_location";
/**
* Whether or not the battery usage of the offending app should fulfill the 1st threshold
* before taking actions for the 2nd threshold.
*/
static final String KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS =
DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "current_drain_decouple_thresholds";
/**
* Default value to the {@link #INDEX_REGULAR_CURRENT_DRAIN_THRESHOLD} of
* the {@link #mBgCurrentDrainRestrictedBucketThreshold}.
@@ -1190,6 +1198,11 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
*/
final boolean mDefaultBgCurrentDrainHighThresholdByBgLocation;
/**
* Default value to {@link #mBgCurrentDrainDecoupleThresholds}.
*/
static final boolean DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD = true;
/**
* The index to {@link #mBgCurrentDrainRestrictedBucketThreshold}
* and {@link #mBgCurrentDrainBgRestrictedThreshold}.
@@ -1257,6 +1270,11 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
*/
volatile boolean mBgCurrentDrainHighThresholdByBgLocation;
/**
* @see #KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS.
*/
volatile boolean mBgCurrentDrainDecoupleThresholds;
/**
* The capacity of the battery when fully charged in mAh.
*/
@@ -1369,6 +1387,9 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
case KEY_BG_CURRENT_DRAIN_EXEMPTED_TYPES:
updateCurrentDrainExemptedTypes();
break;
case KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS:
updateCurrentDrainDecoupleThresholds();
break;
default:
super.onPropertiesChanged(name);
break;
@@ -1466,6 +1487,13 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
mDefaultBgCurrentDrainExemptedTypes);
}
private void updateCurrentDrainDecoupleThresholds() {
mBgCurrentDrainDecoupleThresholds = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS,
DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD);
}
@Override
public void onSystemReady() {
mBatteryFullChargeMah =
@@ -1477,20 +1505,30 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
updateCurrentDrainLocationMinDuration();
updateCurrentDrainEventDurationBasedThresholdEnabled();
updateCurrentDrainExemptedTypes();
updateCurrentDrainDecoupleThresholds();
}
@Override
public @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid) {
@RestrictionLevel
public int getProposedRestrictionLevel(String packageName, int uid,
@RestrictionLevel int maxLevel) {
if (maxLevel <= RESTRICTION_LEVEL_ADAPTIVE_BUCKET) {
return RESTRICTION_LEVEL_UNKNOWN;
}
synchronized (mLock) {
final int index = mHighBgBatteryPackages.indexOfKey(uid);
if (index < 0) {
// Not found, return adaptive as the default one.
return RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
final long[] ts = mHighBgBatteryPackages.get(uid);
if (ts != null) {
final int restrictedLevel = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET] > 0
? RESTRICTION_LEVEL_RESTRICTED_BUCKET
: RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
if (maxLevel > RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
return ts[TIME_STAMP_INDEX_BG_RESTRICTED] > 0
? RESTRICTION_LEVEL_BACKGROUND_RESTRICTED : restrictedLevel;
} else if (maxLevel == RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
return restrictedLevel;
}
}
final long[] ts = mHighBgBatteryPackages.valueAt(index);
return ts[TIME_STAMP_INDEX_BG_RESTRICTED] > 0
? RESTRICTION_LEVEL_BACKGROUND_RESTRICTED
: RESTRICTION_LEVEL_RESTRICTED_BUCKET;
return RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
}
}
@@ -1573,36 +1611,58 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
final int thresholdIndex = getCurrentDrainThresholdIndex(uid, now,
mBgCurrentDrainWindowMs);
final int index = mHighBgBatteryPackages.indexOfKey(uid);
final boolean decoupleThresholds = mBgCurrentDrainDecoupleThresholds;
final double rbThreshold = mBgCurrentDrainRestrictedBucketThreshold[thresholdIndex];
final double brThreshold = mBgCurrentDrainBgRestrictedThreshold[thresholdIndex];
if (index < 0) {
if (rbPercentage >= mBgCurrentDrainRestrictedBucketThreshold[thresholdIndex]) {
long[] ts = null;
if (rbPercentage >= rbThreshold) {
// New findings to us, track it and let the controller know.
final long[] ts = new long[TIME_STAMP_INDEX_LAST];
ts = new long[TIME_STAMP_INDEX_LAST];
ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET] = now;
mHighBgBatteryPackages.put(uid, ts);
notifyController = excessive = true;
}
if (decoupleThresholds && brPercentage >= brThreshold) {
if (ts == null) {
ts = new long[TIME_STAMP_INDEX_LAST];
mHighBgBatteryPackages.put(uid, ts);
}
ts[TIME_STAMP_INDEX_BG_RESTRICTED] = now;
notifyController = excessive = true;
}
} else {
final long[] ts = mHighBgBatteryPackages.valueAt(index);
if (rbPercentage < mBgCurrentDrainRestrictedBucketThreshold[thresholdIndex]) {
// it's actually back to normal, but we don't untrack it until
// explicit user interactions.
notifyController = true;
} else {
excessive = true;
if (brPercentage >= mBgCurrentDrainBgRestrictedThreshold[thresholdIndex]
&& curLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
// If we're in the restricted standby bucket but still seeing high
// current drains, tell the controller again.
final long lastResbucket = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET];
final long lastBgRes = ts[TIME_STAMP_INDEX_BG_RESTRICTED];
// If it has been a while since restricting the app and since the last
// time we notify the controller, notify it again.
if ((now >= lastResbucket + mBgCurrentDrainWindowMs) && (lastBgRes == 0
|| (now >= lastBgRes + mBgCurrentDrainWindowMs))) {
ts[TIME_STAMP_INDEX_BG_RESTRICTED] = now;
notifyController = true;
}
final long lastRestrictBucketTs = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET];
if (rbPercentage >= rbThreshold) {
if (lastRestrictBucketTs == 0) {
ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET] = now;
}
notifyController = excessive = true;
} else {
// It's actually back to normal, but we don't untrack it until
// explicit user interactions, because the restriction could be the cause
// of going back to normal.
}
if (brPercentage >= brThreshold) {
// If either
// a) It's configured to goto threshold 2 directly without threshold 1;
// b) It's already in the restricted standby bucket, but still seeing
// high current drains, and it's been a while since it's restricted;
// tell the controller.
notifyController = decoupleThresholds
|| (curLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET
&& (now > lastRestrictBucketTs + mBgCurrentDrainWindowMs));
if (notifyController) {
ts[TIME_STAMP_INDEX_BG_RESTRICTED] = now;
}
excessive = true;
} else {
// Reset the track now - if it's already background restricted, it requires
// user consent to unrestrict it; or if it's in restricted bucket level,
// resetting this won't lift it from that level.
ts[TIME_STAMP_INDEX_BG_RESTRICTED] = 0;
// Now need to notify the controller.
}
}
}

View File

@@ -24,6 +24,7 @@ import static android.app.ActivityManager.RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
import static android.app.ActivityManager.RESTRICTION_LEVEL_EXEMPTED;
import static android.app.ActivityManager.RESTRICTION_LEVEL_HIBERNATION;
import static android.app.ActivityManager.RESTRICTION_LEVEL_MAX;
import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_UNKNOWN;
import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
@@ -1152,7 +1153,8 @@ public final class AppRestrictionController {
? RESTRICTION_LEVEL_RESTRICTED_BUCKET
: RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
if (calcTrackers) {
@RestrictionLevel int l = calcAppRestrictionLevelFromTackers(uid, packageName);
@RestrictionLevel int l = calcAppRestrictionLevelFromTackers(uid, packageName,
RESTRICTION_LEVEL_MAX);
if (l == RESTRICTION_LEVEL_EXEMPTED) {
return RESTRICTION_LEVEL_EXEMPTED;
}
@@ -1164,7 +1166,8 @@ public final class AppRestrictionController {
uid, 0, packageName).sendToTarget();
}
// Lower the level.
level = RESTRICTION_LEVEL_RESTRICTED_BUCKET;
level = calcAppRestrictionLevelFromTackers(uid, packageName,
RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
}
}
break;
@@ -1181,12 +1184,13 @@ public final class AppRestrictionController {
* monitors certain dimensions of the app, the abusive behaviors could be detected in one or
* more of these dimensions, but not necessarily all of them. </p>
*/
private @RestrictionLevel int calcAppRestrictionLevelFromTackers(int uid, String packageName) {
private @RestrictionLevel int calcAppRestrictionLevelFromTackers(int uid, String packageName,
@RestrictionLevel int maxLevel) {
@RestrictionLevel int level = RESTRICTION_LEVEL_UNKNOWN;
final boolean isRestrictedBucketEnabled = mConstantsObserver.mRestrictedBucketEnabled;
for (int i = mAppStateTrackers.size() - 1; i >= 0; i--) {
@RestrictionLevel int l = mAppStateTrackers.get(i).getPolicy()
.getProposedRestrictionLevel(packageName, uid);
.getProposedRestrictionLevel(packageName, uid, maxLevel);
if (!isRestrictedBucketEnabled && l == RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
l = RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
}
@@ -1475,17 +1479,15 @@ public final class AppRestrictionController {
} else if (curLevel >= RESTRICTION_LEVEL_RESTRICTED_BUCKET
&& level < RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
// Moved out of the background-restricted state.
if (curBucket != STANDBY_BUCKET_RARE) {
synchronized (mSettingsLock) {
final int index = mActiveUids.indexOfKey(uid, pkgName);
if (index >= 0) {
mActiveUids.add(uid, pkgName, null);
}
synchronized (mSettingsLock) {
final int index = mActiveUids.indexOfKey(uid, pkgName);
if (index >= 0) {
mActiveUids.add(uid, pkgName, null);
}
appStandbyInternal.maybeUnrestrictApp(pkgName, UserHandle.getUserId(uid),
prevReason & REASON_MAIN_MASK, prevReason & REASON_SUB_MASK,
reason, subReason);
}
appStandbyInternal.maybeUnrestrictApp(pkgName, UserHandle.getUserId(uid),
prevReason & REASON_MAIN_MASK, prevReason & REASON_SUB_MASK,
reason, subReason);
}
}

View File

@@ -87,9 +87,11 @@ public abstract class BaseAppStatePolicy<T extends BaseAppStateTracker> {
}
/**
* @return The proposed background restriction policy for the given package/uid.
* @return The proposed background restriction policy for the given package/uid,
* the returned level should be capped at {@code maxLevel} (exclusive).
*/
public @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid) {
public @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid,
@RestrictionLevel int maxLevel) {
return RESTRICTION_LEVEL_UNKNOWN;
}

View File

@@ -18,6 +18,7 @@ package com.android.server.am;
import static android.app.ActivityManager.RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
import static android.app.ActivityManager.RESTRICTION_LEVEL_UNKNOWN;
import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE;
@@ -295,11 +296,19 @@ abstract class BaseAppStateTimeSlotEventsTracker
}
@Override
public @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid) {
@RestrictionLevel
public int getProposedRestrictionLevel(String packageName, int uid,
@RestrictionLevel int maxLevel) {
synchronized (mLock) {
return mExcessiveEventPkgs.get(packageName, uid) == null
final int level = mExcessiveEventPkgs.get(packageName, uid) == null
? RESTRICTION_LEVEL_ADAPTIVE_BUCKET
: RESTRICTION_LEVEL_RESTRICTED_BUCKET;
if (maxLevel > RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
return level;
} else if (maxLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
return RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
}
return RESTRICTION_LEVEL_UNKNOWN;
}
}

View File

@@ -579,6 +579,7 @@ public final class BackgroundRestrictionTest {
DeviceConfigSession<Boolean> bgPromptFgsWithNotiToBgRestricted = null;
DeviceConfigSession<Long> bgNotificationMinInterval = null;
DeviceConfigSession<Integer> bgBatteryExemptionTypes = null;
DeviceConfigSession<Boolean> bgCurrentDrainDecoupleThresholds = null;
mBgRestrictionController.addAppBackgroundRestrictionListener(listener);
@@ -648,6 +649,13 @@ public final class BackgroundRestrictionTest {
R.integer.config_bg_current_drain_exempted_types));
bgBatteryExemptionTypes.set(0);
bgCurrentDrainDecoupleThresholds = new DeviceConfigSession<>(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS,
DeviceConfig::getBoolean,
AppBatteryPolicy.DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD);
bgCurrentDrainDecoupleThresholds.set(true);
mCurrentTimeMillis = 10_000L;
doReturn(mCurrentTimeMillis - windowMs).when(stats).getStatsStartTimestamp();
doReturn(mCurrentTimeMillis).when(stats).getStatsEndTimestamp();
@@ -785,6 +793,11 @@ public final class BackgroundRestrictionTest {
anyInt(), anyInt());
});
// Pretend we have the standby buckets set above.
doReturn(STANDBY_BUCKET_RESTRICTED)
.when(mAppStandbyInternal)
.getAppStandbyBucket(eq(testPkgName), eq(testUser), anyLong(), anyBoolean());
// Sleep a while and set a higher drain
Thread.sleep(windowMs);
clearInvocations(mInjector.getAppStandbyInternal());
@@ -921,6 +934,11 @@ public final class BackgroundRestrictionTest {
// Expected.
}
// Reset the standby bucket.
doReturn(STANDBY_BUCKET_RARE)
.when(mAppStandbyInternal)
.getAppStandbyBucket(eq(testPkgName), eq(testUser), anyLong(), anyBoolean());
// Turn OFF the FAS.
listener.mLatchHolder[0] = new CountDownLatch(1);
clearInvocations(mInjector.getAppStandbyInternal());
@@ -930,6 +948,99 @@ public final class BackgroundRestrictionTest {
// It'll go back to restricted bucket because it used to behave poorly.
listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_RESTRICTED_BUCKET);
verifyRestrictionLevel(RESTRICTION_LEVEL_RESTRICTED_BUCKET, testPkgName, testUid);
clearInvocations(mInjector.getAppStandbyInternal());
// Trigger user interaction.
runTestBgCurrentDrainMonitorOnce(listener, stats, uids,
new double[]{restrictBucketThresholdMah - 1, 0},
new double[]{0, restrictBucketThresholdMah - 1}, zeros, zeros,
() -> {
doReturn(mCurrentTimeMillis).when(stats).getStatsStartTimestamp();
doReturn(mCurrentTimeMillis + windowMs)
.when(stats).getStatsEndTimestamp();
mCurrentTimeMillis += windowMs + 1;
mIdleStateListener.onUserInteractionStarted(testPkgName, testUser);
waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
// It should have been back to normal.
listener.verify(timeout, testUid, testPkgName,
RESTRICTION_LEVEL_ADAPTIVE_BUCKET);
verify(mInjector.getAppStandbyInternal(), atLeast(1)).maybeUnrestrictApp(
eq(testPkgName),
eq(testUser),
eq(REASON_MAIN_USAGE),
eq(REASON_SUB_USAGE_USER_INTERACTION),
eq(REASON_MAIN_USAGE),
eq(REASON_SUB_USAGE_USER_INTERACTION));
});
bgCurrentDrainDecoupleThresholds.set(true);
clearInvocations(mInjector.getAppStandbyInternal());
// Go to the threshold right away.
runTestBgCurrentDrainMonitorOnce(listener, stats, uids,
new double[]{0, restrictBucketThresholdMah - 1},
new double[]{bgRestrictedThresholdMah + 1, 0}, zeros, zeros,
() -> {
doReturn(mCurrentTimeMillis).when(stats).getStatsStartTimestamp();
doReturn(mCurrentTimeMillis + windowMs)
.when(stats).getStatsEndTimestamp();
mCurrentTimeMillis += windowMs + 1;
// We won't change restriction level automatically because it needs
// user consent.
try {
listener.verify(timeout, testUid, testPkgName,
RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
fail("There shouldn't be level change event like this");
} catch (Exception e) {
// Expected.
}
verify(mInjector.getAppStandbyInternal(), never()).setAppStandbyBucket(
eq(testPkgName),
eq(STANDBY_BUCKET_RARE),
eq(testUser),
anyInt(), anyInt());
// We should have requested to goto background restricted level.
verify(mBgRestrictionController, times(1)).handleRequestBgRestricted(
eq(testPkgName),
eq(testUid));
// Verify we have the notification posted now because its FGS is invisible.
checkNotificationShown(new String[] {testPkgName}, atLeast(1), true);
});
bgCurrentDrainDecoupleThresholds.set(false);
clearInvocations(mInjector.getAppStandbyInternal());
clearInvocations(mBgRestrictionController);
// Go to the threshold right away, but this time, it shouldn't even request to goto
// bg restricted level because it requires to be in restricted bucket before that.
runTestBgCurrentDrainMonitorOnce(listener, stats, uids,
new double[]{0, restrictBucketThresholdMah - 1},
new double[]{bgRestrictedThresholdMah + 1, 0}, zeros, zeros,
() -> {
doReturn(mCurrentTimeMillis).when(stats).getStatsStartTimestamp();
doReturn(mCurrentTimeMillis + windowMs)
.when(stats).getStatsEndTimestamp();
mCurrentTimeMillis += windowMs + 1;
// We won't change restriction level automatically because it needs
// user consent.
try {
listener.verify(timeout, testUid, testPkgName,
RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
fail("There shouldn't be level change event like this");
} catch (Exception e) {
// Expected.
}
verify(mInjector.getAppStandbyInternal(), never()).setAppStandbyBucket(
eq(testPkgName),
eq(STANDBY_BUCKET_RARE),
eq(testUser),
anyInt(), anyInt());
// We should NOT have requested to goto background restricted level.
verify(mBgRestrictionController, never()).handleRequestBgRestricted(
eq(testPkgName),
eq(testUid));
});
} finally {
closeIfNotNull(bgCurrentDrainMonitor);
closeIfNotNull(bgCurrentDrainWindow);
@@ -938,6 +1049,7 @@ public final class BackgroundRestrictionTest {
closeIfNotNull(bgPromptFgsWithNotiToBgRestricted);
closeIfNotNull(bgNotificationMinInterval);
closeIfNotNull(bgBatteryExemptionTypes);
closeIfNotNull(bgCurrentDrainDecoupleThresholds);
}
}
@@ -1441,6 +1553,7 @@ public final class BackgroundRestrictionTest {
DeviceConfigSession<Boolean> bgPermissionMonitorEnabled = null;
DeviceConfigSession<String> bgPermissionsInMonitor = null;
DeviceConfigSession<Boolean> bgCurrentDrainHighThresholdByBgLocation = null;
DeviceConfigSession<Boolean> bgCurrentDrainDecoupleThresholds = null;
mBgRestrictionController.addAppBackgroundRestrictionListener(listener);
@@ -1572,6 +1685,13 @@ public final class BackgroundRestrictionTest {
R.bool.config_bg_current_drain_high_threshold_by_bg_location));
bgCurrentDrainHighThresholdByBgLocation.set(true);
bgCurrentDrainDecoupleThresholds = new DeviceConfigSession<>(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLDS,
DeviceConfig::getBoolean,
AppBatteryPolicy.DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD);
bgCurrentDrainDecoupleThresholds.set(true);
mCurrentTimeMillis = 10_000L;
doReturn(mCurrentTimeMillis - windowMs).when(stats).getStatsStartTimestamp();
doReturn(mCurrentTimeMillis).when(stats).getStatsEndTimestamp();
@@ -1990,6 +2110,7 @@ public final class BackgroundRestrictionTest {
closeIfNotNull(bgPermissionMonitorEnabled);
closeIfNotNull(bgPermissionsInMonitor);
closeIfNotNull(bgCurrentDrainHighThresholdByBgLocation);
closeIfNotNull(bgCurrentDrainDecoupleThresholds);
}
}