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