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