1 /*
2  * Copyright (C) 2019 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 package android.jobscheduler.cts;
17 
18 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
19 import static android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD;
20 import static android.app.ActivityManager.getCapabilitiesSummary;
21 import static android.app.ActivityManager.procStateToString;
22 import static android.jobscheduler.cts.BaseJobSchedulerTest.HW_TIMEOUT_MULTIPLIER;
23 import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.ACTION_JOB_SCHEDULE_RESULT;
24 import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.EXTRA_REQUEST_JOB_UID_STATE;
25 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STARTED;
26 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
27 import static android.jobscheduler.cts.jobtestapp.TestJobService.INVALID_ADJ;
28 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_CAPABILITIES_KEY;
29 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_OOM_SCORE_ADJ_KEY;
30 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
31 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PROC_STATE_KEY;
32 import static android.server.wm.WindowManagerState.STATE_RESUMED;
33 
34 import static org.junit.Assert.assertEquals;
35 import static org.junit.Assert.assertTrue;
36 import static org.junit.Assert.fail;
37 
38 import android.Manifest;
39 import android.app.ActivityManager;
40 import android.app.AppOpsManager;
41 import android.app.compat.CompatChanges;
42 import android.app.job.JobParameters;
43 import android.app.job.JobScheduler;
44 import android.content.BroadcastReceiver;
45 import android.content.ComponentName;
46 import android.content.Context;
47 import android.content.Intent;
48 import android.content.IntentFilter;
49 import android.content.pm.PackageManager;
50 import android.jobscheduler.cts.jobtestapp.TestActivity;
51 import android.jobscheduler.cts.jobtestapp.TestFgsService;
52 import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver;
53 import android.net.NetworkPolicyManager;
54 import android.os.SystemClock;
55 import android.os.UserHandle;
56 import android.server.wm.WindowManagerStateHelper;
57 import android.util.Log;
58 import android.util.SparseArray;
59 
60 import com.android.compatibility.common.util.AppOpsUtils;
61 import com.android.compatibility.common.util.AppStandbyUtils;
62 import com.android.compatibility.common.util.CallbackAsserter;
63 import com.android.compatibility.common.util.SystemUtil;
64 
65 import java.util.Collections;
66 import java.util.Map;
67 import java.util.Set;
68 import java.util.function.BooleanSupplier;
69 
70 /**
71  * Common functions to interact with the test app.
72  */
73 class TestAppInterface implements AutoCloseable {
74     private static final String TAG = TestAppInterface.class.getSimpleName();
75 
76     public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L;
77 
78     static final String TEST_APP_PACKAGE = "android.jobscheduler.cts.jobtestapp";
79     private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestActivity";
80     private static final String TEST_APP_FGS = TEST_APP_PACKAGE + ".TestFgsService";
81     static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestJobSchedulerReceiver";
82 
83     private final Context mContext;
84     private final NetworkPolicyManager mNetworkPolicyManager;
85     private final int mJobId;
86     private final int mTestPackageUid;
87 
88     /* accesses must be synchronized on itself */
89     private final SparseArray<TestJobState> mTestJobStates = new SparseArray();
90 
TestAppInterface(Context ctx, int jobId)91     TestAppInterface(Context ctx, int jobId) {
92         mContext = ctx;
93         mJobId = jobId;
94         mNetworkPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
95 
96         try {
97             mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0);
98         } catch (PackageManager.NameNotFoundException e) {
99             throw new IllegalStateException("Test app uid not found", e);
100         }
101 
102         final IntentFilter intentFilter = new IntentFilter();
103         intentFilter.addAction(ACTION_JOB_STARTED);
104         intentFilter.addAction(ACTION_JOB_STOPPED);
105         intentFilter.addAction(ACTION_JOB_SCHEDULE_RESULT);
106         mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
107         SystemUtil.runShellCommand(
108                 "am compat enable --no-kill ALLOW_TEST_API_ACCESS " + TEST_APP_PACKAGE);
109         if (AppStandbyUtils.isAppStandbyEnabled()) {
110             // Disable the bucket elevation so that we put the app in lower buckets.
111             SystemUtil.runShellCommand(
112                     "am compat enable --no-kill SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET "
113                             + TEST_APP_PACKAGE);
114             // Force the test app out of the never bucket.
115             SystemUtil.runShellCommand("am set-standby-bucket " + TEST_APP_PACKAGE + " rare");
116         }
117         // Remove the app from the whitelist.
118         SystemUtil.runShellCommand("cmd deviceidle whitelist -" + TEST_APP_PACKAGE);
119         SystemUtil.runShellCommand("cmd netpolicy start-watching " + mTestPackageUid);
120         if (isTestAppTempWhitelisted()) {
121             Log.w(TAG, "Test package already in temp whitelist");
122             if (!removeTestAppFromTempWhitelist()) {
123                 // Don't block the test, but log in case it's an issue.
124                 Log.w(TAG, "Test package wasn't removed from the temp whitelist");
125             }
126         }
127     }
128 
cleanup()129     void cleanup() throws Exception {
130         final Intent cancelJobsIntent = new Intent(TestJobSchedulerReceiver.ACTION_CANCEL_JOBS);
131         cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
132         cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
133         mContext.sendBroadcast(cancelJobsIntent);
134         closeActivity();
135         stopFgs();
136         mContext.unregisterReceiver(mReceiver);
137         AppOpsUtils.reset(TEST_APP_PACKAGE);
138         SystemUtil.runWithShellPermissionIdentity(
139                 () -> CompatChanges.removePackageOverrides(
140                         TestAppInterface.TEST_APP_PACKAGE,
141                         Set.of(ENFORCE_MINIMUM_TIME_WINDOWS)),
142                 OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD, INTERACT_ACROSS_USERS_FULL);
143         SystemUtil.runShellCommand("am compat reset-all " + TEST_APP_PACKAGE);
144         // Remove the app from the whitelist.
145         SystemUtil.runShellCommand("cmd deviceidle whitelist -" + TEST_APP_PACKAGE);
146         removeTestAppFromTempWhitelist();
147         mTestJobStates.clear();
148         SystemUtil.runShellCommand("cmd netpolicy stop-watching");
149         SystemUtil.runShellCommand(
150                 "cmd jobscheduler reset-execution-quota -u current " + TEST_APP_PACKAGE);
151         forceStopApp(); // Clean up as much internal/temporary system state as possible
152     }
153 
154     @Override
close()155     public void close() throws Exception {
156         cleanup();
157     }
158 
scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob)159     void scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob)
160             throws Exception {
161         scheduleJob(allowWhileIdle, requiredNetworkType, asExpeditedJob, false);
162     }
163 
scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob, boolean asUserInitiatedJob)164     void scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob,
165             boolean asUserInitiatedJob) throws Exception {
166         scheduleJob(
167                 Map.of(
168                         TestJobSchedulerReceiver.EXTRA_ALLOW_IN_IDLE, allowWhileIdle,
169                         TestJobSchedulerReceiver.EXTRA_AS_EXPEDITED, asExpeditedJob,
170                         TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, asUserInitiatedJob
171                 ),
172                 Map.of(
173                         TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, requiredNetworkType
174                 ));
175     }
176 
generateScheduleJobIntent(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras, Map<String, Long> longExtras)177     private Intent generateScheduleJobIntent(Map<String, Boolean> booleanExtras,
178             Map<String, Integer> intExtras, Map<String, Long> longExtras) {
179         final Intent scheduleJobIntent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_JOB);
180         scheduleJobIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
181         if (!intExtras.containsKey(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY)) {
182             scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, mJobId);
183         }
184         booleanExtras.forEach(scheduleJobIntent::putExtra);
185         intExtras.forEach(scheduleJobIntent::putExtra);
186         longExtras.forEach(scheduleJobIntent::putExtra);
187         scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
188         return scheduleJobIntent;
189     }
190 
scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)191     void scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)
192             throws Exception {
193         scheduleJob(booleanExtras, intExtras, Collections.emptyMap());
194     }
195 
scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras, Map<String, Long> longExtras)196     void scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras,
197             Map<String, Long> longExtras) throws Exception {
198         final Intent scheduleJobIntent =
199                 generateScheduleJobIntent(booleanExtras, intExtras, longExtras);
200 
201         final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast(
202                 new IntentFilter(TestJobSchedulerReceiver.ACTION_JOB_SCHEDULE_RESULT));
203         mContext.sendBroadcast(scheduleJobIntent);
204         resultBroadcastAsserter.assertCalled("Didn't get schedule job result broadcast",
205                 15 /* 15 seconds */);
206     }
207 
postUiInitiatingNotification(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)208     void postUiInitiatingNotification(Map<String, Boolean> booleanExtras,
209             Map<String, Integer> intExtras) throws Exception {
210         final Intent intent =
211                 generateScheduleJobIntent(booleanExtras, intExtras, Collections.emptyMap());
212         intent.setAction(TestJobSchedulerReceiver.ACTION_POST_UI_INITIATING_NOTIFICATION);
213 
214         final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast(
215                 new IntentFilter(TestJobSchedulerReceiver.ACTION_NOTIFICATION_POSTED));
216         mContext.sendBroadcast(intent);
217         resultBroadcastAsserter.assertCalled("Didn't get notification posted broadcast",
218                 15 /* 15 seconds */);
219     }
220 
221     /** Post an alarm that will start an FGS in the test app. */
postFgsStartingAlarm()222     void postFgsStartingAlarm() throws Exception {
223         AppOpsUtils.setOpMode(TEST_APP_PACKAGE,
224                 AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM, AppOpsManager.MODE_ALLOWED);
225         final Intent intent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_FGS_START_ALARM);
226         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
227         intent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
228 
229         final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast(
230                 new IntentFilter(TestJobSchedulerReceiver.ACTION_ALARM_SCHEDULED));
231         mContext.sendBroadcast(intent);
232         resultBroadcastAsserter.assertCalled("Didn't get alarm scheduled broadcast",
233                 15 /* 15 seconds */);
234     }
235 
236     /** Asks (not forces) JobScheduler to run the job if constraints are met. */
runSatisfiedJob()237     void runSatisfiedJob() throws Exception {
238         runSatisfiedJob(mJobId);
239     }
240 
kill()241     void kill() {
242         SystemUtil.runShellCommand("am stop-app " + TEST_APP_PACKAGE);
243         mTestJobStates.clear();
244     }
245 
isNetworkBlockedByPolicy()246     boolean isNetworkBlockedByPolicy() {
247         try {
248             return SystemUtil.callWithShellPermissionIdentity(
249                     () -> mNetworkPolicyManager.isUidNetworkingBlocked(mTestPackageUid, false),
250                     Manifest.permission.OBSERVE_NETWORK_POLICY);
251         } catch (Exception e) {
252             // Unexpected while calling isUidNetworkingBlocked.
253             throw new RuntimeException(e);
254         }
255     }
256 
runSatisfiedJob(int jobId)257     void runSatisfiedJob(int jobId) throws Exception {
258         if (HW_TIMEOUT_MULTIPLIER > 1) {
259             // Device has increased HW multiplier. Wait a short amount of time before sending the
260             // run command since there's a higher chance JobScheduler's processing is delayed.
261             Thread.sleep(1_000L);
262         }
263         SystemUtil.runShellCommand("cmd jobscheduler run -s"
264                 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + jobId);
265     }
266 
267     /** Forces JobScheduler to run the job */
forceRunJob()268     void forceRunJob() throws Exception {
269         SystemUtil.runShellCommand("cmd jobscheduler run -f"
270                 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId);
271     }
272 
stopJob(int stopReason, int internalStopReason)273     void stopJob(int stopReason, int internalStopReason) throws Exception {
274         SystemUtil.runShellCommand("cmd jobscheduler stop"
275                 + " -u " + UserHandle.myUserId()
276                 + " -s " + stopReason + " -i " + internalStopReason
277                 + " " + TEST_APP_PACKAGE + " " + mJobId);
278     }
279 
forceStopApp()280     void forceStopApp() {
281         SystemUtil.runShellCommand("am force-stop"
282                 + " --user " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE);
283     }
284 
setTestPackageRestricted(boolean restricted)285     void setTestPackageRestricted(boolean restricted) throws Exception {
286         AppOpsUtils.setOpMode(TEST_APP_PACKAGE, "RUN_ANY_IN_BACKGROUND",
287                 restricted ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED);
288     }
289 
cancelJob()290     void cancelJob() throws Exception {
291         SystemUtil.runShellCommand("cmd jobscheduler cancel"
292                 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId);
293     }
294 
startAndKeepTestActivity()295     void startAndKeepTestActivity() {
296         startAndKeepTestActivity(false);
297     }
298 
startAndKeepTestActivity(boolean waitForResume)299     void startAndKeepTestActivity(boolean waitForResume) {
300         final Intent testActivity = new Intent();
301         testActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
302         ComponentName testComponentName = new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY);
303         testActivity.setComponent(testComponentName);
304         mContext.startActivity(testActivity);
305         if (waitForResume) {
306             new WindowManagerStateHelper().waitForActivityState(testComponentName, STATE_RESUMED);
307         }
308     }
309 
closeActivity()310     void closeActivity() {
311         closeActivity(false);
312     }
313 
closeActivity(boolean waitForClose)314     void closeActivity(boolean waitForClose) {
315         mContext.sendBroadcast(new Intent(TestActivity.ACTION_FINISH_ACTIVITY));
316         if (waitForClose) {
317             ComponentName testComponentName =
318                     new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY);
319             new WindowManagerStateHelper().waitForActivityRemoved(testComponentName);
320         }
321     }
322 
isTestAppTempWhitelisted()323     boolean isTestAppTempWhitelisted() {
324         final String output = SystemUtil.runShellCommand("cmd deviceidle tempwhitelist").trim();
325         final String expectedText = "UID=" + UserHandle.getAppId(mTestPackageUid);
326         for (String line : output.split("\n")) {
327             if (line.contains(expectedText)) {
328                 return true;
329             }
330         }
331         return false;
332     }
333 
removeTestAppFromTempWhitelist()334     boolean removeTestAppFromTempWhitelist() {
335         SystemUtil.runShellCommand("cmd deviceidle tempwhitelist"
336                 + " -u " + UserHandle.myUserId()
337                 + " -r " + TEST_APP_PACKAGE);
338         final boolean removed = waitUntilTrue(3_000, () -> !isTestAppTempWhitelisted());
339         if (!removed) {
340             Log.e(TAG, "Test app wasn't removed from temp whitelist");
341         }
342         return removed;
343     }
344 
345     /** Directly start the FGS in the test app. */
startFgs()346     void startFgs() throws Exception {
347         final Intent intent = new Intent(TestJobSchedulerReceiver.ACTION_START_FGS);
348         intent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
349 
350         final CallbackAsserter resultBroadcastAsserter =
351                 CallbackAsserter.forBroadcast(new IntentFilter(TestFgsService.ACTION_FGS_STARTED));
352         mContext.sendBroadcast(intent);
353         resultBroadcastAsserter.assertCalled("Didn't get FGS started broadcast",
354                 15 /* 15 seconds */);
355     }
356 
stopFgs()357     void stopFgs() {
358         final Intent testFgs = new Intent(TestFgsService.ACTION_STOP_FOREGROUND);
359         mContext.sendBroadcast(testFgs);
360     }
361 
362     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
363         @Override
364         public void onReceive(Context context, Intent intent) {
365             Log.d(TAG, "Received action " + intent.getAction());
366             switch (intent.getAction()) {
367                 case ACTION_JOB_STARTED:
368                 case ACTION_JOB_STOPPED:
369                     final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
370                     Log.d(TAG, "JobId: " + params.getJobId());
371                     synchronized (mTestJobStates) {
372                         TestJobState jobState = mTestJobStates.get(params.getJobId());
373                         if (jobState == null) {
374                             jobState = new TestJobState();
375                             mTestJobStates.put(params.getJobId(), jobState);
376                         } else {
377                             jobState.reset();
378                         }
379                         jobState.running = ACTION_JOB_STARTED.equals(intent.getAction());
380                         jobState.params = params;
381                         // With these broadcasts, the job is/was running, and therefore scheduling
382                         // was successful.
383                         jobState.scheduleResult = JobScheduler.RESULT_SUCCESS;
384                         if (intent.getBooleanExtra(EXTRA_REQUEST_JOB_UID_STATE, false)) {
385                             jobState.procState = intent.getIntExtra(JOB_PROC_STATE_KEY,
386                                     ActivityManager.PROCESS_STATE_NONEXISTENT);
387                             jobState.capabilities = intent.getIntExtra(JOB_CAPABILITIES_KEY,
388                                     ActivityManager.PROCESS_CAPABILITY_NONE);
389                             jobState.oomScoreAdj = intent.getIntExtra(JOB_OOM_SCORE_ADJ_KEY,
390                                     INVALID_ADJ);
391                         }
392                     }
393                     break;
394                 case ACTION_JOB_SCHEDULE_RESULT:
395                     synchronized (mTestJobStates) {
396                         final int jobId = intent.getIntExtra(
397                                 TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, 0);
398                         TestJobState jobState = mTestJobStates.get(jobId);
399                         if (jobState == null) {
400                             jobState = new TestJobState();
401                             mTestJobStates.put(jobId, jobState);
402                         } else {
403                             jobState.reset();
404                         }
405                         jobState.running = false;
406                         jobState.params = null;
407                         jobState.scheduleResult = intent.getIntExtra(
408                                 TestJobSchedulerReceiver.EXTRA_SCHEDULE_RESULT, -1);
409                     }
410                     break;
411             }
412         }
413     };
414 
awaitJobStart(long maxWait)415     boolean awaitJobStart(long maxWait) throws Exception {
416         return awaitJobStart(mJobId, maxWait);
417     }
418 
awaitJobStart(int jobId, long maxWait)419     boolean awaitJobStart(int jobId, long maxWait) throws Exception {
420         return waitUntilTrue(maxWait, () -> {
421             synchronized (mTestJobStates) {
422                 TestJobState jobState = mTestJobStates.get(jobId);
423                 return jobState != null && jobState.running;
424             }
425         });
426     }
427 
428     boolean awaitJobStop(long maxWait) throws Exception {
429         return waitUntilTrue(maxWait, () -> {
430             synchronized (mTestJobStates) {
431                 TestJobState jobState = mTestJobStates.get(mJobId);
432                 return jobState != null && !jobState.running;
433             }
434         });
435     }
436 
437     private String getJobState(int jobId) throws Exception {
438         return SystemUtil.runShellCommand(
439                 "cmd jobscheduler get-job-state --user cur " + TEST_APP_PACKAGE + " " + jobId)
440                 .trim();
441     }
442 
443     void assertJobNotReady(int jobId) throws Exception {
444         String state = getJobState(jobId);
445         assertTrue("Job unexpectedly ready, in state: " + state, !state.contains("ready"));
446     }
447 
448     void assertJobUidState(int procState, int capabilities, int oomScoreAdj) {
449         synchronized (mTestJobStates) {
450             TestJobState jobState = mTestJobStates.get(mJobId);
451             if (jobState == null) {
452                 fail("Job not started");
453             }
454             assertEquals("procState expected=" + procStateToString(procState)
455                             + ",actual=" + procStateToString(jobState.procState),
456                     procState, jobState.procState);
457             assertEquals("capabilities expected=" + getCapabilitiesSummary(capabilities)
458                             + ",actual=" + getCapabilitiesSummary(jobState.capabilities),
459                     capabilities, jobState.capabilities);
460             assertEquals("Unexpected oomScoreAdj", oomScoreAdj, jobState.oomScoreAdj);
461         }
462     }
463 
464     boolean awaitJobScheduleResult(long maxWaitMs, int jobResult) throws Exception {
465         return awaitJobScheduleResult(mJobId, maxWaitMs, jobResult);
466     }
467 
468     boolean awaitJobScheduleResult(int jobId, long maxWaitMs, int jobResult) throws Exception {
469         return waitUntilTrue(maxWaitMs, () -> {
470             synchronized (mTestJobStates) {
471                 TestJobState jobState = mTestJobStates.get(jobId);
472                 return jobState != null && jobState.scheduleResult == jobResult;
473             }
474         });
475     }
476 
477     private boolean waitUntilTrue(long maxWait, BooleanSupplier condition) {
478         final long deadline = SystemClock.uptimeMillis() + maxWait;
479         do {
480             SystemClock.sleep(500);
481         } while (!condition.getAsBoolean() && SystemClock.uptimeMillis() < deadline);
482         return condition.getAsBoolean();
483     }
484 
485     JobParameters getLastParams() {
486         synchronized (mTestJobStates) {
487             TestJobState jobState = mTestJobStates.get(mJobId);
488             return jobState == null ? null : jobState.params;
489         }
490     }
491 
492     private static final class TestJobState {
493         int scheduleResult;
494         boolean running;
495         int procState;
496         int capabilities;
497         int oomScoreAdj;
498         JobParameters params;
499 
500         TestJobState() {
501             initState();
502         }
503 
504         private void reset() {
505             initState();
506         }
507 
508         private void initState() {
509             running = false;
510             procState = ActivityManager.PROCESS_STATE_NONEXISTENT;
511             capabilities = ActivityManager.PROCESS_CAPABILITY_NONE;
512             oomScoreAdj = INVALID_ADJ;
513             scheduleResult = -1;
514         }
515     }
516 }
517