| /* |
| * 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<BroadcastReceiver> 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)); |
| } |
| } |