1 /*
2  * Copyright (C) 2017 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 androidx.core.app;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.fail;
21 
22 import android.annotation.SuppressLint;
23 import android.app.job.JobScheduler;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.util.Log;
32 
33 import androidx.test.core.app.ApplicationProvider;
34 import androidx.test.ext.junit.runners.AndroidJUnit4;
35 import androidx.test.filters.MediumTest;
36 import androidx.test.filters.SdkSuppress;
37 
38 import org.jspecify.annotations.NonNull;
39 import org.junit.Before;
40 import org.junit.Ignore;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.ArrayList;
45 import java.util.concurrent.CountDownLatch;
46 import java.util.concurrent.TimeUnit;
47 
48 @SuppressWarnings("deprecation")
49 @RunWith(AndroidJUnit4.class)
50 public class JobIntentServiceTest {
51     static final String TAG = "JobIntentServiceTest";
52 
53     static final int JOB_ID = 0x1000;
54 
55     static final Object sLock = new Object();
56     static CountDownLatch sReadyToRunLatch;
57     static CountDownLatch sServiceWaitingLatch;
58     static CountDownLatch sServiceStoppedLatch;
59     static CountDownLatch sWaitCompleteLatch;
60     static CountDownLatch sServiceFinishedLatch;
61 
62     static boolean sFinished;
63     static ArrayList<Intent> sFinishedWork;
64     static String sFinishedErrorMsg;
65     static String sLastServiceState;
66 
67     Context mContext;
68 
69     @Before
setup()70     public void setup() {
71         mContext = ApplicationProvider.getApplicationContext();
72     }
73 
74     @SuppressLint("BanParcelableUsage")
75     public static final class TestIntentItem implements Parcelable {
76         public static final int FLAG_WAIT = 1 << 0;
77         public static final int FLAG_STOPPED_AFTER_WAIT = 1 << 1;
78 
79         public final Intent intent;
80         public final TestIntentItem[] subitems;
81         public final int flags;
82         public final Uri[] requireUrisGranted;
83         public final Uri[] requireUrisNotGranted;
84 
TestIntentItem(Intent intent)85         public TestIntentItem(Intent intent) {
86             this.intent = intent;
87             subitems = null;
88             flags = 0;
89             requireUrisGranted = null;
90             requireUrisNotGranted = null;
91         }
92 
TestIntentItem(Intent intent, int flags)93         public TestIntentItem(Intent intent, int flags) {
94             this.intent = intent;
95             subitems = null;
96             this.flags = flags;
97             intent.putExtra("flags", flags);
98             requireUrisGranted = null;
99             requireUrisNotGranted = null;
100         }
101 
TestIntentItem(Intent intent, TestIntentItem[] subitems)102         public TestIntentItem(Intent intent, TestIntentItem[] subitems) {
103             this.intent = intent;
104             this.subitems = subitems;
105             intent.putExtra("subitems", subitems);
106             flags = 0;
107             requireUrisGranted = null;
108             requireUrisNotGranted = null;
109         }
110 
TestIntentItem(Intent intent, Uri[] requireUrisGranted, Uri[] requireUrisNotGranted)111         public TestIntentItem(Intent intent, Uri[] requireUrisGranted,
112                 Uri[] requireUrisNotGranted) {
113             this.intent = intent;
114             subitems = null;
115             flags = 0;
116             this.requireUrisGranted = requireUrisGranted;
117             this.requireUrisNotGranted = requireUrisNotGranted;
118         }
119 
120         @Override
toString()121         public String toString() {
122             StringBuilder sb = new StringBuilder(64);
123             sb.append("TestIntentItem { ");
124             sb.append(intent);
125             sb.append(" }");
126             return sb.toString();
127         }
128 
129         @Override
describeContents()130         public int describeContents() {
131             return 0;
132         }
133 
134         @Override
writeToParcel(Parcel parcel, int flags)135         public void writeToParcel(Parcel parcel, int flags) {
136             intent.writeToParcel(parcel, flags);
137             parcel.writeTypedArray(subitems, flags);
138             parcel.writeInt(flags);
139         }
140 
TestIntentItem(Parcel parcel)141         TestIntentItem(Parcel parcel) {
142             intent = Intent.CREATOR.createFromParcel(parcel);
143             subitems = parcel.createTypedArray(CREATOR);
144             flags = parcel.readInt();
145             requireUrisGranted = null;
146             requireUrisNotGranted = null;
147         }
148 
149         public static final Parcelable.Creator<TestIntentItem> CREATOR =
150                 new Parcelable.Creator<TestIntentItem>() {
151 
152                     public TestIntentItem createFromParcel(Parcel source) {
153                         return new TestIntentItem(source);
154                     }
155 
156                     public TestIntentItem[] newArray(int size) {
157                         return new TestIntentItem[size];
158                     }
159                 };
160     }
161 
initStatics()162     static void initStatics() {
163         synchronized (sLock) {
164             sReadyToRunLatch = new CountDownLatch(1);
165             sServiceWaitingLatch = new CountDownLatch(1);
166             sServiceStoppedLatch = new CountDownLatch(1);
167             sWaitCompleteLatch = new CountDownLatch(1);
168             sServiceFinishedLatch = new CountDownLatch(1);
169             sFinished = false;
170             sFinishedWork = null;
171             sFinishedErrorMsg = null;
172         }
173     }
174 
allowServiceToRun()175     static void allowServiceToRun() {
176         sReadyToRunLatch.countDown();
177     }
178 
serviceReportWaiting()179     static void serviceReportWaiting() {
180         sServiceWaitingLatch.countDown();
181     }
182 
ensureServiceWaiting()183     static void ensureServiceWaiting() {
184         try {
185             if (!sServiceWaitingLatch.await(10, TimeUnit.SECONDS)) {
186                 fail("Timed out waiting for wait, service state " + sLastServiceState);
187             }
188         } catch (InterruptedException e) {
189             fail("Interrupted waiting for service to wait: " + e);
190         }
191     }
192 
serviceReportStopped()193     static void serviceReportStopped() {
194         sServiceStoppedLatch.countDown();
195     }
196 
ensureServiceStopped()197     static void ensureServiceStopped() {
198         try {
199             if (!sServiceStoppedLatch.await(10, TimeUnit.SECONDS)) {
200                 fail("Timed out waiting for stop, service state " + sLastServiceState);
201             }
202         } catch (InterruptedException e) {
203             fail("Interrupted waiting for service to stop: " + e);
204         }
205     }
206 
allowServiceToResumeFromWait()207     static void allowServiceToResumeFromWait() {
208         sWaitCompleteLatch.countDown();
209     }
210 
finishServiceExecution(ArrayList<Intent> work, String errorMsg)211     static void finishServiceExecution(ArrayList<Intent> work, String errorMsg) {
212         synchronized (sLock) {
213             if (!sFinished) {
214                 sFinishedWork = work;
215                 sFinishedErrorMsg = errorMsg;
216                 sServiceFinishedLatch.countDown();
217             }
218         }
219     }
220 
updateServiceState(String msg)221     static void updateServiceState(String msg) {
222         synchronized (sLock) {
223             sLastServiceState = msg;
224         }
225     }
226 
waitServiceFinish()227     void waitServiceFinish() {
228         try {
229             if (!sServiceFinishedLatch.await(10, TimeUnit.SECONDS)) {
230                 synchronized (sLock) {
231                     if (sFinishedErrorMsg != null) {
232                         fail("Timed out waiting for finish, service state " + sLastServiceState
233                                 + ", had error: " + sFinishedErrorMsg);
234                     }
235                     fail("Timed out waiting for finish, service state " + sLastServiceState);
236                 }
237             }
238         } catch (InterruptedException e) {
239             fail("Interrupted waiting for service to finish: " + e);
240         }
241         synchronized (sLock) {
242             if (sFinishedErrorMsg != null) {
243                 fail(sFinishedErrorMsg);
244             }
245         }
246     }
247 
248     public static class TargetService extends JobIntentService {
249         final ArrayList<Intent> mReceivedWork = new ArrayList<>();
250 
251         @Override
onCreate()252         public void onCreate() {
253             super.onCreate();
254             updateServiceState("Creating: " + this);
255             Log.i(TAG, "Creating: " + this);
256             Log.i(TAG, "Waiting for ready to run...");
257             try {
258                 if (!sReadyToRunLatch.await(10, TimeUnit.SECONDS)) {
259                     finishServiceExecution(null, "Timeout waiting for ready");
260                 }
261             } catch (InterruptedException e) {
262                 finishServiceExecution(null, "Interrupted waiting for ready: " + e);
263             }
264             updateServiceState("Past ready to run");
265             Log.i(TAG, "Running!");
266         }
267 
268         @SuppressWarnings("deprecation")
269         @Override
onHandleWork(@onNull Intent intent)270         protected void onHandleWork(@NonNull Intent intent) {
271             Log.i(TAG, "Handling work: " + intent);
272             updateServiceState("Handling work: " + intent);
273             mReceivedWork.add(intent);
274             intent.setExtrasClassLoader(TestIntentItem.class.getClassLoader());
275             int flags = intent.getIntExtra("flags", 0);
276             if ((flags & TestIntentItem.FLAG_WAIT) != 0) {
277                 serviceReportWaiting();
278                 try {
279                     if (!sWaitCompleteLatch.await(10, TimeUnit.SECONDS)) {
280                         finishServiceExecution(null, "Timeout waiting for wait complete");
281                     }
282                 } catch (InterruptedException e) {
283                     finishServiceExecution(null, "Interrupted waiting for wait complete: " + e);
284                 }
285                 if ((flags & TestIntentItem.FLAG_STOPPED_AFTER_WAIT) != 0) {
286                     if (!isStopped()) {
287                         finishServiceExecution(null, "Service not stopped after waiting");
288                     }
289                 }
290             }
291             Parcelable[] subitems = intent.getParcelableArrayExtra("subitems");
292             if (subitems != null) {
293                 for (Parcelable pitem : subitems) {
294                     JobIntentService.enqueueWork(this, TargetService.class,
295                             JOB_ID, ((TestIntentItem) pitem).intent);
296                 }
297             }
298         }
299 
300         @Override
onStopCurrentWork()301         public boolean onStopCurrentWork() {
302             serviceReportStopped();
303             return super.onStopCurrentWork();
304         }
305 
306         @Override
onDestroy()307         public void onDestroy() {
308             Log.i(TAG, "Destroying: " + this);
309             updateServiceState("Destroying: " + this);
310             finishServiceExecution(mReceivedWork, null);
311             super.onDestroy();
312         }
313     }
314 
intentEquals(Intent i1, Intent i2)315     private boolean intentEquals(Intent i1, Intent i2) {
316         if (i1 == i2) {
317             return true;
318         }
319         if (i1 == null || i2 == null) {
320             return false;
321         }
322         return i1.filterEquals(i2);
323     }
324 
compareIntents(TestIntentItem[] expected, ArrayList<Intent> received)325     private void compareIntents(TestIntentItem[] expected, ArrayList<Intent> received) {
326         if (received == null) {
327             fail("Didn't receive any expected work.");
328         }
329         ArrayList<TestIntentItem> expectedArray = new ArrayList<>();
330         for (int i = 0; i < expected.length; i++) {
331             expectedArray.add(expected[i]);
332         }
333 
334         ComponentName serviceComp = new ComponentName(mContext, TargetService.class.getName());
335 
336         for (int i = 0; i < received.size(); i++) {
337             Intent r = received.get(i);
338             if (i < expected.length && expected[i].subitems != null) {
339                 TestIntentItem[] sub = expected[i].subitems;
340                 for (int j = 0; j < sub.length; j++) {
341                     expectedArray.add(sub[j]);
342                 }
343             }
344             if (i >= expectedArray.size()) {
345                 fail("Received more than " + expected.length + " work items, first extra is "
346                         + r);
347             }
348             if (r.getComponent() != null) {
349                 // Intents we get back from the compat service will have a component... make
350                 // sure that is correct, and then erase it so the intentEquals() will pass.
351                 assertEquals(serviceComp, r.getComponent());
352                 r.setComponent(null);
353             }
354             if (!intentEquals(r, expectedArray.get(i).intent)) {
355                 fail("Received intent #" + i + " " + r + " but expected " + expected[i]);
356             }
357         }
358         if (received.size() < expected.length) {
359             fail("Received only " + received.size() + " work items, but expected "
360                     + expected.length);
361         }
362     }
363 
364     /**
365      * Test simple case of enqueueing one piece of work.
366      */
367     @MediumTest
368     @Test
369     @Ignore("JobIntentService is deprecated and no longer maintained")
testEnqueueOne()370     public void testEnqueueOne() throws Throwable {
371         initStatics();
372 
373         TestIntentItem[] items = new TestIntentItem[] {
374                 new TestIntentItem(new Intent("FIRST")),
375         };
376 
377         for (TestIntentItem item : items) {
378             JobIntentService.enqueueWork(mContext, TargetService.class, JOB_ID, item.intent);
379         }
380         allowServiceToRun();
381 
382         waitServiceFinish();
383         compareIntents(items, sFinishedWork);
384     }
385 
386     /**
387      * Test case of enqueueing multiple pieces of work.
388      */
389     @MediumTest
390     @Test
391     @Ignore("JobIntentService is deprecated and no longer maintained")
testEnqueueMultiple()392     public void testEnqueueMultiple() throws Throwable {
393         initStatics();
394 
395         TestIntentItem[] items = new TestIntentItem[] {
396                 new TestIntentItem(new Intent("FIRST")),
397                 new TestIntentItem(new Intent("SECOND")),
398                 new TestIntentItem(new Intent("THIRD")),
399                 new TestIntentItem(new Intent("FOURTH")),
400         };
401 
402         for (TestIntentItem item : items) {
403             JobIntentService.enqueueWork(mContext, TargetService.class, JOB_ID, item.intent);
404         }
405         allowServiceToRun();
406 
407         waitServiceFinish();
408         compareIntents(items, sFinishedWork);
409     }
410 
411     /**
412      * Test case of enqueueing multiple pieces of work.
413      */
414     @MediumTest
415     @Test
416     @Ignore("JobIntentService is deprecated and no longer maintained")
testEnqueueSubWork()417     public void testEnqueueSubWork() throws Throwable {
418         initStatics();
419 
420         TestIntentItem[] items = new TestIntentItem[] {
421                 new TestIntentItem(new Intent("FIRST")),
422                 new TestIntentItem(new Intent("SECOND")),
423                 new TestIntentItem(new Intent("THIRD"), new TestIntentItem[] {
424                         new TestIntentItem(new Intent("FIFTH")),
425                         new TestIntentItem(new Intent("SIXTH")),
426                         new TestIntentItem(new Intent("SEVENTH")),
427                         new TestIntentItem(new Intent("EIGTH")),
428                 }),
429                 new TestIntentItem(new Intent("FOURTH")),
430         };
431 
432         for (TestIntentItem item : items) {
433             JobIntentService.enqueueWork(mContext, TargetService.class, JOB_ID, item.intent);
434         }
435         allowServiceToRun();
436 
437         waitServiceFinish();
438         compareIntents(items, sFinishedWork);
439     }
440 
441     /**
442      * Test case of job stopping while it is doing work.
443      */
444     @MediumTest
445     @Test
446     @Ignore("JobIntentService is deprecated and no longer maintained")
447     @SdkSuppress(minSdkVersion = 26)
testStopWhileWorking()448     public void testStopWhileWorking() throws Throwable {
449         if (Build.VERSION.SDK_INT < 26) {
450             // This test only makes sense when running on top of JobScheduler.
451             return;
452         }
453 
454         initStatics();
455 
456         TestIntentItem[] items = new TestIntentItem[] {
457                 new TestIntentItem(new Intent("FIRST"),
458                         TestIntentItem.FLAG_WAIT | TestIntentItem.FLAG_STOPPED_AFTER_WAIT),
459         };
460 
461         for (TestIntentItem item : items) {
462             JobIntentService.enqueueWork(mContext, TargetService.class, JOB_ID, item.intent);
463         }
464         allowServiceToRun();
465         ensureServiceWaiting();
466 
467         // At this point we will make the job stop...  this isn't normally how this would
468         // happen with an IntentJobService, and doing it this way breaks re-delivery of
469         // work, but we have CTS tests for the underlying redlivery mechanism.
470         ((JobScheduler) mContext.getApplicationContext().getSystemService(
471                 Context.JOB_SCHEDULER_SERVICE)).cancel(JOB_ID);
472         ensureServiceStopped();
473 
474         allowServiceToResumeFromWait();
475 
476         waitServiceFinish();
477         compareIntents(items, sFinishedWork);
478     }
479 }
480