1 /*
2  * Copyright (C) 2014 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(params);
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         private JobParameters mStopJobParameters;
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 
getLastStartJobParameters()395         public JobParameters getLastStartJobParameters() {
396             return mExecutedJobParameters;
397         }
398 
getLastStopJobParameters()399         public JobParameters getLastStopJobParameters() {
400             return mStopJobParameters;
401         }
402 
getLastPermCheckRead()403         public int getLastPermCheckRead() {
404             return mExecutedPermCheckRead;
405         }
406 
getLastPermCheckWrite()407         public int getLastPermCheckWrite() {
408             return mExecutedPermCheckWrite;
409         }
410 
getLastReceivedWork()411         public ArrayList<JobWorkItem> getLastReceivedWork() {
412             return mExecutedReceivedWork;
413         }
414 
getLastErrorMessage()415         public String getLastErrorMessage() {
416             return mExecutedErrorMessage;
417         }
418 
419         /**
420          * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
421          * job on this service.
422          */
awaitExecution()423         public boolean awaitExecution() throws InterruptedException {
424             return awaitExecution(DEFAULT_TIMEOUT_MILLIS);
425         }
426 
awaitExecution(long timeoutMillis)427         public boolean awaitExecution(long timeoutMillis) throws InterruptedException {
428             final boolean executed = mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
429             if (getLastErrorMessage() != null) {
430                 Assert.fail(getLastErrorMessage());
431             }
432             return executed;
433         }
434 
435         /**
436          * Block the test thread, expecting to timeout but still listening to ensure that no jobs
437          * land in the interim.
438          * @return True if the latch timed out waiting on an execution.
439          */
awaitTimeout()440         public boolean awaitTimeout() throws InterruptedException {
441             return awaitTimeout(DEFAULT_TIMEOUT_MILLIS);
442         }
443 
awaitTimeout(long timeoutMillis)444         public boolean awaitTimeout(long timeoutMillis) throws InterruptedException {
445             return !mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
446         }
447 
awaitWaitingForStop()448         public boolean awaitWaitingForStop() throws InterruptedException {
449             return mWaitingForStopLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
450         }
451 
awaitDoWork()452         public boolean awaitDoWork() throws InterruptedException {
453             return mDoWorkLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
454         }
455 
awaitDoJob()456         public boolean awaitDoJob() throws InterruptedException {
457             if (mDoJobLatch == null) {
458                 return true;
459             }
460             return mDoJobLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
461         }
462 
awaitStopped()463         public boolean awaitStopped() throws InterruptedException {
464             return mStoppedLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
465         }
466 
notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite, ArrayList<JobWorkItem> receivedWork, String errorMsg)467         private void notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite,
468                 ArrayList<JobWorkItem> receivedWork, String errorMsg) {
469             //Log.d(TAG, "Job executed:" + params.getJobId());
470             mExecutedJobParameters = params;
471             mExecutedPermCheckRead = permCheckRead;
472             mExecutedPermCheckWrite = permCheckWrite;
473             mExecutedReceivedWork = receivedWork;
474             mExecutedErrorMessage = errorMsg;
475             if (mLatch != null) {
476                 mLatch.countDown();
477             }
478         }
479 
notifyWaitingForStop()480         private void notifyWaitingForStop() {
481             mWaitingForStopLatch.countDown();
482         }
483 
notifyStopped(JobParameters params)484         private void notifyStopped(JobParameters params) {
485             mStopJobParameters = params;
486             if (mStoppedLatch != null) {
487                 mStoppedLatch.countDown();
488             }
489         }
490 
setExpectedExecutions(int numExecutions)491         public void setExpectedExecutions(int numExecutions) {
492             // For no executions expected, set count to 1 so we can still block for the timeout.
493             if (numExecutions == 0) {
494                 mLatch = new CountDownLatch(1);
495             } else {
496                 mLatch = new CountDownLatch(numExecutions);
497             }
498             mWaitingForStopLatch = null;
499             mDoJobLatch = null;
500             mStoppedLatch = null;
501             mDoWorkLatch = null;
502             mExpectedWork = null;
503             mContinueAfterStart = false;
504         }
505 
setExpectedWaitForStop()506         public void setExpectedWaitForStop() {
507             mWaitingForStopLatch = new CountDownLatch(1);
508         }
509 
setExpectedWork(TestWorkItem[] work)510         public void setExpectedWork(TestWorkItem[] work) {
511             mExpectedWork = work;
512             mDoWorkLatch = new CountDownLatch(1);
513         }
514 
setExpectedStopped()515         public void setExpectedStopped() {
516             mStoppedLatch = new CountDownLatch(1);
517         }
518 
readyToWork()519         public void readyToWork() {
520             mDoWorkLatch.countDown();
521         }
522 
setExpectedWaitForRun()523         public void setExpectedWaitForRun() {
524             mDoJobLatch = new CountDownLatch(1);
525         }
526 
readyToRun()527         public void readyToRun() {
528             mDoJobLatch.countDown();
529         }
530 
setContinueAfterStart()531         public void setContinueAfterStart() {
532             mContinueAfterStart = true;
533         }
534 
handleContinueAfterStart()535         public boolean handleContinueAfterStart() {
536             boolean res = mContinueAfterStart;
537             mContinueAfterStart = false;
538             return res;
539         }
540 
541         /** Called in each testCase#setup */
setUp()542         public void setUp() {
543             mLatch = null;
544             mExecutedJobParameters = null;
545             mStopJobParameters = null;
546         }
547 
548     }
549 }