Merge "Add unit test for BackgroundDexOptService"
This commit is contained in:
committed by
Android (Google) Code Review
commit
7b9d5d9d29
@@ -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() {
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user