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