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