summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2021-11-16 00:52:39 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2021-11-16 00:52:39 +0000
commit7b9d5d9d29fdf19112809a1d82b2fdc17c89aa6b (patch)
tree953c0bcaebb35aff6b9e7bb5e55535b2ebc1828d
parentbbe80a038217d1ead4eb7104028536ba0f9ea006 (diff)
parentda73d5538c603e77a70c05c4763aed137daf5a7b (diff)
Merge "Add unit test for BackgroundDexOptService"
-rw-r--r--services/core/java/com/android/server/pm/BackgroundDexOptService.java88
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java2
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java480
3 files changed, 526 insertions, 44 deletions
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index af9c4013ce1e..27db2f90ab25 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -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() {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index accb7a00bb30..5f56d4e10036 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -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!");
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
new file mode 100644
index 000000000000..8223b8c86c5b
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
@@ -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;
+ }
+}