1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.pm;
18 
19 import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED;
20 import static com.android.server.pm.BackgroundDexOptService.STATUS_FATAL_ERROR;
21 import static com.android.server.pm.BackgroundDexOptService.STATUS_OK;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.argThat;
27 import static org.mockito.Mockito.atLeastOnce;
28 import static org.mockito.Mockito.doThrow;
29 import static org.mockito.Mockito.inOrder;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.reset;
33 import static org.mockito.Mockito.timeout;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 import static org.testng.Assert.assertThrows;
38 
39 import android.annotation.Nullable;
40 import android.app.job.JobInfo;
41 import android.app.job.JobParameters;
42 import android.app.job.JobScheduler;
43 import android.content.BroadcastReceiver;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.IntentFilter;
47 import android.os.HandlerThread;
48 import android.os.PowerManager;
49 import android.os.Process;
50 import android.os.SystemProperties;
51 import android.util.Log;
52 
53 import com.android.internal.util.IndentingPrintWriter;
54 import com.android.server.LocalServices;
55 import com.android.server.PinnerService;
56 import com.android.server.pm.dex.DexManager;
57 import com.android.server.pm.dex.DexoptOptions;
58 
59 import org.junit.After;
60 import org.junit.Assume;
61 import org.junit.Before;
62 import org.junit.Test;
63 import org.junit.runner.RunWith;
64 import org.mockito.ArgumentCaptor;
65 import org.mockito.InOrder;
66 import org.mockito.Mock;
67 import org.mockito.junit.MockitoJUnitRunner;
68 
69 import java.io.ByteArrayOutputStream;
70 import java.io.PrintWriter;
71 import java.util.ArrayList;
72 import java.util.Arrays;
73 import java.util.Collections;
74 import java.util.List;
75 import java.util.concurrent.CountDownLatch;
76 import java.util.stream.Collectors;
77 
78 @RunWith(MockitoJUnitRunner.class)
79 public final class BackgroundDexOptServiceUnitTest {
80     private static final String TAG = BackgroundDexOptServiceUnitTest.class.getSimpleName();
81 
82     private static final long USABLE_SPACE_NORMAL = 1_000_000_000;
83     private static final long STORAGE_LOW_BYTES = 1_000_000;
84 
85     private static final long TEST_WAIT_TIMEOUT_MS = 10_000;
86 
87     private static final String PACKAGE_AAA = "aaa";
88     private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb");
89     private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
90 
91     // Store expected dexopt sequence for verification.
92     private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>();
93 
94     @Mock
95     private Context mContext;
96     @Mock
97     private PackageManagerService mPackageManager;
98     @Mock
99     private DexOptHelper mDexOptHelper;
100     @Mock
101     private DexManager mDexManager;
102     @Mock
103     private PinnerService mPinnerService;
104     @Mock
105     private JobScheduler mJobScheduler;
106     @Mock
107     private BackgroundDexOptService.Injector mInjector;
108     @Mock
109     private BackgroundDexOptJobService mJobServiceForPostBoot;
110     @Mock
111     private BackgroundDexOptJobService mJobServiceForIdle;
112 
113     private final JobParameters mJobParametersForPostBoot =
114             createJobParameters(BackgroundDexOptService.JOB_POST_BOOT_UPDATE);
115     private final JobParameters mJobParametersForIdle =
116             createJobParameters(BackgroundDexOptService.JOB_IDLE_OPTIMIZE);
117 
createJobParameters(int jobId)118     private static JobParameters createJobParameters(int jobId) {
119         JobParameters params = mock(JobParameters.class);
120         when(params.getJobId()).thenReturn(jobId);
121         return params;
122     }
123 
124     private BackgroundDexOptService mService;
125 
126     private StartAndWaitThread mDexOptThread;
127     private StartAndWaitThread mCancelThread;
128 
129     @Before
setUp()130     public void setUp() throws Exception {
131         // These tests are only applicable to the legacy BackgroundDexOptService and cannot be run
132         // when ART Service is enabled.
133         Assume.assumeFalse(SystemProperties.getBoolean("dalvik.vm.useartservice", false));
134 
135         when(mInjector.getCallingUid()).thenReturn(Process.FIRST_APPLICATION_UID);
136         when(mInjector.getContext()).thenReturn(mContext);
137         when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
138         when(mInjector.getDexManager()).thenReturn(mDexManager);
139         when(mInjector.getPinnerService()).thenReturn(mPinnerService);
140         when(mInjector.getJobScheduler()).thenReturn(mJobScheduler);
141         when(mInjector.getPackageManagerService()).thenReturn(mPackageManager);
142 
143         // These mocking can be overwritten in some tests but still keep it here as alternative
144         // takes too many repetitive codes.
145         when(mInjector.getDataDirUsableSpace()).thenReturn(USABLE_SPACE_NORMAL);
146         when(mInjector.getDataDirStorageLowBytes()).thenReturn(STORAGE_LOW_BYTES);
147         when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
148         when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE);
149         when(mInjector.supportSecondaryDex()).thenReturn(true);
150         setupDexOptHelper();
151 
152         mService = new BackgroundDexOptService(mInjector);
153     }
154 
setupDexOptHelper()155     private void setupDexOptHelper() {
156         when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST);
157         when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> {
158             DexoptOptions opt = inv.getArgument(0);
159             if (opt.getPackageName().equals(PACKAGE_AAA)) {
160                 return mDexOptResultForPackageAAA;
161             }
162             return PackageDexOptimizer.DEX_OPT_PERFORMED;
163         });
164         when(mDexOptHelper.performDexOpt(any())).thenReturn(true);
165     }
166 
167     @After
tearDown()168     public void tearDown() throws Exception {
169         LocalServices.removeServiceForTest(BackgroundDexOptService.class);
170     }
171 
172     @Test
testGetService()173     public void testGetService() {
174         assertThat(BackgroundDexOptService.getService()).isEqualTo(mService);
175     }
176 
177     @Test
testBootCompleted()178     public void testBootCompleted() throws Exception {
179         initUntilBootCompleted();
180     }
181 
182     @Test
testNoExecutionForIdleJobBeforePostBootUpdate()183     public void testNoExecutionForIdleJobBeforePostBootUpdate() throws Exception {
184         initUntilBootCompleted();
185 
186         assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse();
187     }
188 
189     @Test
testNoExecutionForLowStorage()190     public void testNoExecutionForLowStorage() throws Exception {
191         initUntilBootCompleted();
192         when(mPackageManager.isStorageLow()).thenReturn(true);
193 
194         assertThat(mService.onStartJob(mJobServiceForPostBoot,
195                 mJobParametersForPostBoot)).isFalse();
196         verify(mDexOptHelper, never()).performDexOpt(any());
197     }
198 
199     @Test
testNoExecutionForNoOptimizablePackages()200     public void testNoExecutionForNoOptimizablePackages() throws Exception {
201         initUntilBootCompleted();
202         when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList());
203 
204         assertThat(mService.onStartJob(mJobServiceForPostBoot,
205                 mJobParametersForPostBoot)).isFalse();
206         verify(mDexOptHelper, never()).performDexOpt(any());
207     }
208 
209     @Test
testPostBootUpdateFullRun()210     public void testPostBootUpdateFullRun() throws Exception {
211         initUntilBootCompleted();
212 
213         runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
214                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
215                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
216     }
217 
218     @Test
testPostBootUpdateFullRunWithPackageFailure()219     public void testPostBootUpdateFullRunWithPackageFailure() throws Exception {
220         mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
221 
222         initUntilBootCompleted();
223 
224         runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
225                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
226                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
227 
228         assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
229         assertThat(getFailedPackageNamesSecondary()).isEmpty();
230     }
231 
232     @Test
testIdleJobFullRun()233     public void testIdleJobFullRun() throws Exception {
234         initUntilBootCompleted();
235         runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
236                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
237                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
238         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
239                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
240                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
241     }
242 
243     @Test
testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate()244     public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() throws Exception {
245         mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
246 
247         initUntilBootCompleted();
248 
249         runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
250                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
251                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
252 
253         assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
254         assertThat(getFailedPackageNamesSecondary()).isEmpty();
255 
256         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
257                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
258                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
259 
260         assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
261         assertThat(getFailedPackageNamesSecondary()).isEmpty();
262 
263         mService.notifyPackageChanged(PACKAGE_AAA);
264 
265         assertThat(getFailedPackageNamesPrimary()).isEmpty();
266         assertThat(getFailedPackageNamesSecondary()).isEmpty();
267 
268         // Succeed this time.
269         mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
270 
271         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
272                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
273                 /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null);
274 
275         assertThat(getFailedPackageNamesPrimary()).isEmpty();
276         assertThat(getFailedPackageNamesSecondary()).isEmpty();
277     }
278 
279     @Test
testIdleJobFullRunWithFatalError()280     public void testIdleJobFullRunWithFatalError() throws Exception {
281         initUntilBootCompleted();
282         runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
283                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
284                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
285 
286         doThrow(RuntimeException.class).when(mDexOptHelper).performDexOptWithStatus(any());
287 
288         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
289                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_FATAL_ERROR,
290                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
291     }
292 
293     @Test
testSystemReadyWhenDisabled()294     public void testSystemReadyWhenDisabled() throws Exception {
295         when(mInjector.isBackgroundDexOptDisabled()).thenReturn(true);
296 
297         mService.systemReady();
298 
299         verify(mContext, never()).registerReceiver(any(), any());
300     }
301 
302     @Test
testStopByCancelFlag()303     public void testStopByCancelFlag() throws Exception {
304         when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
305         initUntilBootCompleted();
306 
307         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
308 
309         ArgumentCaptor<Runnable> argDexOptThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
310         verify(mInjector, atLeastOnce()).createAndStartThread(any(),
311                 argDexOptThreadRunnable.capture());
312 
313         // Stopping requires a separate thread
314         HandlerThread cancelThread = new HandlerThread("Stopping");
315         cancelThread.start();
316         when(mInjector.createAndStartThread(any(), any())).thenReturn(cancelThread);
317 
318         // Cancel
319         assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
320 
321         // Capture Runnable for cancel
322         ArgumentCaptor<Runnable> argCancelThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
323         verify(mInjector, atLeastOnce()).createAndStartThread(any(),
324                 argCancelThreadRunnable.capture());
325 
326         // Execute cancelling part
327         cancelThread.getThreadHandler().post(argCancelThreadRunnable.getValue());
328 
329         verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(true);
330 
331         // Dexopt thread run and cancelled
332         argDexOptThreadRunnable.getValue().run();
333 
334         // Wait until cancellation Runnable is completed.
335         assertThat(cancelThread.getThreadHandler().runWithScissors(
336                 argCancelThreadRunnable.getValue(), TEST_WAIT_TIMEOUT_MS)).isTrue();
337 
338         // Now cancel completed
339         verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
340         verifyLastControlDexOptBlockingCall(false);
341     }
342 
343     @Test
testPostUpdateCancelFirst()344     public void testPostUpdateCancelFirst() throws Exception {
345         initUntilBootCompleted();
346         when(mInjector.createAndStartThread(any(), any())).thenAnswer(
347                 i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
348 
349         // Start
350         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
351         // Cancel
352         assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
353 
354         mCancelThread.runActualRunnable();
355 
356         // Wait until cancel has set the flag.
357         verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(
358                 true);
359 
360         mDexOptThread.runActualRunnable();
361 
362         // All threads should finish.
363         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
364         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
365 
366         // Retry later if post boot job was cancelled
367         verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
368         verifyLastControlDexOptBlockingCall(false);
369     }
370 
371     @Test
testPostUpdateCancelLater()372     public void testPostUpdateCancelLater() throws Exception {
373         initUntilBootCompleted();
374         when(mInjector.createAndStartThread(any(), any())).thenAnswer(
375                 i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
376 
377         // Start
378         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
379         // Cancel
380         assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
381 
382         // Dexopt thread runs and finishes
383         mDexOptThread.runActualRunnable();
384         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
385 
386         mCancelThread.runActualRunnable();
387         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
388 
389         // Already completed before cancel, so no rescheduling.
390         verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, false);
391         verify(mDexOptHelper, never()).controlDexOptBlocking(true);
392     }
393 
394     @Test
testPeriodicJobCancelFirst()395     public void testPeriodicJobCancelFirst() throws Exception {
396         initUntilBootCompleted();
397         when(mInjector.createAndStartThread(any(), any())).thenAnswer(
398                 i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
399 
400         // Start and finish post boot job
401         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
402         mDexOptThread.runActualRunnable();
403         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
404 
405         // Start
406         assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
407         // Cancel
408         assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
409 
410         mCancelThread.runActualRunnable();
411 
412         // Wait until cancel has set the flag.
413         verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(
414                 true);
415 
416         mDexOptThread.runActualRunnable();
417 
418         // All threads should finish.
419         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
420         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
421 
422         // The job should be rescheduled.
423         verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true /* wantsReschedule */);
424         verifyLastControlDexOptBlockingCall(false);
425     }
426 
427     @Test
testPeriodicJobCancelLater()428     public void testPeriodicJobCancelLater() throws Exception {
429         initUntilBootCompleted();
430         when(mInjector.createAndStartThread(any(), any())).thenAnswer(
431                 i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1)));
432 
433         // Start and finish post boot job
434         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
435         mDexOptThread.runActualRunnable();
436         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
437 
438         // Start
439         assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
440         // Cancel
441         assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue();
442 
443         // Dexopt thread finishes first.
444         mDexOptThread.runActualRunnable();
445         mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
446 
447         mCancelThread.runActualRunnable();
448         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
449 
450         // Always reschedule for periodic job
451         verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false);
452         verify(mDexOptHelper, never()).controlDexOptBlocking(true);
453     }
454 
455     @Test
testStopByThermal()456     public void testStopByThermal() throws Exception {
457         when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
458         initUntilBootCompleted();
459 
460         assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue();
461 
462         ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
463         verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture());
464 
465         // Thermal cancel level
466         when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
467 
468         argThreadRunnable.getValue().run();
469 
470         verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true);
471         verifyLastControlDexOptBlockingCall(false);
472     }
473 
474     @Test
testRunShellCommandWithInvalidUid()475     public void testRunShellCommandWithInvalidUid() {
476         // Test uid cannot execute the command APIs
477         assertThrows(SecurityException.class, () -> mService.runBackgroundDexoptJob(null));
478     }
479 
480     @Test
testCancelShellCommandWithInvalidUid()481     public void testCancelShellCommandWithInvalidUid() {
482         // Test uid cannot execute the command APIs
483         assertThrows(SecurityException.class, () -> mService.cancelBackgroundDexoptJob());
484     }
485 
486     @Test
testDisableJobSchedulerJobs()487     public void testDisableJobSchedulerJobs() throws Exception {
488         when(mInjector.getCallingUid()).thenReturn(Process.SHELL_UID);
489         mService.setDisableJobSchedulerJobs(true);
490         assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse();
491         verify(mDexOptHelper, never()).performDexOpt(any());
492         verify(mDexOptHelper, never()).performDexOptWithStatus(any());
493     }
494 
495     @Test
testSetDisableJobSchedulerJobsWithInvalidUid()496     public void testSetDisableJobSchedulerJobsWithInvalidUid() {
497         // Test uid cannot execute the command APIs
498         assertThrows(SecurityException.class, () -> mService.setDisableJobSchedulerJobs(true));
499     }
500 
initUntilBootCompleted()501     private void initUntilBootCompleted() throws Exception {
502         ArgumentCaptor<BroadcastReceiver> argReceiver = ArgumentCaptor.forClass(
503                 BroadcastReceiver.class);
504         ArgumentCaptor<IntentFilter> argIntentFilter = ArgumentCaptor.forClass(IntentFilter.class);
505 
506         mService.systemReady();
507 
508         verify(mContext).registerReceiver(argReceiver.capture(), argIntentFilter.capture());
509         assertThat(argIntentFilter.getValue().getAction(0)).isEqualTo(Intent.ACTION_BOOT_COMPLETED);
510 
511         argReceiver.getValue().onReceive(mContext, null);
512 
513         verify(mContext).unregisterReceiver(argReceiver.getValue());
514         ArgumentCaptor<JobInfo> argJobs = ArgumentCaptor.forClass(JobInfo.class);
515         verify(mJobScheduler, times(2)).schedule(argJobs.capture());
516 
517         List<Integer> expectedJobIds = Arrays.asList(BackgroundDexOptService.JOB_IDLE_OPTIMIZE,
518                 BackgroundDexOptService.JOB_POST_BOOT_UPDATE);
519         List<Integer> jobIds = argJobs.getAllValues().stream().map(job -> job.getId()).collect(
520                 Collectors.toList());
521         assertThat(jobIds).containsExactlyElementsIn(expectedJobIds);
522     }
523 
verifyLastControlDexOptBlockingCall(boolean expected)524     private void verifyLastControlDexOptBlockingCall(boolean expected) throws Exception {
525         ArgumentCaptor<Boolean> argDexOptBlock = ArgumentCaptor.forClass(Boolean.class);
526         verify(mDexOptHelper, atLeastOnce()).controlDexOptBlocking(argDexOptBlock.capture());
527         assertThat(argDexOptBlock.getValue()).isEqualTo(expected);
528     }
529 
runFullJob(BackgroundDexOptJobService jobService, JobParameters params, boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams, @Nullable String expectedSkippedPackage)530     private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params,
531             boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams,
532             @Nullable String expectedSkippedPackage) throws Exception {
533         when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
534         addFullRunSequence(expectedSkippedPackage);
535         assertThat(mService.onStartJob(jobService, params)).isTrue();
536 
537         ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
538         verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture());
539 
540         try {
541             argThreadRunnable.getValue().run();
542         } catch (RuntimeException e) {
543             if (expectedStatus != STATUS_FATAL_ERROR) {
544                 throw e;
545             }
546         }
547 
548         verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params,
549                 expectedReschedule);
550         // Never block
551         verify(mDexOptHelper, never()).controlDexOptBlocking(true);
552         if (expectedStatus != STATUS_FATAL_ERROR) {
553             verifyPerformDexOpt();
554         }
555         assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus);
556     }
557 
verifyPerformDexOpt()558     private void verifyPerformDexOpt() {
559         InOrder inOrder = inOrder(mDexOptHelper);
560         inOrder.verify(mDexOptHelper).getOptimizablePackages(any());
561         for (DexOptInfo info : mDexInfoSequence) {
562             if (info.isPrimary) {
563                 verify(mDexOptHelper).performDexOptWithStatus(
564                         argThat((option) -> option.getPackageName().equals(info.packageName)
565                                 && !option.isDexoptOnlySecondaryDex()));
566             } else {
567                 inOrder.verify(mDexOptHelper).performDexOpt(
568                         argThat((option) -> option.getPackageName().equals(info.packageName)
569                                 && option.isDexoptOnlySecondaryDex()));
570             }
571         }
572 
573         // Even InOrder cannot check the order if the same call is made multiple times.
574         // To check the order across multiple runs, we reset the mock so that order can be checked
575         // in each call.
576         mDexInfoSequence.clear();
577         reset(mDexOptHelper);
578         setupDexOptHelper();
579     }
580 
findDumpValueForKey(String key)581     private String findDumpValueForKey(String key) {
582         ByteArrayOutputStream out = new ByteArrayOutputStream();
583         PrintWriter pw = new PrintWriter(out, true);
584         IndentingPrintWriter writer = new IndentingPrintWriter(pw, "");
585         try {
586             mService.dump(writer);
587             writer.flush();
588             Log.i(TAG, "dump output:" + out.toString());
589             for (String line : out.toString().split(System.lineSeparator())) {
590                 String[] vals = line.split(":");
591                 if (vals[0].equals(key)) {
592                     if (vals.length == 2) {
593                         return vals[1].strip();
594                     } else {
595                         break;
596                     }
597                 }
598             }
599             return "";
600         } finally {
601             writer.close();
602         }
603     }
604 
findStringListFromDump(String key)605     List<String> findStringListFromDump(String key) {
606         String values = findDumpValueForKey(key);
607         if (values.isEmpty()) {
608             return Collections.emptyList();
609         }
610         return Arrays.asList(values.split(","));
611     }
612 
getFailedPackageNamesPrimary()613     private List<String> getFailedPackageNamesPrimary() {
614         return findStringListFromDump("mFailedPackageNamesPrimary");
615     }
616 
getFailedPackageNamesSecondary()617     private List<String> getFailedPackageNamesSecondary() {
618         return findStringListFromDump("mFailedPackageNamesSecondary");
619     }
620 
getLastExecutionStatus()621     private int getLastExecutionStatus() {
622         return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus"));
623     }
624 
625     private static class DexOptInfo {
626         public final String packageName;
627         public final boolean isPrimary;
628 
DexOptInfo(String packageName, boolean isPrimary)629         private DexOptInfo(String packageName, boolean isPrimary) {
630             this.packageName = packageName;
631             this.isPrimary = isPrimary;
632         }
633     }
634 
addFullRunSequence(@ullable String expectedSkippedPackage)635     private void addFullRunSequence(@Nullable String expectedSkippedPackage) {
636         for (String packageName : DEFAULT_PACKAGE_LIST) {
637             if (packageName.equals(expectedSkippedPackage)) {
638                 // only fails primary dexopt in mocking but add secodary
639                 mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
640             } else {
641                 mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true));
642                 mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
643             }
644         }
645     }
646 
647     private static class StartAndWaitThread extends Thread {
648         private final Runnable mActualRunnable;
649         private final CountDownLatch mLatch = new CountDownLatch(1);
650 
StartAndWaitThread(String name, Runnable runnable)651         private StartAndWaitThread(String name, Runnable runnable) {
652             super(name);
653             mActualRunnable = runnable;
654         }
655 
runActualRunnable()656         private void runActualRunnable() {
657             mLatch.countDown();
658         }
659 
660         @Override
run()661         public void run() {
662             // Thread is started but does not run actual code. This is for controlling the execution
663             // order while still meeting Thread.isAlive() check.
664             try {
665                 mLatch.await();
666             } catch (InterruptedException e) {
667                 throw new RuntimeException(e);
668             }
669             mActualRunnable.run();
670         }
671     }
672 
createAndStartExecutionThread(String name, Runnable runnable)673     private Thread createAndStartExecutionThread(String name, Runnable runnable) {
674         final boolean isDexOptThread = !name.equals("DexOptCancel");
675         StartAndWaitThread thread = new StartAndWaitThread(name, runnable);
676         if (isDexOptThread) {
677             mDexOptThread = thread;
678         } else {
679             mCancelThread = thread;
680         }
681         thread.start();
682         return thread;
683     }
684 }
685