From 16a6a79558c458d964d49bdfec3f92155fd56ed2 Mon Sep 17 00:00:00 2001 From: Kweku Adams <kwekua@google.com> Date: Fri, 29 Sep 2023 15:31:17 +0000 Subject: [PATCH] Split JobScheduler idle value by charging state. Create a separate JobScheduler idle value by charging + battery-not-low state so that we can have different values when the device is charging compared to when it's on battery. Bug: 236261941 Bug: 297106511 Bug: 299329948 Test: atest CtsJobSchedulerTestCases:IdleConstraintTest Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Change-Id: I16f41f05f7ffe5ab7fd1f660e0e08ea7544ff921 --- .../job/controllers/IdleController.java | 27 ++- .../controllers/idle/CarIdlenessTracker.java | 19 +- .../idle/DeviceIdlenessTracker.java | 106 ++++++++- .../job/controllers/idle/IdlenessTracker.java | 16 +- core/res/res/values/config.xml | 4 + core/res/res/values/symbols.xml | 1 + .../idle/DeviceIdlenessTrackerTest.java | 210 ++++++++++++++++++ 7 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java index a25af7110ee55..47d3fd5bc8c43 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -18,13 +18,16 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import com.android.server.job.controllers.idle.CarIdlenessTracker; @@ -89,6 +92,19 @@ public final class IdleController extends RestrictingController implements Idlen } } + @Override + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + mIdleTracker.processConstant(properties, key); + } + + @Override + @GuardedBy("mLock") + public void onBatteryStateChangedLocked() { + mIdleTracker.onBatteryStateChanged( + mService.isBatteryCharging(), mService.isBatteryNotLow()); + } + /** * State-change notifications from the idleness tracker */ @@ -119,7 +135,16 @@ public final class IdleController extends RestrictingController implements Idlen } else { mIdleTracker = new DeviceIdlenessTracker(); } - mIdleTracker.startTracking(ctx, this); + mIdleTracker.startTracking(ctx, mService, this); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.println(); + pw.println("IdleController:"); + pw.increaseIndent(); + mIdleTracker.dumpConstants(pw); + pw.decreaseIndent(); } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java index c458caec28735..ba0e633975053 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java @@ -16,10 +16,13 @@ package com.android.server.job.controllers.idle; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -73,7 +76,8 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen } @Override - public void startTracking(Context context, IdlenessListener listener) { + public void startTracking(Context context, JobSchedulerService service, + IdlenessListener listener) { mIdleListener = listener; IntentFilter filter = new IntentFilter(); @@ -94,6 +98,15 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen context.registerReceiver(this, filter, null, AppSchedulingModuleThread.getHandler()); } + /** Process the specified constant and update internal constants if relevant. */ + public void processConstant(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + } + + @Override + public void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow) { + } + @Override public void dump(PrintWriter pw) { pw.print(" mIdle: "); pw.println(mIdle); @@ -118,6 +131,10 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen proto.end(token); } + @Override + public void dumpConstants(IndentingPrintWriter pw) { + } + @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java index c943e73eb12cf..7dd3d13433797 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java @@ -17,9 +17,12 @@ package com.android.server.job.controllers.idle; import static android.app.UiModeManager.PROJECTION_TYPE_NONE; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.app.AlarmManager; import android.app.UiModeManager; import android.content.BroadcastReceiver; @@ -27,10 +30,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.PowerManager; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; import com.android.server.am.ActivityManagerService; import com.android.server.job.JobSchedulerService; @@ -45,17 +51,38 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ + private static final String IC_DIT_CONSTANT_PREFIX = "ic_dit_"; + @VisibleForTesting + static final String KEY_INACTIVITY_IDLE_THRESHOLD_MS = + IC_DIT_CONSTANT_PREFIX + "inactivity_idle_threshold_ms"; + @VisibleForTesting + static final String KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS = + IC_DIT_CONSTANT_PREFIX + "inactivity_idle_stable_power_threshold_ms"; + private static final String KEY_IDLE_WINDOW_SLOP_MS = + IC_DIT_CONSTANT_PREFIX + "idle_window_slop_ms"; + private AlarmManager mAlarm; private PowerManager mPowerManager; // After construction, mutations of idle/screen-on/projection states will only happen // on the JobScheduler thread, either in onReceive(), in an alarm callback, or in on.*Changed. private long mInactivityIdleThreshold; + private long mInactivityStablePowerIdleThreshold; private long mIdleWindowSlop; + /** Stable power is defined as "charging + battery not low." */ + private boolean mIsStablePower; private boolean mIdle; private boolean mScreenOn; private boolean mDockIdle; private boolean mProjectionActive; + + /** + * Time (in the elapsed realtime timebase) when the idleness check was scheduled. This should + * be a negative value if the device is not in state to be considered idle. + */ + private long mIdlenessCheckScheduledElapsed = -1; + private IdlenessListener mIdleListener; private final UiModeManager.OnProjectionStateChangedListener mOnProjectionStateChangedListener = this::onProjectionStateChanged; @@ -76,10 +103,14 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id } @Override - public void startTracking(Context context, IdlenessListener listener) { + public void startTracking(Context context, JobSchedulerService service, + IdlenessListener listener) { mIdleListener = listener; mInactivityIdleThreshold = context.getResources().getInteger( com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + mInactivityStablePowerIdleThreshold = context.getResources().getInteger( + com.android.internal.R.integer + .config_jobSchedulerInactivityIdleThresholdOnStablePower); mIdleWindowSlop = context.getResources().getInteger( com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop); mAlarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); @@ -107,6 +138,46 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id context.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener( UiModeManager.PROJECTION_TYPE_ALL, AppSchedulingModuleThread.getExecutor(), mOnProjectionStateChangedListener); + + mIsStablePower = service.isBatteryCharging() && service.isBatteryNotLow(); + } + + /** Process the specified constant and update internal constants if relevant. */ + public void processConstant(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + switch (key) { + case KEY_INACTIVITY_IDLE_THRESHOLD_MS: + // Keep the threshold in the range [1 minute, 4 hours]. + mInactivityIdleThreshold = Math.max(MINUTE_IN_MILLIS, Math.min(4 * HOUR_IN_MILLIS, + properties.getLong(key, mInactivityIdleThreshold))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + case KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS: + // Keep the threshold in the range [1 minute, 4 hours]. + mInactivityStablePowerIdleThreshold = Math.max(MINUTE_IN_MILLIS, + Math.min(4 * HOUR_IN_MILLIS, + properties.getLong(key, mInactivityStablePowerIdleThreshold))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + case KEY_IDLE_WINDOW_SLOP_MS: + // Keep the slop in the range [1 minute, 15 minutes]. + mIdleWindowSlop = Math.max(MINUTE_IN_MILLIS, Math.min(15 * MINUTE_IN_MILLIS, + properties.getLong(key, mIdleWindowSlop))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + } + } + + @Override + public void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow) { + final boolean isStablePower = isCharging && isBatteryNotLow; + if (mIsStablePower != isStablePower) { + mIsStablePower = isStablePower; + maybeScheduleIdlenessCheck("stable power changed"); + } } private void onProjectionStateChanged(@UiModeManager.ProjectionType int activeProjectionTypes, @@ -134,8 +205,10 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id public void dump(PrintWriter pw) { pw.print(" mIdle: "); pw.println(mIdle); pw.print(" mScreenOn: "); pw.println(mScreenOn); + pw.print(" mIsStablePower: "); pw.println(mIsStablePower); pw.print(" mDockIdle: "); pw.println(mDockIdle); pw.print(" mProjectionActive: "); pw.println(mProjectionActive); + pw.print(" mIdlenessCheckScheduledElapsed: "); pw.println(mIdlenessCheckScheduledElapsed); } @Override @@ -161,6 +234,17 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id proto.end(token); } + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.println("DeviceIdlenessTracker:"); + pw.increaseIndent(); + pw.print(KEY_INACTIVITY_IDLE_THRESHOLD_MS, mInactivityIdleThreshold).println(); + pw.print(KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS, mInactivityStablePowerIdleThreshold) + .println(); + pw.print(KEY_IDLE_WINDOW_SLOP_MS, mIdleWindowSlop).println(); + pw.decreaseIndent(); + } + @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -220,9 +304,24 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private void maybeScheduleIdlenessCheck(String reason) { if ((!mScreenOn || mDockIdle) && !mProjectionActive) { final long nowElapsed = sElapsedRealtimeClock.millis(); - final long when = nowElapsed + mInactivityIdleThreshold; + final long inactivityThresholdMs = mIsStablePower + ? mInactivityStablePowerIdleThreshold : mInactivityIdleThreshold; + if (mIdlenessCheckScheduledElapsed >= 0) { + if (mIdlenessCheckScheduledElapsed + inactivityThresholdMs <= nowElapsed) { + if (DEBUG) { + Slog.v(TAG, "Previous idle check @ " + mIdlenessCheckScheduledElapsed + + " allows device to be idle now"); + } + handleIdleTrigger(); + return; + } + } else { + mIdlenessCheckScheduledElapsed = nowElapsed; + } + final long when = mIdlenessCheckScheduledElapsed + inactivityThresholdMs; if (DEBUG) { - Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + " when=" + when); + Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + + " checkElapsed=" + mIdlenessCheckScheduledElapsed + " when=" + when); } mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mIdleWindowSlop, "JS idleness", @@ -232,6 +331,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private void cancelIdlenessCheck() { mAlarm.cancel(mIdleAlarmListener); + mIdlenessCheckScheduledElapsed = -1; } private void handleIdleTrigger() { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java index cdab7e538ca5f..92ad4dfddfc25 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java @@ -16,9 +16,14 @@ package com.android.server.job.controllers.idle; +import android.annotation.NonNull; import android.content.Context; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; +import com.android.server.job.JobSchedulerService; + import java.io.PrintWriter; public interface IdlenessTracker { @@ -29,7 +34,7 @@ public interface IdlenessTracker { * non-interacting state. When the idle state changes thereafter, the given * listener must be called to report the new state. */ - void startTracking(Context context, IdlenessListener listener); + void startTracking(Context context, JobSchedulerService service, IdlenessListener listener); /** * Report whether the device is currently considered "idle" for purposes of @@ -40,6 +45,12 @@ public interface IdlenessTracker { */ boolean isIdle(); + /** Process the specified constant and update internal constants if relevant. */ + void processConstant(@NonNull DeviceConfig.Properties properties, @NonNull String key); + + /** Called when the battery state changes. */ + void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow); + /** * Dump useful information about tracked idleness-related state in plaintext. */ @@ -49,4 +60,7 @@ public interface IdlenessTracker { * Dump useful information about tracked idleness-related state to proto. */ void dump(ProtoOutputStream proto, long fieldId); + + /** Dump any internal constants the tracker may have. */ + void dumpConstants(IndentingPrintWriter pw); } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index ab71b41bccd40..c2d69b2068570 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4174,6 +4174,10 @@ <!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider the device to be "idle" after being inactive for this long. --> <integer name="config_jobSchedulerInactivityIdleThreshold">1860000</integer> + <!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider + the device to be "idle" after being inactive for this long if the device is on stable + power. Stable power is defined as "charging + battery not low". --> + <integer name="config_jobSchedulerInactivityIdleThresholdOnStablePower">1860000</integer> <!-- The alarm window (in milliseconds) that JobScheduler uses to enter the idle state --> <integer name="config_jobSchedulerIdleWindowSlop">300000</integer> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 14bbb966f7502..1bb0d5558aa62 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2878,6 +2878,7 @@ <java-symbol type="integer" name="config_defaultNightMode" /> <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThreshold" /> + <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThresholdOnStablePower" /> <java-symbol type="integer" name="config_jobSchedulerIdleWindowSlop" /> <java-symbol type="bool" name="config_jobSchedulerRestrictBackgroundUser" /> <java-symbol type="integer" name="config_jobSchedulerUserGracePeriod" /> diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java new file mode 100644 index 0000000000000..09935f24cf93b --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 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.job.controllers.idle; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.controllers.idle.DeviceIdlenessTracker.KEY_INACTIVITY_IDLE_THRESHOLD_MS; +import static com.android.server.job.controllers.idle.DeviceIdlenessTracker.KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.AlarmManager; +import android.app.UiModeManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.PowerManager; +import android.os.SystemClock; +import android.provider.DeviceConfig; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.AppSchedulingModuleThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZoneOffset; + +@RunWith(AndroidJUnit4.class) +public class DeviceIdlenessTrackerTest { + private DeviceIdlenessTracker mDeviceIdlenessTracker; + private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); + private BroadcastReceiver mBroadcastReceiver; + private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder = + new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);; + + private MockitoSession mMockingSession; + @Mock + private AlarmManager mAlarmManager; + @Mock + private Context mContext; + @Mock + private JobSchedulerService mJobSchedulerService; + @Mock + private PowerManager mPowerManager; + @Mock + private Resources mResources; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(DeviceConfig.class) + .mockStatic(LocalServices.class) + .startMocking(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + when(mJobSchedulerService.getConstants()).thenReturn(mConstants); + // Called in DeviceIdlenessTracker.startTracking. + when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager); + when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); + when(mContext.getResources()).thenReturn(mResources); + doReturn((int) (31 * MINUTE_IN_MILLIS)).when(mResources).getInteger( + com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + doReturn((int) (17 * MINUTE_IN_MILLIS)).when(mResources).getInteger( + com.android.internal.R.integer + .config_jobSchedulerInactivityIdleThresholdOnStablePower); + doReturn(mPowerManager).when(() -> LocalServices.getService(PowerManager.class)); + + // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions + // in the past, and QuotaController sometimes floors values at 0, so if the test time + // causes sessions with negative timestamps, they will fail. + JobSchedulerService.sSystemClock = + getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sUptimeMillisClock = getAdvancedClock( + Clock.fixed(SystemClock.uptimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + mDeviceIdlenessTracker = new DeviceIdlenessTracker(); + mDeviceIdlenessTracker.startTracking(mContext, + mJobSchedulerService, mock(IdlenessListener.class)); + + verify(mContext).registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + mBroadcastReceiver = broadcastReceiverCaptor.getValue(); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private Clock getAdvancedClock(Clock clock, long incrementMs) { + return Clock.offset(clock, Duration.ofMillis(incrementMs)); + } + + private void advanceElapsedClock(long incrementMs) { + JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( + JobSchedulerService.sElapsedRealtimeClock, incrementMs); + } + + private void setBatteryState(boolean isCharging, boolean isBatteryNotLow) { + doReturn(isCharging).when(mJobSchedulerService).isBatteryCharging(); + doReturn(isBatteryNotLow).when(mJobSchedulerService).isBatteryNotLow(); + mDeviceIdlenessTracker.onBatteryStateChanged(isCharging, isBatteryNotLow); + } + + private void setDeviceConfigLong(String key, long val) { + mDeviceConfigPropertiesBuilder.setLong(key, val); + mDeviceIdlenessTracker.processConstant(mDeviceConfigPropertiesBuilder.build(), key); + } + + @Test + public void testThresholdChangeWithStablePowerChange() { + setDeviceConfigLong(KEY_INACTIVITY_IDLE_THRESHOLD_MS, 10 * MINUTE_IN_MILLIS); + setDeviceConfigLong(KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS, 5 * MINUTE_IN_MILLIS); + setBatteryState(false, false); + + Intent screenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF); + mBroadcastReceiver.onReceive(mContext, screenOffIntent); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + long expectedUnstableAlarmElapsed = nowElapsed + 10 * MINUTE_IN_MILLIS; + long expectedStableAlarmElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS; + + InOrder inOrder = inOrder(mAlarmManager); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedUnstableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Advanced the clock a little to make sure the tracker continues to use the original time. + advanceElapsedClock(MINUTE_IN_MILLIS); + + // Charging isn't enough for stable power. + setBatteryState(true, false); + inOrder.verify(mAlarmManager, never()) + .setWindow(anyInt(), anyLong(), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Now on stable power. + setBatteryState(true, true); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedStableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Battery-not-low isn't enough for stable power. Go back to unstable timing. + setBatteryState(false, true); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedUnstableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Still not on stable power. + setBatteryState(false, false); + inOrder.verify(mAlarmManager, never()) + .setWindow(anyInt(), anyLong(), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + } +} -- GitLab