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;
     }