From 9bbddb730210b524d015b8a1054b6b9a89b8eeb3 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Thu, 13 Jan 2022 16:51:34 +0000 Subject: [PATCH] Satisfy charging constraint earlier for top apps. Several releases ago, we delayed the CHARGING broadcast to ~15 minutes after the device was plugged in. This means that any job that specifies a charging constraint won't have the constraint satisfied until 15 minutes after the device is plugged in. If such a job's setting is explicitly set by the user and the user plugs in their device while the app is on top, they will see that the work doesn't start immediately. This would be confusing for the user. We now the top app's charging constraint jobs as satisfied when the device is plugged in to avoid this confusion. If any TOP app's charging constraint jobs start while the app is on top, the job will be allowed to continue running after the user leaves the app as long as the device is plugged in. Any jobs that didn't start will be treated regularly and will have to wait for the CHARGING broadcast. Bug: 198624541 Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest CtsJobSchedulerTestCases Change-Id: I914176d7bf9fd4a8998c05d411483bd4607589f2 --- .../server/job/JobSchedulerService.java | 2 +- .../job/controllers/BatteryController.java | 162 ++++++++- .../controllers/ConnectivityController.java | 2 +- .../job/controllers/PrefetchController.java | 17 +- .../job/controllers/StateController.java | 2 +- .../controllers/BatteryControllerTest.java | 315 ++++++++++++++++++ .../controllers/PrefetchControllerTest.java | 4 +- 7 files changed, 479 insertions(+), 25 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 3fb1fadba062c..12a8654c61f70 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1556,7 +1556,7 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "UID " + uid + " bias changed from " + prevBias + " to " + newBias); } for (int c = 0; c < mControllers.size(); ++c) { - mControllers.get(c).onUidBiasChangedLocked(uid, newBias); + mControllers.get(c).onUidBiasChangedLocked(uid, prevBias, newBias); } } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java index f7338494384d1..63781ae334d23 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -18,6 +18,14 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; @@ -27,6 +35,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.server.JobSchedulerBackgroundThread; +import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; @@ -42,10 +51,26 @@ public final class BatteryController extends RestrictingController { private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + @GuardedBy("mLock") private final ArraySet mTrackedTasks = new ArraySet<>(); + /** + * List of jobs that started while the UID was in the TOP state. + */ + @GuardedBy("mLock") + private final ArraySet mTopStartedJobs = new ArraySet<>(); + + private final PowerTracker mPowerTracker; + + /** + * Helper set to avoid too much GC churn from frequent calls to + * {@link #maybeReportNewChargingStateLocked()}. + */ + private final ArraySet mChangedJobs = new ArraySet<>(); public BatteryController(JobSchedulerService service) { super(service); + mPowerTracker = new PowerTracker(); + mPowerTracker.startTracking(); } @Override @@ -54,8 +79,15 @@ public final class BatteryController extends RestrictingController { final long nowElapsed = sElapsedRealtimeClock.millis(); mTrackedTasks.add(taskStatus); taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); - taskStatus.setChargingConstraintSatisfied(nowElapsed, - mService.isBatteryCharging() && mService.isBatteryNotLow()); + if (taskStatus.hasChargingConstraint()) { + if (hasTopExemptionLocked(taskStatus)) { + taskStatus.setChargingConstraintSatisfied(nowElapsed, + mPowerTracker.isPowerConnected()); + } else { + taskStatus.setChargingConstraintSatisfied(nowElapsed, + mService.isBatteryCharging() && mService.isBatteryNotLow()); + } + } taskStatus.setBatteryNotLowConstraintSatisfied(nowElapsed, mService.isBatteryNotLow()); } } @@ -65,10 +97,33 @@ public final class BatteryController extends RestrictingController { maybeStartTrackingJobLocked(jobStatus, null); } + @Override + @GuardedBy("mLock") + public void prepareForExecutionLocked(JobStatus jobStatus) { + if (DEBUG) { + Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); + } + + final int uid = jobStatus.getSourceUid(); + if (mService.getUidBias(uid) == JobInfo.BIAS_TOP_APP) { + if (DEBUG) { + Slog.d(TAG, jobStatus.toShortString() + " is top started job"); + } + mTopStartedJobs.add(jobStatus); + } + } + + @Override + @GuardedBy("mLock") + public void unprepareFromExecutionLocked(JobStatus jobStatus) { + mTopStartedJobs.remove(jobStatus); + } + @Override public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { mTrackedTasks.remove(taskStatus); + mTopStartedJobs.remove(taskStatus); } } @@ -90,33 +145,124 @@ public final class BatteryController extends RestrictingController { }); } + @Override + @GuardedBy("mLock") + public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { + if (prevBias == JobInfo.BIAS_TOP_APP || newBias == JobInfo.BIAS_TOP_APP) { + maybeReportNewChargingStateLocked(); + } + } + + @GuardedBy("mLock") + private boolean hasTopExemptionLocked(@NonNull JobStatus taskStatus) { + return mService.getUidBias(taskStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP + || mTopStartedJobs.contains(taskStatus); + } + @GuardedBy("mLock") private void maybeReportNewChargingStateLocked() { + final boolean powerConnected = mPowerTracker.isPowerConnected(); final boolean stablePower = mService.isBatteryCharging() && mService.isBatteryNotLow(); final boolean batteryNotLow = mService.isBatteryNotLow(); if (DEBUG) { - Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower); + Slog.d(TAG, "maybeReportNewChargingStateLocked: " + + powerConnected + "/" + stablePower + "/" + batteryNotLow); } final long nowElapsed = sElapsedRealtimeClock.millis(); - boolean reportChange = false; for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { final JobStatus ts = mTrackedTasks.valueAt(i); - reportChange |= ts.setChargingConstraintSatisfied(nowElapsed, stablePower); - reportChange |= ts.setBatteryNotLowConstraintSatisfied(nowElapsed, batteryNotLow); + if (ts.hasChargingConstraint()) { + if (hasTopExemptionLocked(ts) + && ts.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT) { + // If the job started while the app was on top or the app is currently on top, + // let the job run as long as there's power connected, even if the device isn't + // officially charging. + // For user requested/initiated jobs, users may be confused when the task stops + // running even though the device is plugged in. + // Low priority jobs don't need to be exempted. + if (ts.setChargingConstraintSatisfied(nowElapsed, powerConnected)) { + mChangedJobs.add(ts); + } + } else if (ts.setChargingConstraintSatisfied(nowElapsed, stablePower)) { + mChangedJobs.add(ts); + } + } + if (ts.hasBatteryNotLowConstraint() + && ts.setBatteryNotLowConstraintSatisfied(nowElapsed, batteryNotLow)) { + mChangedJobs.add(ts); + } } if (stablePower || batteryNotLow) { // If one of our conditions has been satisfied, always schedule any newly ready jobs. mStateChangedListener.onRunJobNow(null); - } else if (reportChange) { + } else if (mChangedJobs.size() > 0) { // Otherwise, just let the job scheduler know the state has changed and take care of it // as it thinks is best. - mStateChangedListener.onControllerStateChanged(mTrackedTasks); + mStateChangedListener.onControllerStateChanged(mChangedJobs); + } + mChangedJobs.clear(); + } + + private final class PowerTracker extends BroadcastReceiver { + /** + * Track whether there is power connected. It doesn't mean the device is charging. + * Use {@link JobSchedulerService#isBatteryCharging()} to determine if the device is + * charging. + */ + private boolean mPowerConnected; + + PowerTracker() { + } + + void startTracking() { + IntentFilter filter = new IntentFilter(); + + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + mContext.registerReceiver(this, filter); + + // Initialize tracker state. + BatteryManagerInternal batteryManagerInternal = + LocalServices.getService(BatteryManagerInternal.class); + mPowerConnected = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } + + boolean isPowerConnected() { + return mPowerConnected; + } + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + final String action = intent.getAction(); + + if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis()); + } + if (mPowerConnected) { + return; + } + mPowerConnected = true; + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis()); + } + if (!mPowerConnected) { + return; + } + mPowerConnected = false; + } + + maybeReportNewChargingStateLocked(); + } } } @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate predicate) { + pw.println("Power connected: " + mPowerTracker.isPowerConnected()); pw.println("Stable power: " + (mService.isBatteryCharging() && mService.isBatteryNotLow())); pw.println("Not low: " + mService.isBatteryNotLow()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index 9fb7ab5946079..c678755f73dac 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -517,7 +517,7 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") @Override - public void onUidBiasChangedLocked(int uid, int newBias) { + public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { UidStats uidStats = mUidStats.get(uid); if (uidStats != null && uidStats.baseBias != newBias) { uidStats.baseBias = newBias; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java index 9749c8087caf8..428f2cbaefc29 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java @@ -40,7 +40,6 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArrayMap; -import android.util.SparseBooleanArray; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; @@ -81,9 +80,6 @@ public class PrefetchController extends StateController { */ @GuardedBy("mLock") private final SparseArrayMap mEstimatedLaunchTimes = new SparseArrayMap<>(); - /** Cached list of UIDs in the TOP state. */ - @GuardedBy("mLock") - private final SparseBooleanArray mTopUids = new SparseBooleanArray(); private final ThresholdAlarmListener mThresholdAlarmListener; /** @@ -186,15 +182,9 @@ public class PrefetchController extends StateController { @GuardedBy("mLock") @Override - public void onUidBiasChangedLocked(int uid, int newBias) { + public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP; - final boolean wasTop = mTopUids.get(uid); - if (isNowTop) { - mTopUids.put(uid, true); - } else { - // Delete entries of non-top apps so the set doesn't get too large. - mTopUids.delete(uid); - } + final boolean wasTop = prevBias == JobInfo.BIAS_TOP_APP; if (isNowTop != wasTop) { mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget(); } @@ -314,7 +304,8 @@ public class PrefetchController extends StateController { // 3. The app is not open but has an active widget (we can't tell if a widget displays // status/data, so this assumes the prefetch job is to update the data displayed on // the widget). - final boolean appIsOpen = mTopUids.get(jobStatus.getSourceUid()); + final boolean appIsOpen = + mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP; final boolean satisfied; if (!appIsOpen) { final int userId = jobStatus.getSourceUserId(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java index 509fb6963a3c7..2a2d602b24bf2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java @@ -144,7 +144,7 @@ public abstract class StateController { * important the UID is. */ @GuardedBy("mLock") - public void onUidBiasChangedLocked(int uid, int newBias) { + public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { } protected boolean wouldBeReadyWithConstraintLocked(JobStatus jobStatus, int constraint) { diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java new file mode 100644 index 0000000000000..52d0494bd9f02 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2018 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; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +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.FREQUENT_INDEX; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; + +import android.app.AppGlobals; +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ServiceInfo; +import android.os.BatteryManagerInternal; +import android.os.RemoteException; +import android.util.ArraySet; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.JobSchedulerBackgroundThread; +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.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class BatteryControllerTest { + private static final int CALLING_UID = 1000; + private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; + private static final int SOURCE_USER_ID = 0; + + private BatteryController mBatteryController; + private BroadcastReceiver mPowerReceiver; + private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); + private int mSourceUid; + + private MockitoSession mMockingSession; + @Mock + private Context mContext; + @Mock + private BatteryManagerInternal mBatteryManagerInternal; + @Mock + private JobSchedulerService mJobSchedulerService; + @Mock + private PackageManagerInternal mPackageManagerInternal; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .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 BatteryController constructor. + doReturn(mBatteryManagerInternal) + .when(() -> LocalServices.getService(BatteryManagerInternal.class)); + // Used in JobStatus. + doReturn(mPackageManagerInternal) + .when(() -> LocalServices.getService(PackageManagerInternal.class)); + + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + mBatteryController = new BatteryController(mJobSchedulerService); + + verify(mContext).registerReceiver(receiverCaptor.capture(), + ArgumentMatchers.argThat(filter -> + filter.hasAction(Intent.ACTION_POWER_CONNECTED) + && filter.hasAction(Intent.ACTION_POWER_DISCONNECTED))); + mPowerReceiver = receiverCaptor.getValue(); + try { + mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0); + // Need to do this since we're using a mock JS and not a real object. + doReturn(new ArraySet<>(new String[]{SOURCE_PACKAGE})) + .when(mJobSchedulerService).getPackagesForUidLocked(mSourceUid); + } catch (RemoteException e) { + fail(e.getMessage()); + } + setPowerConnected(false); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private void setBatteryNotLow(boolean notLow) { + doReturn(notLow).when(mJobSchedulerService).isBatteryNotLow(); + synchronized (mBatteryController.mLock) { + mBatteryController.onBatteryStateChangedLocked(); + } + waitForNonDelayedMessagesProcessed(); + } + + private void setCharging() { + doReturn(true).when(mJobSchedulerService).isBatteryCharging(); + synchronized (mBatteryController.mLock) { + mBatteryController.onBatteryStateChangedLocked(); + } + waitForNonDelayedMessagesProcessed(); + } + + private void setDischarging() { + doReturn(false).when(mJobSchedulerService).isBatteryCharging(); + synchronized (mBatteryController.mLock) { + mBatteryController.onBatteryStateChangedLocked(); + } + waitForNonDelayedMessagesProcessed(); + } + + private void setPowerConnected(boolean connected) { + Intent intent = new Intent( + connected ? Intent.ACTION_POWER_CONNECTED : Intent.ACTION_POWER_DISCONNECTED); + mPowerReceiver.onReceive(mContext, intent); + } + + private void setUidBias(int uid, int bias) { + int prevBias = mJobSchedulerService.getUidBias(uid); + doReturn(bias).when(mJobSchedulerService).getUidBias(uid); + synchronized (mBatteryController.mLock) { + mBatteryController.onUidBiasChangedLocked(uid, prevBias, bias); + } + } + + private void trackJobs(JobStatus... jobs) { + for (JobStatus job : jobs) { + synchronized (mBatteryController.mLock) { + mBatteryController.maybeStartTrackingJobLocked(job, null); + } + } + } + + private void waitForNonDelayedMessagesProcessed() { + JobSchedulerBackgroundThread.getHandler().runWithScissors(() -> {}, 15_000); + } + + private JobInfo.Builder createBaseJobInfoBuilder(int jobId) { + return new JobInfo.Builder(jobId, new ComponentName(mContext, "TestBatteryJobService")); + } + + private JobInfo.Builder createBaseJobInfoBuilder(int jobId, String pkgName) { + return new JobInfo.Builder(jobId, new ComponentName(pkgName, "TestBatteryJobService")); + } + + private JobStatus createJobStatus(String testTag, String packageName, int callingUid, + JobInfo jobInfo) { + JobStatus js = JobStatus.createFromJobInfo( + jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag); + js.serviceInfo = mock(ServiceInfo.class); + // Make sure tests aren't passing just because the default bucket is likely ACTIVE. + js.setStandbyBucket(FREQUENT_INDEX); + return js; + } + + @Test + public void testBatteryNotLow() { + JobStatus job1 = createJobStatus("testBatteryNotLow", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(1).setRequiresBatteryNotLow(true).build()); + JobStatus job2 = createJobStatus("testBatteryNotLow", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(2).setRequiresBatteryNotLow(true).build()); + + setBatteryNotLow(false); + trackJobs(job1); + assertFalse(job1.isConstraintSatisfied(JobStatus.CONSTRAINT_BATTERY_NOT_LOW)); + + setBatteryNotLow(true); + assertTrue(job1.isConstraintSatisfied(JobStatus.CONSTRAINT_BATTERY_NOT_LOW)); + + trackJobs(job2); + assertTrue(job2.isConstraintSatisfied(JobStatus.CONSTRAINT_BATTERY_NOT_LOW)); + } + + @Test + public void testCharging_BatteryNotLow() { + JobStatus job1 = createJobStatus("testCharging_BatteryNotLow", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(1) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(true).build()); + JobStatus job2 = createJobStatus("testCharging_BatteryNotLow", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(2) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(false).build()); + + setBatteryNotLow(true); + setDischarging(); + trackJobs(job1, job2); + assertFalse(job1.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(job2.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + + setCharging(); + assertTrue(job1.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertTrue(job2.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + } + + @Test + public void testTopPowerConnectedExemption() { + final int uid1 = mSourceUid; + final int uid2 = mSourceUid + 1; + final int uid3 = mSourceUid + 2; + JobStatus jobFg = createJobStatus("testTopPowerConnectedExemption", SOURCE_PACKAGE, uid1, + createBaseJobInfoBuilder(1).setRequiresCharging(true).build()); + JobStatus jobFgRunner = createJobStatus("testTopPowerConnectedExemption", + SOURCE_PACKAGE, uid1, + createBaseJobInfoBuilder(2).setRequiresCharging(true).build()); + JobStatus jobFgLow = createJobStatus("testTopPowerConnectedExemption", SOURCE_PACKAGE, uid1, + createBaseJobInfoBuilder(3) + .setRequiresCharging(true) + .setPriority(JobInfo.PRIORITY_LOW) + .build()); + JobStatus jobBg = createJobStatus("testTopPowerConnectedExemption", + "some.background.app", uid2, + createBaseJobInfoBuilder(4, "some.background.app") + .setRequiresCharging(true) + .build()); + JobStatus jobLateFg = createJobStatus("testTopPowerConnectedExemption", + "switch.to.fg", uid3, + createBaseJobInfoBuilder(5, "switch.to.fg").setRequiresCharging(true).build()); + JobStatus jobLateFgLow = createJobStatus("testTopPowerConnectedExemption", + "switch.to.fg", uid3, + createBaseJobInfoBuilder(6, "switch.to.fg") + .setRequiresCharging(true) + .setPriority(JobInfo.PRIORITY_MIN) + .build()); + + setBatteryNotLow(false); + setDischarging(); + setUidBias(uid1, JobInfo.BIAS_TOP_APP); + setUidBias(uid2, JobInfo.BIAS_DEFAULT); + setUidBias(uid3, JobInfo.BIAS_DEFAULT); + + // Jobs are scheduled when power isn't connected. + setPowerConnected(false); + trackJobs(jobFg, jobFgLow, jobBg, jobLateFg, jobLateFgLow); + assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + + // Power is connected. TOP app should be allowed to start job DEFAULT+ jobs. + setPowerConnected(true); + assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + + // Test that newly scheduled job of TOP app is correctly allowed to run. + trackJobs(jobFgRunner); + assertTrue(jobFgRunner.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + + // Switch top app. New TOP app should be allowed to run job and the running job of + // previously TOP app should be allowed to continue to run. + synchronized (mBatteryController.mLock) { + mBatteryController.prepareForExecutionLocked(jobFgRunner); + } + setUidBias(uid1, JobInfo.BIAS_DEFAULT); + setUidBias(uid2, JobInfo.BIAS_DEFAULT); + setUidBias(uid3, JobInfo.BIAS_TOP_APP); + assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertTrue(jobFgRunner.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertTrue(jobLateFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + + setPowerConnected(false); + assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobFgRunner.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFg.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + assertFalse(jobLateFgLow.isConstraintSatisfied(JobStatus.CONSTRAINT_CHARGING)); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java index 95912b26e830f..d7414591f83c7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -200,8 +200,10 @@ public class PrefetchControllerTest { } private void setUidBias(int uid, int bias) { + int prevBias = mJobSchedulerService.getUidBias(uid); + doReturn(bias).when(mJobSchedulerService).getUidBias(uid); synchronized (mPrefetchController.mLock) { - mPrefetchController.onUidBiasChangedLocked(uid, bias); + mPrefetchController.onUidBiasChangedLocked(uid, prevBias, bias); } }