Merge "Add unit test for BackgroundDexOptService"

This commit is contained in:
TreeHugger Robot
2021-11-16 00:52:39 +00:00
committed by Android (Google) Code Review
3 changed files with 526 additions and 44 deletions

View File

@@ -121,6 +121,8 @@ public final class BackgroundDexOptService {
private final Injector mInjector;
private final DexOptHelper mDexOptHelper;
private final Object mLock = new Object();
// Thread currently running dexopt. This will be null if dexopt is not running.
@@ -168,13 +170,15 @@ public final class BackgroundDexOptService {
void onPackagesUpdated(ArraySet<String> updatedPackages);
}
public BackgroundDexOptService(Context context, DexManager dexManager) {
this(new Injector(context, dexManager));
public BackgroundDexOptService(Context context, DexManager dexManager,
PackageManagerService pm) {
this(new Injector(context, dexManager, pm));
}
@VisibleForTesting
public BackgroundDexOptService(Injector injector) {
mInjector = injector;
mDexOptHelper = mInjector.getDexOptHelper();
LocalServices.addService(BackgroundDexOptService.class, this);
mDowngradeUnusedAppsThresholdInMillis = mInjector.getDowngradeUnusedAppsThresholdInMillis();
}
@@ -251,15 +255,13 @@ public final class BackgroundDexOptService {
resetStatesForNewDexOptRunLocked(Thread.currentThread());
}
PackageManagerService pm = mInjector.getPackageManagerService();
DexOptHelper dexOptHelper = new DexOptHelper(pm);
ArraySet<String> packagesToOptimize;
if (packageNames == null) {
packagesToOptimize = dexOptHelper.getOptimizablePackages();
packagesToOptimize = mDexOptHelper.getOptimizablePackages();
} else {
packagesToOptimize = new ArraySet<>(packageNames);
}
return runIdleOptimization(pm, dexOptHelper, packagesToOptimize,
/* isPostBootUpdate= */ false);
return runIdleOptimization(pm, packagesToOptimize, /* isPostBootUpdate= */ false);
} finally {
Binder.restoreCallingIdentity(identity);
markDexOptCompleted();
@@ -320,8 +322,7 @@ public final class BackgroundDexOptService {
return false;
}
DexOptHelper dexOptHelper = new DexOptHelper(pm);
ArraySet<String> pkgs = dexOptHelper.getOptimizablePackages();
ArraySet<String> pkgs = mDexOptHelper.getOptimizablePackages();
if (pkgs.isEmpty()) {
Slog.i(TAG, "No packages to optimize");
markPostBootUpdateCompleted(params);
@@ -347,7 +348,7 @@ public final class BackgroundDexOptService {
tr.traceBegin("jobExecution");
boolean completed = false;
try {
completed = runIdleOptimization(pm, dexOptHelper, pkgs,
completed = runIdleOptimization(pm, pkgs,
params.getJobId() == JOB_POST_BOOT_UPDATE);
} finally { // Those cleanup should be done always.
tr.traceEnd();
@@ -461,7 +462,7 @@ public final class BackgroundDexOptService {
@GuardedBy("mLock")
private void controlDexOptBlockingLocked(boolean block) {
PackageManagerService pm = mInjector.getPackageManagerService();
new DexOptHelper(pm).controlDexOptBlocking(block);
mDexOptHelper.controlDexOptBlocking(block);
}
private void scheduleAJob(int jobId) {
@@ -511,10 +512,10 @@ public final class BackgroundDexOptService {
}
/** Returns true if completed */
private boolean runIdleOptimization(PackageManagerService pm, DexOptHelper dexOptHelper,
ArraySet<String> pkgs, boolean isPostBootUpdate) {
private boolean runIdleOptimization(PackageManagerService pm, ArraySet<String> pkgs,
boolean isPostBootUpdate) {
long lowStorageThreshold = getLowStorageThreshold();
int status = idleOptimizePackages(pm, dexOptHelper, pkgs, lowStorageThreshold,
int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold,
isPostBootUpdate);
logStatus(status);
synchronized (mLock) {
@@ -562,8 +563,8 @@ public final class BackgroundDexOptService {
}
@Status
private int idleOptimizePackages(PackageManagerService pm, DexOptHelper dexOptHelper,
ArraySet<String> pkgs, long lowStorageThreshold, boolean isPostBootUpdate) {
private int idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
long lowStorageThreshold, boolean isPostBootUpdate) {
ArraySet<String> updatedPackages = new ArraySet<>();
ArraySet<String> updatedPackagesDueToSecondaryDex = new ArraySet<>();
@@ -600,7 +601,7 @@ public final class BackgroundDexOptService {
// Should be aborted by the scheduler.
return abortCode;
}
@DexOptResult int downgradeResult = downgradePackage(pm, dexOptHelper, pkg,
@DexOptResult int downgradeResult = downgradePackage(pm, pkg,
/* isForPrimaryDex= */ true, isPostBootUpdate);
if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) {
updatedPackages.add(pkg);
@@ -611,7 +612,7 @@ public final class BackgroundDexOptService {
return status;
}
if (supportSecondaryDex) {
downgradeResult = downgradePackage(pm, dexOptHelper, pkg,
downgradeResult = downgradePackage(pm, pkg,
/* isForPrimaryDex= */false, isPostBootUpdate);
status = convertPackageDexOptimizerStatusToInternal(downgradeResult);
if (status != STATUS_OK) {
@@ -625,9 +626,8 @@ public final class BackgroundDexOptService {
}
}
@Status int primaryResult = optimizePackages(dexOptHelper, pkgs,
lowStorageThreshold, /*isForPrimaryDex=*/ true, updatedPackages,
isPostBootUpdate);
@Status int primaryResult = optimizePackages(pkgs, lowStorageThreshold,
/*isForPrimaryDex=*/ true, updatedPackages, isPostBootUpdate);
if (primaryResult != STATUS_OK) {
return primaryResult;
}
@@ -636,9 +636,9 @@ public final class BackgroundDexOptService {
return STATUS_OK;
}
@Status int secondaryResult = optimizePackages(dexOptHelper, pkgs,
lowStorageThreshold, /*isForPrimaryDex*/ false,
updatedPackagesDueToSecondaryDex, isPostBootUpdate);
@Status int secondaryResult = optimizePackages(pkgs, lowStorageThreshold,
/*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex,
isPostBootUpdate);
return secondaryResult;
} finally {
// Always let the pinner service know about changes.
@@ -651,9 +651,8 @@ public final class BackgroundDexOptService {
}
@Status
private int optimizePackages(DexOptHelper dexOptHelper,
ArraySet<String> pkgs, long lowStorageThreshold, boolean isForPrimaryDex,
ArraySet<String> updatedPackages, boolean isPostBootUpdate) {
private int optimizePackages(ArraySet<String> pkgs, long lowStorageThreshold,
boolean isForPrimaryDex, ArraySet<String> updatedPackages, boolean isPostBootUpdate) {
for (String pkg : pkgs) {
int abortCode = abortIdleOptimizations(lowStorageThreshold);
if (abortCode != STATUS_OK) {
@@ -661,8 +660,7 @@ public final class BackgroundDexOptService {
return abortCode;
}
@DexOptResult int result = optimizePackage(dexOptHelper, pkg, isForPrimaryDex,
isPostBootUpdate);
@DexOptResult int result = optimizePackage(pkg, isForPrimaryDex, isPostBootUpdate);
if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
updatedPackages.add(pkg);
} else if (result != PackageDexOptimizer.DEX_OPT_SKIPPED) {
@@ -681,8 +679,8 @@ public final class BackgroundDexOptService {
* @return PackageDexOptimizer.DEX_*
*/
@DexOptResult
private int downgradePackage(PackageManagerService pm, DexOptHelper dexOptHelper, String pkg,
boolean isForPrimaryDex, boolean isPostBootUpdate) {
private int downgradePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex,
boolean isPostBootUpdate) {
if (DEBUG) {
Slog.d(TAG, "Downgrading " + pkg);
}
@@ -704,10 +702,10 @@ public final class BackgroundDexOptService {
// remove their compiler artifacts from dalvik cache.
pm.deleteOatArtifactsOfPackage(pkg);
} else {
result = performDexOptPrimary(dexOptHelper, pkg, reason, dexoptFlags);
result = performDexOptPrimary(pkg, reason, dexoptFlags);
}
} else {
result = performDexOptSecondary(dexOptHelper, pkg, reason, dexoptFlags);
result = performDexOptSecondary(pkg, reason, dexoptFlags);
}
if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
@@ -733,15 +731,13 @@ public final class BackgroundDexOptService {
*
* Optimize package if needed. Note that there can be no race between
* concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
* @param dexOptHelper An instance of DexOptHelper
* @param pkg The package to be downgraded.
* @param isForPrimaryDex Apps can have several dex file, primary and secondary.
* @param isPostBootUpdate is post boot update or not.
* @return PackageDexOptimizer#DEX_OPT_*
*/
@DexOptResult
private int optimizePackage(DexOptHelper dexOptHelper, String pkg,
boolean isForPrimaryDex, boolean isPostBootUpdate) {
private int optimizePackage(String pkg, boolean isForPrimaryDex, boolean isPostBootUpdate) {
int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT
: PackageManagerService.REASON_BACKGROUND_DEXOPT;
int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE;
@@ -753,27 +749,27 @@ public final class BackgroundDexOptService {
// System server share the same code path as primary dex files.
// PackageManagerService will select the right optimization path for it.
if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) {
return performDexOptPrimary(dexOptHelper, pkg, reason, dexoptFlags);
return performDexOptPrimary(pkg, reason, dexoptFlags);
} else {
return performDexOptSecondary(dexOptHelper, pkg, reason, dexoptFlags);
return performDexOptSecondary(pkg, reason, dexoptFlags);
}
}
@DexOptResult
private int performDexOptPrimary(DexOptHelper dexOptHelper, String pkg, int reason,
private int performDexOptPrimary(String pkg, int reason,
int dexoptFlags) {
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true,
() -> dexOptHelper.performDexOptWithStatus(
() -> mDexOptHelper.performDexOptWithStatus(
new DexoptOptions(pkg, reason, dexoptFlags)));
}
@DexOptResult
private int performDexOptSecondary(DexOptHelper dexOptHelper, String pkg, int reason,
private int performDexOptSecondary(String pkg, int reason,
int dexoptFlags) {
DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason,
dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
() -> dexOptHelper.performDexOpt(dexoptOptions)
() -> mDexOptHelper.performDexOpt(dexoptOptions)
? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
);
}
@@ -911,11 +907,13 @@ public final class BackgroundDexOptService {
static final class Injector {
private final Context mContext;
private final DexManager mDexManager;
private final PackageManagerService mPackageManagerService;
private final File mDataDir = Environment.getDataDirectory();
Injector(Context context, DexManager dexManager) {
Injector(Context context, DexManager dexManager, PackageManagerService pm) {
mContext = context;
mDexManager = dexManager;
mPackageManagerService = pm;
}
Context getContext() {
@@ -923,7 +921,11 @@ public final class BackgroundDexOptService {
}
PackageManagerService getPackageManagerService() {
return (PackageManagerService) ServiceManager.getService("package");
return mPackageManagerService;
}
DexOptHelper getDexOptHelper() {
return new DexOptHelper(getPackageManagerService());
}
JobScheduler getJobScheduler() {

View File

@@ -1486,7 +1486,7 @@ public class PackageManagerService extends IPackageManager.Stub
new DefaultSystemWrapper(),
LocalServices::getService,
context::getSystemService,
(i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager()));
(i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm));
if (Build.VERSION.SDK_INT <= 0) {
Slog.w(TAG, "**** ro.build.version.sdk not set!");

View File

@@ -0,0 +1,480 @@
/*
* Copyright (C) 2021 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.pm;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.HandlerThread;
import android.os.PowerManager;
import android.util.ArraySet;
import com.android.server.LocalServices;
import com.android.server.PinnerService;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
@RunWith(MockitoJUnitRunner.class)
public final class BackgroundDexOptServiceUnitTest {
private static final String TAG = BackgroundDexOptServiceUnitTest.class.getSimpleName();
private static final long USABLE_SPACE_NORMAL = 1_000_000_000;
private static final long STORAGE_LOW_BYTES = 1_000_000;
private static final long TEST_WAIT_TIMEOUT_MS = 10_000;
private static final ArraySet<String> DEFAULT_PACKAGE_LIST = new ArraySet<>(
Arrays.asList("aaa", "bbb"));
private static final ArraySet<String> EMPTY_PACKAGE_LIST = new ArraySet<>();
@Mock
private Context mContext;
@Mock
private PackageManagerService mPackageManager;
@Mock
private DexOptHelper mDexOptHelper;
@Mock
private DexManager mDexManager;
@Mock
private PinnerService mPinnerService;
@Mock
private JobScheduler mJobScheduler;
@Mock
private BackgroundDexOptService.Injector mInjector;
@Mock
private BackgroundDexOptJobService mJobServiceForPostBoot;
@Mock
private BackgroundDexOptJobService mJobServiceForIdle;
private final JobParameters mJobParametersForPostBoot = new JobParameters(null,
BackgroundDexOptService.JOB_POST_BOOT_UPDATE, null, null, null,
0, false, false, null, null, null);
private final JobParameters mJobParametersForIdle = new JobParameters(null,
BackgroundDexOptService.JOB_IDLE_OPTIMIZE, null, null, null,
0, false, false, null, null, null);
private BackgroundDexOptService mService;
private StartAndWaitThread mDexOptThread;
private StartAndWaitThread mCancelThread;
@Before
public void setUp() throws Exception {
when(mInjector.getContext()).thenReturn(mContext);
when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
when(mInjector.getDexManager()).thenReturn(mDexManager);
when(mInjector.getPinnerService()).thenReturn(mPinnerService);
when(mInjector.getJobScheduler()).thenReturn(mJobScheduler);
when(mInjector.getPackageManagerService()).thenReturn(mPackageManager);
// These mocking can be overwritten in some tests but still keep it here as alternative
// takes too many repetitive codes.
when(mInjector.getDataDirUsableSpace()).thenReturn(USABLE_SPACE_NORMAL);
when(mInjector.getDataDirStorageLowBytes()).thenReturn(STORAGE_LOW_BYTES);
when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE);
when(mDexOptHelper.getOptimizablePackages()).thenReturn(DEFAULT_PACKAGE_LIST);
when(mDexOptHelper.performDexOptWithStatus(any())).thenReturn(
PackageDexOptimizer.DEX_OPT_PERFORMED);
mService = new BackgroundDexOptService(mInjector);
}
@After
public void tearDown() throws Exception {
LocalServices.removeServiceForTest(BackgroundDexOptService.class);
}
@Test
public void testGetService() {
assertThat(BackgroundDexOptService.getService()).isEqualTo(mService);
}
@Test
public void testBootCompleted() {
initUntilBootCompleted();
}
@Test
public void testNoExecutionForIdleJobBeforePostBootUpdate() {
initUntilBootCompleted();
assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse();
}
@Test
public void testNoExecutionForLowStorage() {
initUntilBootCompleted();
when(mPackageManager.isStorageLow()).thenReturn(true);
assertThat(mService.onStartJob(mJobServiceForPostBoot,
mJobParametersForPostBoot)).isFalse();
verify(mDexOptHelper, never()).performDexOpt(any());
}
@Test
public void testNoExecutionForNoOptimizablePackages() {
initUntilBootCompleted();
when(mDexOptHelper.getOptimizablePackages()).thenReturn(EMPTY_PACKAGE_LIST);
assertThat(mService.onStartJob(mJobServiceForPostBoot,
mJobParametersForPostBoot)).isFalse();
verify(mDexOptHelper, never()).performDexOpt(any());
}
@Test
public void testPostBootUpdateFullRun() {
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
}
@Test
public void testIdleJobFullRun() {
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
runFullJob(mJobServiceForIdle, mJobParametersForIdle, true, 2);
}
@Test
public void testSystemReadyWhenDisabled() {
when(mInjector.isBackgroundDexOptDisabled()).thenReturn(true);
mService.systemReady();
verify(mContext, never()).registerReceiver(any(), any());
}
@Test
public void testStopByCancelFlag() {
when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
initUntilBootCompleted();
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
ArgumentCaptor<Runnable> argDexOptThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
verify(mInjector, atLeastOnce()).createAndStartThread(any(),
argDexOptThreadRunnable.capture());
// Stopping requires a separate thread
HandlerThread cancelThread = new HandlerThread("Stopping");
cancelThread.start();
when(mInjector.createAndStartThread(any(), any())).thenReturn(cancelThread);
// Cancel
assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
// Capture Runnable for cancel
ArgumentCaptor<Runnable> argCancelThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
verify(mInjector, atLeastOnce()).createAndStartThread(any(),
argCancelThreadRunnable.capture());
// Execute cancelling part
cancelThread.getThreadHandler().post(argCancelThreadRunnable.getValue());
verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(true);
// Dexopt thread run and cancelled
argDexOptThreadRunnable.getValue().run();
// Wait until cancellation Runnable is completed.
assertThat(cancelThread.getThreadHandler().runWithScissors(
argCancelThreadRunnable.getValue(), TEST_WAIT_TIMEOUT_MS)).isTrue();
// Now cancel completed
verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
verifyLastControlDexOptBlockingCall(false);
}
@Test
public void testPostUpdateCancelFirst() throws Exception {
initUntilBootCompleted();
when(mInjector.createAndStartThread(any(), any())).thenAnswer(
i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
// Start
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
// Cancel
assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
mCancelThread.runActualRunnable();
// Wait until cancel has set the flag.
verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(
true);
mDexOptThread.runActualRunnable();
// All threads should finish.
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
// Retry later if post boot job was cancelled
verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
verifyLastControlDexOptBlockingCall(false);
}
@Test
public void testPostUpdateCancelLater() throws Exception {
initUntilBootCompleted();
when(mInjector.createAndStartThread(any(), any())).thenAnswer(
i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
// Start
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
// Cancel
assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
// Dexopt thread runs and finishes
mDexOptThread.runActualRunnable();
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
mCancelThread.runActualRunnable();
mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
// Already completed before cancel, so no rescheduling.
verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, false);
verify(mDexOptHelper, never()).controlDexOptBlocking(true);
}
@Test
public void testPeriodicJobCancelFirst() throws Exception {
initUntilBootCompleted();
when(mInjector.createAndStartThread(any(), any())).thenAnswer(
i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
// Start and finish post boot job
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
mDexOptThread.runActualRunnable();
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
// Start
assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
// Cancel
assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
mCancelThread.runActualRunnable();
// Wait until cancel has set the flag.
verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(
true);
mDexOptThread.runActualRunnable();
// All threads should finish.
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
// Always reschedule for periodic job
verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true);
verifyLastControlDexOptBlockingCall(false);
}
@Test
public void testPeriodicJobCancelLater() throws Exception {
initUntilBootCompleted();
when(mInjector.createAndStartThread(any(), any())).thenAnswer(
i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
// Start and finish post boot job
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
mDexOptThread.runActualRunnable();
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
// Start
assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
// Cancel
assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
// Dexopt thread finishes first.
mDexOptThread.runActualRunnable();
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
mCancelThread.runActualRunnable();
mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
// Always reschedule for periodic job
verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true);
verify(mDexOptHelper, never()).controlDexOptBlocking(true);
}
@Test
public void testStopByThermal() {
when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
initUntilBootCompleted();
assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture());
// Thermal cancel level
when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
argThreadRunnable.getValue().run();
verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
verifyLastControlDexOptBlockingCall(false);
}
@Test
public void testRunShellCommandWithInvalidUid() {
// Test uid cannot execute the command APIs
assertThrows(SecurityException.class, () -> mService.runBackgroundDexoptJob(null));
}
@Test
public void testCancelShellCommandWithInvalidUid() {
// Test uid cannot execute the command APIs
assertThrows(SecurityException.class, () -> mService.cancelBackgroundDexoptJob());
}
private void initUntilBootCompleted() {
ArgumentCaptor<BroadcastReceiver> argReceiver = ArgumentCaptor.forClass(
BroadcastReceiver.class);
ArgumentCaptor<IntentFilter> argIntentFilter = ArgumentCaptor.forClass(IntentFilter.class);
mService.systemReady();
verify(mContext).registerReceiver(argReceiver.capture(), argIntentFilter.capture());
assertThat(argIntentFilter.getValue().getAction(0)).isEqualTo(Intent.ACTION_BOOT_COMPLETED);
argReceiver.getValue().onReceive(mContext, null);
verify(mContext).unregisterReceiver(argReceiver.getValue());
ArgumentCaptor<JobInfo> argJobs = ArgumentCaptor.forClass(JobInfo.class);
verify(mJobScheduler, times(2)).schedule(argJobs.capture());
List<Integer> expectedJobIds = Arrays.asList(BackgroundDexOptService.JOB_IDLE_OPTIMIZE,
BackgroundDexOptService.JOB_POST_BOOT_UPDATE);
List<Integer> jobIds = argJobs.getAllValues().stream().map(job -> job.getId()).collect(
Collectors.toList());
assertThat(jobIds).containsExactlyElementsIn(expectedJobIds);
}
private void verifyLastControlDexOptBlockingCall(boolean expected) {
ArgumentCaptor<Boolean> argDexOptBlock = ArgumentCaptor.forClass(Boolean.class);
verify(mDexOptHelper, atLeastOnce()).controlDexOptBlocking(argDexOptBlock.capture());
assertThat(argDexOptBlock.getValue()).isEqualTo(expected);
}
private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params,
boolean expectedReschedule, int totalJobRuns) {
when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
assertThat(mService.onStartJob(jobService, params)).isTrue();
ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture());
argThreadRunnable.getValue().run();
verify(jobService).jobFinished(params, expectedReschedule);
// Never block
verify(mDexOptHelper, never()).controlDexOptBlocking(true);
verifyPerformDexOpt(DEFAULT_PACKAGE_LIST, totalJobRuns);
}
private void verifyPerformDexOpt(ArraySet<String> pkgs, int expectedRuns) {
ArgumentCaptor<DexoptOptions> dexOptOptions = ArgumentCaptor.forClass(DexoptOptions.class);
verify(mDexOptHelper, atLeastOnce()).performDexOptWithStatus(dexOptOptions.capture());
HashMap<String, Integer> primaryPkgs = new HashMap<>(); // K: pkg, V: dexopt runs left
for (String pkg : pkgs) {
primaryPkgs.put(pkg, expectedRuns);
}
for (DexoptOptions opt : dexOptOptions.getAllValues()) {
assertThat(pkgs).contains(opt.getPackageName());
assertThat(opt.isDexoptOnlySecondaryDex()).isFalse();
Integer count = primaryPkgs.get(opt.getPackageName());
assertThat(count).isNotNull();
if (count == 1) {
primaryPkgs.remove(opt.getPackageName());
} else {
primaryPkgs.put(opt.getPackageName(), count - 1);
}
}
assertThat(primaryPkgs).isEmpty();
}
private static class StartAndWaitThread extends Thread {
private final Runnable mActualRunnable;
private final CountDownLatch mLatch = new CountDownLatch(1);
private StartAndWaitThread(String name, Runnable runnable) {
super(name);
mActualRunnable = runnable;
}
private void runActualRunnable() {
mLatch.countDown();
}
@Override
public void run() {
// Thread is started but does not run actual code. This is for controlling the execution
// order while still meeting Thread.isAlive() check.
try {
mLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
mActualRunnable.run();
}
}
private Thread createAndStartExecutionThread(String name, Runnable runnable) {
final boolean isDexOptThread = !name.equals("DexOptCancel");
StartAndWaitThread thread = new StartAndWaitThread(name, runnable);
if (isDexOptThread) {
mDexOptThread = thread;
} else {
mCancelThread = thread;
}
thread.start();
return thread;
}
}