Merge "Reschedules alarms when BOOT_COMPLETED, TIMEZONE_CHANGED, and TIME_SET. Test: Updated unit tests. Fixes: b/73313567" into flatfoot-background
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
new file mode 100644
index 0000000..bf3e435
--- /dev/null
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.impl.background.systemjob;
+
+
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_ID;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.os.PersistableBundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.work.Work;
+import androidx.work.WorkManagerTest;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.worker.TestWorker;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
+public class SystemJobSchedulerTest extends WorkManagerTest {
+
+ private static final String TEST_ID = "test";
+
+ private JobScheduler mJobScheduler;
+ private SystemJobScheduler mSystemJobScheduler;
+
+ @Before
+ public void setUp() {
+ mJobScheduler = mock(JobScheduler.class);
+ doReturn(RESULT_SUCCESS).when(mJobScheduler).schedule(any(JobInfo.class));
+
+ List<JobInfo> allJobInfos = new ArrayList<>(2);
+ PersistableBundle extras = new PersistableBundle();
+ extras.putString(EXTRA_WORK_SPEC_ID, TEST_ID);
+ JobInfo mockJobInfo1 = mock(JobInfo.class);
+ doReturn(extras).when(mockJobInfo1).getExtras();
+ JobInfo mockJobInfo2 = mock(JobInfo.class);
+ doReturn(extras).when(mockJobInfo2).getExtras();
+
+ allJobInfos.add(mockJobInfo1);
+ allJobInfos.add(mockJobInfo2);
+ doReturn(allJobInfos).when(mJobScheduler).getAllPendingJobs();
+
+ mSystemJobScheduler =
+ spy(new SystemJobScheduler(mJobScheduler,
+ new SystemJobInfoConverter(InstrumentationRegistry.getTargetContext())));
+ doNothing().when(mSystemJobScheduler).scheduleInternal(any(WorkSpec.class));
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 23)
+ public void testSystemJobScheduler_schedulesTwiceOnApi23() {
+ Work work1 = new Work.Builder(TestWorker.class).build();
+ WorkSpec workSpec1 = getWorkSpec(work1);
+
+ Work work2 = new Work.Builder(TestWorker.class).build();
+ WorkSpec workSpec2 = getWorkSpec(work2);
+
+ mSystemJobScheduler.schedule(workSpec1, workSpec2);
+
+ verify(mSystemJobScheduler, times(2)).scheduleInternal(workSpec1);
+ verify(mSystemJobScheduler, times(2)).scheduleInternal(workSpec2);
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 24)
+ public void testSystemJobScheduler_schedulesOnceAtOrAboveApi24() {
+ Work work1 = new Work.Builder(TestWorker.class).build();
+ WorkSpec workSpec1 = getWorkSpec(work1);
+
+ Work work2 = new Work.Builder(TestWorker.class).build();
+ WorkSpec workSpec2 = getWorkSpec(work2);
+
+ mSystemJobScheduler.schedule(workSpec1, workSpec2);
+
+ verify(mSystemJobScheduler, times(1)).scheduleInternal(workSpec1);
+ verify(mSystemJobScheduler, times(1)).scheduleInternal(workSpec2);
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 23)
+ public void testSystemJobScheduler_cancelsAllOnApi23() {
+ mSystemJobScheduler.cancel(TEST_ID);
+ verify(mJobScheduler, times(2)).cancel(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 24)
+ public void testSystemJobScheduler_cancelsOnceAtOrAboveApi24() {
+ mSystemJobScheduler.cancel(TEST_ID);
+ verify(mJobScheduler, times(1)).cancel(anyInt());
+ }
+}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
index ae3e23b..ad8a43e 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
@@ -129,6 +129,17 @@
assertThat(mSystemJobService.onStopJob(mockParams), is(false));
}
+ @Test
+ @SmallTest
+ public void testStartJob_ReturnsFalseWithDuplicateJob() {
+ Work work = new Work.Builder(InfiniteTestWorker.class).build();
+ insertWork(work);
+
+ JobParameters mockParams = createMockJobParameters(work.getId());
+ assertThat(mSystemJobService.onStartJob(mockParams), is(true));
+ assertThat(mSystemJobService.onStartJob(mockParams), is(false));
+ }
+
private JobParameters createMockJobParameters(String id) {
JobParameters jobParameters = mock(JobParameters.class);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index 712ef2f..c4921d9 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -19,8 +19,10 @@
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.Context;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
import java.util.List;
@@ -44,19 +46,45 @@
private SystemJobInfoConverter mSystemJobInfoConverter;
public SystemJobScheduler(Context context) {
- mJobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- mSystemJobInfoConverter = new SystemJobInfoConverter(context);
+ this((JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE),
+ new SystemJobInfoConverter(context));
+ }
+
+ @VisibleForTesting
+ public SystemJobScheduler(
+ JobScheduler jobScheduler,
+ SystemJobInfoConverter systemJobInfoConverter) {
+ mJobScheduler = jobScheduler;
+ mSystemJobInfoConverter = systemJobInfoConverter;
}
@Override
public void schedule(WorkSpec... workSpecs) {
for (WorkSpec workSpec : workSpecs) {
- JobInfo jobInfo = mSystemJobInfoConverter.convert(workSpec);
- Logger.debug(TAG, "Scheduling work ID %s Job ID %s", workSpec.getId(), jobInfo.getId());
- mJobScheduler.schedule(jobInfo);
+ scheduleInternal(workSpec);
+
+ // API 23 JobScheduler only kicked off jobs if there were at least two jobs in the
+ // queue, even if the job constraints were met. This behavior was considered
+ // undesirable and later changed in Marshmallow MR1. To match the new behavior, we will
+ // double-schedule jobs on API 23 and dedupe them in SystemJobService as needed.
+ if (Build.VERSION.SDK_INT == 23) {
+ scheduleInternal(workSpec);
+ }
}
}
+ /**
+ * Schedules one job with JobScheduler.
+ *
+ * @param workSpec The {@link WorkSpec} to schedule with JobScheduler.
+ */
+ @VisibleForTesting
+ public void scheduleInternal(WorkSpec workSpec) {
+ JobInfo jobInfo = mSystemJobInfoConverter.convert(workSpec);
+ Logger.debug(TAG, "Scheduling work ID %s Job ID %s", workSpec.getId(), jobInfo.getId());
+ mJobScheduler.schedule(jobInfo);
+ }
+
@Override
public void cancel(@NonNull String workSpecId) {
// Note: despite what the word "pending" and the associated Javadoc might imply, this is
@@ -67,7 +95,11 @@
if (workSpecId.equals(
jobInfo.getExtras().getString(SystemJobInfoConverter.EXTRA_WORK_SPEC_ID))) {
mJobScheduler.cancel(jobInfo.getId());
- return;
+
+ // See comment in #schedule.
+ if (Build.VERSION.SDK_INT != 23) {
+ return;
+ }
}
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
index 34a2cbe..2752db0 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
@@ -65,18 +65,27 @@
return false;
}
- boolean isPeriodic = extras.getBoolean(SystemJobInfoConverter.EXTRA_IS_PERIODIC, false);
- if (isPeriodic && params.isOverrideDeadlineExpired()) {
- Logger.debug(TAG, "Override deadline expired for id %s. Retry requested", workSpecId);
- jobFinished(params, true);
- return false;
- }
-
- Logger.debug(TAG, "onStartJob for %s", workSpecId);
-
synchronized (mJobParameters) {
+ if (mJobParameters.containsKey(workSpecId)) {
+ // This condition may happen due to our workaround for an undesired behavior in API
+ // 23. See the documentation in {@link SystemJobScheduler#schedule}.
+ Logger.debug(TAG,
+ "Job is already being executed by SystemJobService: %s", workSpecId);
+ return false;
+ }
+
+ boolean isPeriodic = extras.getBoolean(SystemJobInfoConverter.EXTRA_IS_PERIODIC, false);
+ if (isPeriodic && params.isOverrideDeadlineExpired()) {
+ Logger.debug(TAG,
+ "Override deadline expired for id %s. Retry requested", workSpecId);
+ jobFinished(params, true);
+ return false;
+ }
+
+ Logger.debug(TAG, "onStartJob for %s", workSpecId);
mJobParameters.put(workSpecId, params);
}
+
mWorkManagerImpl.startWork(workSpecId);
return true;
}