1 /*
2  * Copyright (C) 2018 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 android.jobscheduler;
18 
19 import android.annotation.TargetApi;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.app.job.JobWorkItem;
25 import android.content.ClipData;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.net.Uri;
30 import android.os.Process;
31 import android.util.Log;
32 
33 import junit.framework.Assert;
34 
35 import java.util.ArrayList;
36 import java.util.concurrent.CountDownLatch;
37 import java.util.concurrent.TimeUnit;
38 
39 /**
40  * Handles callback from the framework {@link android.app.job.JobScheduler}. The behaviour of this
41  * class is configured through the static
42  * {@link TestEnvironment}.
43  */
44 @TargetApi(21)
45 public class MockJobService extends JobService {
46     private static final String TAG = "MockJobService";
47 
48     /** Wait this long before timing out the test. */
49     private static final long DEFAULT_TIMEOUT_MILLIS = 30000L; // 30 seconds.
50 
51     private JobParameters mParams;
52 
53     ArrayList<JobWorkItem> mReceivedWork = new ArrayList<>();
54 
55     ArrayList<JobWorkItem> mPendingCompletions = new ArrayList<>();
56 
57     private boolean mWaitingForStop;
58 
59     @Override
onDestroy()60     public void onDestroy() {
61         super.onDestroy();
62         Log.i(TAG, "Destroying test service");
63         if (TestEnvironment.getTestEnvironment().getExpectedWork() != null) {
64             TestEnvironment.getTestEnvironment().notifyExecution(mParams, 0, 0, mReceivedWork,
65                     null);
66         }
67     }
68 
69     @Override
onCreate()70     public void onCreate() {
71         super.onCreate();
72         Log.i(TAG, "Created test service.");
73     }
74 
75     @Override
onStartJob(JobParameters params)76     public boolean onStartJob(JobParameters params) {
77         Log.i(TAG, "Test job executing: " + params.getJobId());
78         mParams = params;
79 
80         int permCheckRead = PackageManager.PERMISSION_DENIED;
81         int permCheckWrite = PackageManager.PERMISSION_DENIED;
82         ClipData clip = params.getClipData();
83         if (clip != null) {
84             permCheckRead = checkUriPermission(clip.getItemAt(0).getUri(), Process.myPid(),
85                     Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
86             permCheckWrite = checkUriPermission(clip.getItemAt(0).getUri(), Process.myPid(),
87                     Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
88         }
89 
90         TestWorkItem[] expectedWork = TestEnvironment.getTestEnvironment().getExpectedWork();
91         if (expectedWork != null) {
92             try {
93                 if (!TestEnvironment.getTestEnvironment().awaitDoWork()) {
94                     TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
95                             permCheckWrite, null, "Spent too long waiting to start executing work");
96                     return false;
97                 }
98             } catch (InterruptedException e) {
99                 TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
100                         permCheckWrite, null, "Failed waiting for work: " + e);
101                 return false;
102             }
103             JobWorkItem work;
104             int index = 0;
105             while ((work = params.dequeueWork()) != null) {
106                 Log.i(TAG, "Received work #" + index + ": " + work.getIntent());
107                 mReceivedWork.add(work);
108 
109                 int flags = 0;
110 
111                 if (index < expectedWork.length) {
112                     TestWorkItem expected = expectedWork[index];
113                     int grantFlags = work.getIntent().getFlags();
114                     if (expected.requireUrisGranted != null) {
115                         for (int ui = 0; ui < expected.requireUrisGranted.length; ui++) {
116                             if ((grantFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
117                                 if (checkUriPermission(expected.requireUrisGranted[ui],
118                                         Process.myPid(), Process.myUid(),
119                                         Intent.FLAG_GRANT_READ_URI_PERMISSION)
120                                         != PackageManager.PERMISSION_GRANTED) {
121                                     TestEnvironment.getTestEnvironment().notifyExecution(params,
122                                             permCheckRead, permCheckWrite, null,
123                                             "Expected read permission but not granted: "
124                                                     + expected.requireUrisGranted[ui]
125                                                     + " @ #" + index);
126                                     return false;
127                                 }
128                             }
129                             if ((grantFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
130                                 if (checkUriPermission(expected.requireUrisGranted[ui],
131                                         Process.myPid(), Process.myUid(),
132                                         Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
133                                         != PackageManager.PERMISSION_GRANTED) {
134                                     TestEnvironment.getTestEnvironment().notifyExecution(params,
135                                             permCheckRead, permCheckWrite, null,
136                                             "Expected write permission but not granted: "
137                                                     + expected.requireUrisGranted[ui]
138                                                     + " @ #" + index);
139                                     return false;
140                                 }
141                             }
142                         }
143                     }
144                     if (expected.requireUrisNotGranted != null) {
145                         // XXX note no delay here, current impl will have fully revoked the
146                         // permission by the time we return from completing the last work.
147                         for (int ui = 0; ui < expected.requireUrisNotGranted.length; ui++) {
148                             if ((grantFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
149                                 if (checkUriPermission(expected.requireUrisNotGranted[ui],
150                                         Process.myPid(), Process.myUid(),
151                                         Intent.FLAG_GRANT_READ_URI_PERMISSION)
152                                         != PackageManager.PERMISSION_DENIED) {
153                                     TestEnvironment.getTestEnvironment().notifyExecution(params,
154                                             permCheckRead, permCheckWrite, null,
155                                             "Not expected read permission but granted: "
156                                                     + expected.requireUrisNotGranted[ui]
157                                                     + " @ #" + index);
158                                     return false;
159                                 }
160                             }
161                             if ((grantFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
162                                 if (checkUriPermission(expected.requireUrisNotGranted[ui],
163                                         Process.myPid(), Process.myUid(),
164                                         Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
165                                         != PackageManager.PERMISSION_DENIED) {
166                                     TestEnvironment.getTestEnvironment().notifyExecution(params,
167                                             permCheckRead, permCheckWrite, null,
168                                             "Not expected write permission but granted: "
169                                                     + expected.requireUrisNotGranted[ui]
170                                                     + " @ #" + index);
171                                     return false;
172                                 }
173                             }
174                         }
175                     }
176 
177                     flags = expected.flags;
178 
179                     if ((flags & TestWorkItem.FLAG_WAIT_FOR_STOP) != 0) {
180                         Log.i(TAG, "Now waiting to stop");
181                         mWaitingForStop = true;
182                         TestEnvironment.getTestEnvironment().notifyWaitingForStop();
183                         return true;
184                     }
185 
186                     if ((flags & TestWorkItem.FLAG_COMPLETE_NEXT) != 0) {
187                         if (!processNextPendingCompletion()) {
188                             TestEnvironment.getTestEnvironment().notifyExecution(params,
189                                     0, 0, null,
190                                     "Expected to complete next pending work but there was none: "
191                                             + " @ #" + index);
192                             return false;
193                         }
194                     }
195                 }
196 
197                 if ((flags & TestWorkItem.FLAG_DELAY_COMPLETE_PUSH_BACK) != 0) {
198                     mPendingCompletions.add(work);
199                 } else if ((flags & TestWorkItem.FLAG_DELAY_COMPLETE_PUSH_TOP) != 0) {
200                     mPendingCompletions.add(0, work);
201                 } else {
202                     mParams.completeWork(work);
203                 }
204 
205                 if (index < expectedWork.length) {
206                     TestWorkItem expected = expectedWork[index];
207                     if (expected.subitems != null) {
208                         final TestWorkItem[] sub = expected.subitems;
209                         final JobInfo ji = expected.jobInfo;
210                         final JobScheduler js = (JobScheduler) getSystemService(
211                                 Context.JOB_SCHEDULER_SERVICE);
212                         for (int subi = 0; subi < sub.length; subi++) {
213                             js.enqueue(ji, new JobWorkItem(sub[subi].intent));
214                         }
215                     }
216                 }
217 
218                 index++;
219             }
220 
221             if (processNextPendingCompletion()) {
222                 // We had some pending completions, clean them all out...
223                 while (processNextPendingCompletion()) {
224                 }
225                 // ...and we need to do a final dequeue to complete the job, which should not
226                 // return any remaining work.
227                 if ((work = params.dequeueWork()) != null) {
228                     TestEnvironment.getTestEnvironment().notifyExecution(params,
229                             0, 0, null,
230                             "Expected no remaining work after dequeue pending, but got: " + work);
231                 }
232             }
233 
234             Log.i(TAG, "Done with all work at #" + index);
235             // We don't notifyExecution here because we want to make sure the job properly
236             // stops itself.
237             return true;
238         } else {
239             boolean continueAfterStart
240                     = TestEnvironment.getTestEnvironment().handleContinueAfterStart();
241             try {
242                 if (!TestEnvironment.getTestEnvironment().awaitDoJob()) {
243                     TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
244                             permCheckWrite, null, "Spent too long waiting to start job");
245                     return false;
246                 }
247             } catch (InterruptedException e) {
248                 TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
249                         permCheckWrite, null, "Failed waiting to start job: " + e);
250                 return false;
251             }
252             TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
253                     permCheckWrite, null, null);
254             return continueAfterStart;
255         }
256     }
257 
processNextPendingCompletion()258     boolean processNextPendingCompletion() {
259         if (mPendingCompletions.size() <= 0) {
260             return false;
261         }
262 
263         JobWorkItem next = mPendingCompletions.remove(0);
264         mParams.completeWork(next);
265         return true;
266     }
267 
268     @Override
onStopJob(JobParameters params)269     public boolean onStopJob(JobParameters params) {
270         Log.i(TAG, "Received stop callback");
271         TestEnvironment.getTestEnvironment().notifyStopped();
272         return mWaitingForStop;
273     }
274 
275     public static final class TestWorkItem {
276         /**
277          * Stop processing work for now, waiting for the service to be stopped.
278          */
279         public static final int FLAG_WAIT_FOR_STOP = 1<<0;
280         /**
281          * Don't complete this work now, instead push it on the back of the stack of
282          * pending completions.
283          */
284         public static final int FLAG_DELAY_COMPLETE_PUSH_BACK = 1<<1;
285         /**
286          * Don't complete this work now, instead insert to the top of the stack of
287          * pending completions.
288          */
289         public static final int FLAG_DELAY_COMPLETE_PUSH_TOP = 1<<2;
290         /**
291          * Complete next pending completion on the stack before completing this one.
292          */
293         public static final int FLAG_COMPLETE_NEXT = 1<<3;
294 
295         public final Intent intent;
296         public final JobInfo jobInfo;
297         public final int flags;
298         public final int deliveryCount;
299         public final TestWorkItem[] subitems;
300         public final Uri[] requireUrisGranted;
301         public final Uri[] requireUrisNotGranted;
302 
TestWorkItem(Intent _intent)303         public TestWorkItem(Intent _intent) {
304             intent = _intent;
305             jobInfo = null;
306             flags = 0;
307             deliveryCount = 1;
308             subitems = null;
309             requireUrisGranted = null;
310             requireUrisNotGranted = null;
311         }
312 
TestWorkItem(Intent _intent, int _flags)313         public TestWorkItem(Intent _intent, int _flags) {
314             intent = _intent;
315             jobInfo = null;
316             flags = _flags;
317             deliveryCount = 1;
318             subitems = null;
319             requireUrisGranted = null;
320             requireUrisNotGranted = null;
321         }
322 
TestWorkItem(Intent _intent, int _flags, int _deliveryCount)323         public TestWorkItem(Intent _intent, int _flags, int _deliveryCount) {
324             intent = _intent;
325             jobInfo = null;
326             flags = _flags;
327             deliveryCount = _deliveryCount;
328             subitems = null;
329             requireUrisGranted = null;
330             requireUrisNotGranted = null;
331         }
332 
TestWorkItem(Intent _intent, JobInfo _jobInfo, TestWorkItem[] _subitems)333         public TestWorkItem(Intent _intent, JobInfo _jobInfo, TestWorkItem[] _subitems) {
334             intent = _intent;
335             jobInfo = _jobInfo;
336             flags = 0;
337             deliveryCount = 1;
338             subitems = _subitems;
339             requireUrisGranted = null;
340             requireUrisNotGranted = null;
341         }
342 
TestWorkItem(Intent _intent, Uri[] _requireUrisGranted, Uri[] _requireUrisNotGranted)343         public TestWorkItem(Intent _intent, Uri[] _requireUrisGranted,
344                 Uri[] _requireUrisNotGranted) {
345             intent = _intent;
346             jobInfo = null;
347             flags = 0;
348             deliveryCount = 1;
349             subitems = null;
350             requireUrisGranted = _requireUrisGranted;
351             requireUrisNotGranted = _requireUrisNotGranted;
352         }
353 
354         @Override
toString()355         public String toString() {
356             return "TestWorkItem { " + intent + " dc=" + deliveryCount + " }";
357         }
358     }
359 
360     /**
361      * Configures the expected behaviour for each test. This object is shared across consecutive
362      * tests, so to clear state each test is responsible for calling
363      * {@link TestEnvironment#setUp()}.
364      */
365     public static final class TestEnvironment {
366 
367         private static TestEnvironment kTestEnvironment;
368         //public static final int INVALID_JOB_ID = -1;
369 
370         private CountDownLatch mLatch;
371         private CountDownLatch mWaitingForStopLatch;
372         private CountDownLatch mDoJobLatch;
373         private CountDownLatch mStoppedLatch;
374         private CountDownLatch mDoWorkLatch;
375         private TestWorkItem[] mExpectedWork;
376         private boolean mContinueAfterStart;
377         private JobParameters mExecutedJobParameters;
378         private int mExecutedPermCheckRead;
379         private int mExecutedPermCheckWrite;
380         private ArrayList<JobWorkItem> mExecutedReceivedWork;
381         private String mExecutedErrorMessage;
382 
getTestEnvironment()383         public static TestEnvironment getTestEnvironment() {
384             if (kTestEnvironment == null) {
385                 kTestEnvironment = new TestEnvironment();
386             }
387             return kTestEnvironment;
388         }
389 
getExpectedWork()390         public TestWorkItem[] getExpectedWork() {
391             return mExpectedWork;
392         }
393 
getLastJobParameters()394         public JobParameters getLastJobParameters() {
395             return mExecutedJobParameters;
396         }
397 
getLastPermCheckRead()398         public int getLastPermCheckRead() {
399             return mExecutedPermCheckRead;
400         }
401 
getLastPermCheckWrite()402         public int getLastPermCheckWrite() {
403             return mExecutedPermCheckWrite;
404         }
405 
getLastReceivedWork()406         public ArrayList<JobWorkItem> getLastReceivedWork() {
407             return mExecutedReceivedWork;
408         }
409 
getLastErrorMessage()410         public String getLastErrorMessage() {
411             return mExecutedErrorMessage;
412         }
413 
414         /**
415          * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
416          * job on this service.
417          */
awaitExecution()418         public boolean awaitExecution() throws InterruptedException {
419             return awaitExecution(DEFAULT_TIMEOUT_MILLIS);
420         }
421 
awaitExecution(long timeoutMillis)422         public boolean awaitExecution(long timeoutMillis) throws InterruptedException {
423             final boolean executed = mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
424             if (getLastErrorMessage() != null) {
425                 Assert.fail(getLastErrorMessage());
426             }
427             return executed;
428         }
429 
430         /**
431          * Block the test thread, expecting to timeout but still listening to ensure that no jobs
432          * land in the interim.
433          * @return True if the latch timed out waiting on an execution.
434          */
awaitTimeout()435         public boolean awaitTimeout() throws InterruptedException {
436             return awaitTimeout(DEFAULT_TIMEOUT_MILLIS);
437         }
438 
awaitTimeout(long timeoutMillis)439         public boolean awaitTimeout(long timeoutMillis) throws InterruptedException {
440             return !mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
441         }
442 
awaitWaitingForStop()443         public boolean awaitWaitingForStop() throws InterruptedException {
444             return mWaitingForStopLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
445         }
446 
awaitDoWork()447         public boolean awaitDoWork() throws InterruptedException {
448             return mDoWorkLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
449         }
450 
awaitDoJob()451         public boolean awaitDoJob() throws InterruptedException {
452             if (mDoJobLatch == null) {
453                 return true;
454             }
455             return mDoJobLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
456         }
457 
awaitStopped()458         public boolean awaitStopped() throws InterruptedException {
459             return mStoppedLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
460         }
461 
notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite, ArrayList<JobWorkItem> receivedWork, String errorMsg)462         private void notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite,
463                 ArrayList<JobWorkItem> receivedWork, String errorMsg) {
464             //Log.d(TAG, "Job executed:" + params.getJobId());
465             mExecutedJobParameters = params;
466             mExecutedPermCheckRead = permCheckRead;
467             mExecutedPermCheckWrite = permCheckWrite;
468             mExecutedReceivedWork = receivedWork;
469             mExecutedErrorMessage = errorMsg;
470             mLatch.countDown();
471         }
472 
notifyWaitingForStop()473         private void notifyWaitingForStop() {
474             mWaitingForStopLatch.countDown();
475         }
476 
notifyStopped()477         private void notifyStopped() {
478             if (mStoppedLatch != null) {
479                 mStoppedLatch.countDown();
480             }
481         }
482 
setExpectedExecutions(int numExecutions)483         public void setExpectedExecutions(int numExecutions) {
484             // For no executions expected, set count to 1 so we can still block for the timeout.
485             if (numExecutions == 0) {
486                 mLatch = new CountDownLatch(1);
487             } else {
488                 mLatch = new CountDownLatch(numExecutions);
489             }
490             mWaitingForStopLatch = null;
491             mDoJobLatch = null;
492             mStoppedLatch = null;
493             mDoWorkLatch = null;
494             mExpectedWork = null;
495             mContinueAfterStart = false;
496         }
497 
setExpectedWaitForStop()498         public void setExpectedWaitForStop() {
499             mWaitingForStopLatch = new CountDownLatch(1);
500         }
501 
setExpectedWork(TestWorkItem[] work)502         public void setExpectedWork(TestWorkItem[] work) {
503             mExpectedWork = work;
504             mDoWorkLatch = new CountDownLatch(1);
505         }
506 
setExpectedStopped()507         public void setExpectedStopped() {
508             mStoppedLatch = new CountDownLatch(1);
509         }
510 
readyToWork()511         public void readyToWork() {
512             mDoWorkLatch.countDown();
513         }
514 
setExpectedWaitForRun()515         public void setExpectedWaitForRun() {
516             mDoJobLatch = new CountDownLatch(1);
517         }
518 
readyToRun()519         public void readyToRun() {
520             mDoJobLatch.countDown();
521         }
522 
setContinueAfterStart()523         public void setContinueAfterStart() {
524             mContinueAfterStart = true;
525         }
526 
handleContinueAfterStart()527         public boolean handleContinueAfterStart() {
528             boolean res = mContinueAfterStart;
529             mContinueAfterStart = false;
530             return res;
531         }
532 
533         /** Called in each testCase#setup */
setUp()534         public void setUp() {
535             mLatch = null;
536             mExecutedJobParameters = null;
537         }
538 
539     }
540 }