Merge "Use special WM's namespace for scheduling jobs on API 34+" into androidx-main
diff --git a/work/work-runtime/src/androidTest/AndroidManifest.xml b/work/work-runtime/src/androidTest/AndroidManifest.xml
index 3f130ae..95dec8b 100644
--- a/work/work-runtime/src/androidTest/AndroidManifest.xml
+++ b/work/work-runtime/src/androidTest/AndroidManifest.xml
@@ -29,5 +29,12 @@
     <application android:name="androidx.multidex.MultiDexApplication">
         <service android:name="androidx.work.impl.foreground.SystemForegroundService"
             android:foregroundServiceType="specialUse" />
+        <service
+            android:name="androidx.work.impl.background.systemjob.WhateverJobService"
+            android:process=".foo"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:exported="true"
+            android:enabled="true"
+            android:directBootAware="false"/>
     </application>
 </manifest>
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkManagerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkManagerTest.java
index 55d05f7..9ba2bb2 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkManagerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkManagerTest.java
@@ -33,7 +33,11 @@
         if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
             JobScheduler jobScheduler = (JobScheduler) ApplicationProvider.getApplicationContext()
                     .getSystemService(Context.JOB_SCHEDULER_SERVICE);
-            jobScheduler.cancelAll();
+            if (Build.VERSION.SDK_INT < 34) {
+                jobScheduler.cancelAll();
+            } else {
+                jobScheduler.cancelInAllNamespaces();
+            }
         }
     }
 }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/JobSchedulerNamespaceTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/JobSchedulerNamespaceTest.kt
new file mode 100644
index 0000000..1dec970
--- /dev/null
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/JobSchedulerNamespaceTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 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 android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.app.job.JobScheduler.RESULT_SUCCESS
+import android.app.job.JobService
+import android.content.ComponentName
+import android.content.Context.JOB_SCHEDULER_SERVICE
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.work.Configuration
+import androidx.work.OneTimeWorkRequest
+import androidx.work.testutils.TestEnv
+import androidx.work.worker.TestWorker
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RequiresApi(34)
+@RunWith(AndroidJUnit4::class)
+class JobSchedulerNamespaceTest {
+    private val env = TestEnv(Configuration.Builder().build())
+    private val systemJobScheduler = with(env) { SystemJobScheduler(context, db, configuration) }
+    private val globalJobScheduler = env.context
+        .getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler
+
+    @Before
+    fun setup() {
+        // making sure we start at the clean state
+        globalJobScheduler.cancelInAllNamespaces()
+        assertThat(globalJobScheduler.pendingJobsInAllNamespaces.size).isEqualTo(0)
+    }
+
+    @SdkSuppress(minSdkVersion = 34)
+    @MediumTest
+    @Test
+    fun checkNamespaces() {
+        val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setInitialDelay(365, TimeUnit.DAYS)
+            .build()
+        env.db.workSpecDao().insertWorkSpec(request.workSpec)
+        systemJobScheduler.schedule(request.workSpec)
+        val pendingJobs = globalJobScheduler.pendingJobsInAllNamespaces
+        assertThat(pendingJobs[null]).isNull()
+        val workManagerJobs = pendingJobs[WORKMANAGER_NAMESPACE]
+        assertThat(workManagerJobs).isNotNull()
+        assertThat(workManagerJobs).hasSize(1)
+    }
+
+    @SdkSuppress(minSdkVersion = 34)
+    @MediumTest
+    @Test
+    fun cancelAllInAllNamespaces() {
+        val componentName = ComponentName(env.context, WhateverJobService::class.java)
+        val jobInfo = JobInfo.Builder(10, componentName)
+            .setMinimumLatency(TimeUnit.DAYS.toMillis(365))
+            .build()
+        assertThat(globalJobScheduler.schedule(jobInfo)).isEqualTo(RESULT_SUCCESS)
+
+        val workManagerComponent = ComponentName(env.context, SystemJobService::class.java)
+        val oldWorkManagerInfo = JobInfo.Builder(12, workManagerComponent)
+            .setMinimumLatency(TimeUnit.DAYS.toMillis(365))
+            .build()
+        assertThat(globalJobScheduler.schedule(oldWorkManagerInfo)).isEqualTo(RESULT_SUCCESS)
+
+        val request = prepareWorkSpec()
+        systemJobScheduler.schedule(request.workSpec)
+        val pendingJobsPreCancellation = globalJobScheduler.pendingJobsInAllNamespaces
+        assertThat(pendingJobsPreCancellation).hasSize(2)
+        assertThat(pendingJobsPreCancellation[null]).hasSize(2)
+        SystemJobScheduler.cancelAllInAllNamespaces(env.context)
+        val pendingJobsPostCancellation = globalJobScheduler.pendingJobsInAllNamespaces
+        assertThat(pendingJobsPostCancellation).hasSize(1)
+        assertThat(pendingJobsPostCancellation[null]).hasSize(1)
+    }
+
+    @SdkSuppress(minSdkVersion = 34)
+    @MediumTest
+    @Test
+    fun reconcileTest() {
+        val request = prepareWorkSpec()
+        systemJobScheduler.schedule(request.workSpec)
+        val workManagerComponent = ComponentName(env.context, SystemJobService::class.java)
+        val unknownWorkInfo = JobInfo.Builder(4000, workManagerComponent)
+            .setMinimumLatency(TimeUnit.DAYS.toMillis(365))
+            .build()
+        env.context.wmJobScheduler.schedule(unknownWorkInfo)
+        globalJobScheduler.schedule(unknownWorkInfo)
+        val preReconcile = globalJobScheduler.pendingJobsInAllNamespaces
+        assertThat(preReconcile[WORKMANAGER_NAMESPACE]).hasSize(2)
+        assertThat(preReconcile[null]).hasSize(1)
+        SystemJobScheduler.reconcileJobs(env.context, env.db)
+        val postReconcile = globalJobScheduler.pendingJobsInAllNamespaces
+        assertThat(postReconcile).hasSize(2)
+        assertThat(postReconcile[WORKMANAGER_NAMESPACE]).hasSize(1)
+        assertThat(preReconcile[null]).hasSize(1)
+    }
+
+    @After
+    fun tearDown() {
+        globalJobScheduler.cancelInAllNamespaces()
+    }
+
+    private fun prepareWorkSpec(): OneTimeWorkRequest {
+        val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setInitialDelay(365, TimeUnit.DAYS)
+            .build()
+        env.db.workSpecDao().insertWorkSpec(request.workSpec)
+        return request
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class WhateverJobService : JobService() {
+    override fun onStartJob(params: JobParameters?): Boolean {
+        throw UnsupportedOperationException()
+    }
+
+    override fun onStopJob(params: JobParameters?): Boolean {
+        throw UnsupportedOperationException()
+    }
+}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
index 164b411..3715173 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -254,7 +254,8 @@
 
     @Test
     @LargeTest
-    @SdkSuppress(minSdkVersion = 23)
+    // JobSchedulerNamespaceTest covers maxSdkVersion 34+
+    @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 33)
     public void testSystemJobScheduler_cancelsInvalidJobs() {
         List<JobInfo> allJobInfos = new ArrayList<>(2);
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
index 85de971..75754f0 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
@@ -30,6 +30,8 @@
 import androidx.work.impl.WorkDatabaseVersions.VERSION_10
 import androidx.work.impl.WorkDatabaseVersions.VERSION_11
 import androidx.work.impl.WorkDatabaseVersions.VERSION_2
+import androidx.work.impl.WorkDatabaseVersions.VERSION_21
+import androidx.work.impl.WorkDatabaseVersions.VERSION_22
 import androidx.work.impl.WorkDatabaseVersions.VERSION_3
 import androidx.work.impl.WorkDatabaseVersions.VERSION_5
 import androidx.work.impl.WorkDatabaseVersions.VERSION_6
@@ -70,7 +72,7 @@
         AutoMigration(from = 19, to = 20, spec = AutoMigration_19_20::class),
         AutoMigration(from = 20, to = 21),
     ],
-    version = 21
+    version = 22
 )
 @TypeConverters(value = [Data::class, WorkTypeConverters::class])
 abstract class WorkDatabase : RoomDatabase() {
@@ -164,6 +166,7 @@
                 .addMigrations(Migration_12_13)
                 .addMigrations(Migration_15_16)
                 .addMigrations(Migration_16_17)
+                .addMigrations(RescheduleMigration(context, VERSION_21, VERSION_22))
                 .fallbackToDestructiveMigration()
                 .build()
         }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
index 45e76c6..098b4a5 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
@@ -85,6 +85,9 @@
     const val VERSION_20 = 20
     // added NetworkRequest to Constraints
     const val VERSION_21 = 21
+    // need to reschedule jobs in workmanager's namespace,
+    // but no actual schema changes.
+    const val VERSION_22 = 22
 }
 
 private const val CREATE_SYSTEM_ID_INFO =
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index b2d4e6c..27248a9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -632,7 +632,7 @@
     public void rescheduleEligibleWork() {
         // TODO (rahulrav@) Make every scheduler do its own cancelAll().
         if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
-            SystemJobScheduler.cancelAll(getApplicationContext());
+            SystemJobScheduler.cancelAllInAllNamespaces(getApplicationContext());
         }
 
         // Reset scheduled state.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/JobSchedulerExt.kt b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/JobSchedulerExt.kt
new file mode 100644
index 0000000..95c4c4d
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/JobSchedulerExt.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 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 android.app.job.JobInfo
+import android.app.job.JobScheduler
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.work.Configuration
+import androidx.work.Logger
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.background.systemjob.SystemJobScheduler.getPendingJobs
+import androidx.work.loge
+
+internal const val WORKMANAGER_NAMESPACE = "androidx.work.systemjobscheduler"
+
+// using SystemJobScheduler as tag for simplicity, because everything here is about
+// SystemJobScheduler
+private val TAG = Logger.tagWithPrefix("SystemJobScheduler")
+
+@get:RequiresApi(21)
+internal val Context.wmJobScheduler: JobScheduler
+    get() {
+        val defaultJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+        return if (Build.VERSION.SDK_INT >= 34) {
+            JobScheduler34.forNamespace(defaultJobScheduler)
+        } else defaultJobScheduler
+    }
+
+@RequiresApi(34)
+private object JobScheduler34 {
+    fun forNamespace(jobScheduler: JobScheduler): JobScheduler {
+        return jobScheduler.forNamespace(WORKMANAGER_NAMESPACE)
+    }
+}
+
+@RequiresApi(21)
+private object JobScheduler21 {
+    fun getAllPendingJobs(jobScheduler: JobScheduler): List<JobInfo> {
+        return jobScheduler.allPendingJobs
+    }
+}
+
+@get:RequiresApi(21)
+val JobScheduler.safePendingJobs: List<JobInfo>?
+    get() {
+        return try {
+            // Note: despite what the word "pending" and the associated Javadoc might imply, this is
+            // actually a list of all unfinished jobs that JobScheduler knows about for the current
+            // process.
+            JobScheduler21.getAllPendingJobs(this)
+        } catch (exception: Throwable) {
+            // OEM implementation bugs in JobScheduler cause the app to crash. Avoid crashing.
+            // see b/134028937
+            loge(TAG, exception) { "getAllPendingJobs() is not reliable on this device." }
+            null
+        }
+    }
+
+@RequiresApi(23)
+internal fun createErrorMessage(
+    context: Context,
+    workDatabase: WorkDatabase,
+    configuration: Configuration,
+): String {
+    val totalLimit = if (Build.VERSION.SDK_INT >= 31) 150 else 100
+    val dbScheduledCount = workDatabase.workSpecDao().getScheduledWork().size
+    val jobSchedulerStats = if (Build.VERSION.SDK_INT >= 34) {
+        val namespacedScheduler = context.wmJobScheduler
+        val allJobsInNamespace = namespacedScheduler.safePendingJobs
+        if (allJobsInNamespace != null) {
+            val pendingJobs = getPendingJobs(context, namespacedScheduler)
+            val diff = if (pendingJobs != null) allJobsInNamespace.size - pendingJobs.size else 0
+
+            val nonWmJobsMessage = when (diff) {
+                0 -> null
+                else -> "$diff of which are not owned by WorkManager"
+            }
+
+            val defaultJobScheduler =
+                context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+            val wmJobsInDefault = getPendingJobs(context, defaultJobScheduler)?.size ?: 0
+
+            val defaultNamespaceMessage = when (wmJobsInDefault) {
+                0 -> null
+                else -> "$wmJobsInDefault from WorkManager in the default namespace"
+            }
+
+            listOfNotNull(
+                "${allJobsInNamespace.size} jobs in \"$WORKMANAGER_NAMESPACE\" namespace",
+                nonWmJobsMessage,
+                defaultNamespaceMessage
+            ).joinToString(",\n")
+        } else "<faulty JobScheduler failed to getPendingJobs>"
+    } else {
+        when (val pendingJobs = getPendingJobs(context, context.wmJobScheduler)) {
+            null -> "<faulty JobScheduler failed to getPendingJobs>"
+            else -> "${pendingJobs.size} jobs from WorkManager"
+        }
+    }
+
+    return "JobScheduler $totalLimit job limit exceeded.\n" +
+        "In JobScheduler there are $jobSchedulerStats.\n" +
+        "There are $dbScheduledCount jobs tracked by WorkManager's database;\n" +
+        "the Configuration limit is ${configuration.maxSchedulerLimit}."
+}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index fc679669..b186c71 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -18,6 +18,10 @@
 import static android.content.Context.JOB_SCHEDULER_SERVICE;
 
 import static androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+import static androidx.work.impl.WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL;
+import static androidx.work.impl.background.systemjob.JobSchedulerExtKt.createErrorMessage;
+import static androidx.work.impl.background.systemjob.JobSchedulerExtKt.getSafePendingJobs;
+import static androidx.work.impl.background.systemjob.JobSchedulerExtKt.getWmJobScheduler;
 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_GENERATION;
 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_ID;
 import static androidx.work.impl.model.SystemIdInfoKt.systemIdInfo;
@@ -42,7 +46,6 @@
 import androidx.work.WorkInfo;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkDatabase;
-import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.SystemIdInfo;
 import androidx.work.impl.model.WorkGenerationalId;
 import androidx.work.impl.model.WorkSpec;
@@ -57,10 +60,9 @@
 
 /**
  * A class that schedules work using {@link android.app.job.JobScheduler}.
- *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
+@RequiresApi(MIN_JOB_SCHEDULER_API_LEVEL)
 public class SystemJobScheduler implements Scheduler {
 
     private static final String TAG = Logger.tagWithPrefix("SystemJobScheduler");
@@ -77,7 +79,7 @@
         this(context,
                 workDatabase,
                 configuration,
-                (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE),
+                getWmJobScheduler(context),
                 new SystemJobInfoConverter(context, configuration.getClock())
         );
     }
@@ -207,17 +209,7 @@
         } catch (IllegalStateException e) {
             // This only gets thrown if we exceed 100 jobs.  Let's figure out if WorkManager is
             // responsible for all these jobs.
-            List<JobInfo> jobs = getPendingJobs(mContext, mJobScheduler);
-            int numWorkManagerJobs = jobs != null ? jobs.size() : 0;
-
-            String message = String.format(Locale.getDefault(),
-                    "JobScheduler 100 job limit exceeded.  We count %d WorkManager "
-                            + "jobs in JobScheduler; we have %d tracked jobs in our DB; "
-                            + "our Configuration limit is %d.",
-                    numWorkManagerJobs,
-                    mWorkDatabase.workSpecDao().getScheduledWork().size(),
-                    mConfiguration.getMaxSchedulerLimit());
-
+            String message = createErrorMessage(mContext, mWorkDatabase, mConfiguration);
             Logger.get().error(TAG, message);
 
             IllegalStateException schedulingException = new IllegalStateException(message, e);
@@ -274,14 +266,22 @@
      *
      * @param context The {@link Context} for the {@link JobScheduler}
      */
-    public static void cancelAll(@NonNull Context context) {
+    public static void cancelAllInAllNamespaces(@NonNull Context context) {
+        // on API 34+ at first we cancel everything in our own namespace.
+        if (Build.VERSION.SDK_INT >= 34) {
+            JobScheduler namespacedScheduler = getWmJobScheduler(context);
+            namespacedScheduler.cancelAll();
+        }
+
+        // on API 34+ we still cancel our jobs in the default namespace, because
+        // there can be jobs scheduled by older version of library in the default namespace.
+        // On the previous APIs there is no namespaces, so we cancel our jobs in the only one
+        // global JobScheduler.
         JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
-        if (jobScheduler != null) {
-            List<JobInfo> jobs = getPendingJobs(context, jobScheduler);
-            if (jobs != null && !jobs.isEmpty()) {
-                for (JobInfo jobInfo : jobs) {
-                    cancelJobById(jobScheduler, jobInfo.getId());
-                }
+        List<JobInfo> jobs = getPendingJobs(context, jobScheduler);
+        if (jobs != null && !jobs.isEmpty()) {
+            for (JobInfo jobInfo : jobs) {
+                cancelJobById(jobScheduler, jobInfo.getId());
             }
         }
     }
@@ -297,14 +297,16 @@
      * expected {@link WorkSpec}s, reset the {@code scheduleRequestedAt} bit, so that jobs can be
      * rescheduled.
      *
-     * @param context     The application {@link Context}
+     * @param context      The application {@link Context}
      * @param workDatabase The {@link WorkDatabase} instance
      * @return <code>true</code> if jobs need to be reconciled.
      */
     public static boolean reconcileJobs(
             @NonNull Context context,
             @NonNull WorkDatabase workDatabase) {
-        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
+        // reconcile only in the namespaced jobscheduler.
+        // all the work is explicitly migrated to namespace on API 34+.
+        JobScheduler jobScheduler = getWmJobScheduler(context);
         List<JobInfo> jobs = getPendingJobs(context, jobScheduler);
         List<String> workManagerWorkSpecs =
                 workDatabase.systemIdInfoDao().getWorkSpecIds();
@@ -355,21 +357,10 @@
     }
 
     @Nullable
-    private static List<JobInfo> getPendingJobs(
+    static List<JobInfo> getPendingJobs(
             @NonNull Context context,
             @NonNull JobScheduler jobScheduler) {
-        List<JobInfo> pendingJobs = null;
-        try {
-            // Note: despite what the word "pending" and the associated Javadoc might imply, this is
-            // actually a list of all unfinished jobs that JobScheduler knows about for the current
-            // process.
-            pendingJobs = jobScheduler.getAllPendingJobs();
-        } catch (Throwable exception) {
-            // OEM implementation bugs in JobScheduler cause the app to crash. Avoid crashing.
-            Logger.get().error(TAG, "getAllPendingJobs() is not reliable on this device.",
-                    exception);
-        }
-
+        List<JobInfo> pendingJobs = getSafePendingJobs(jobScheduler);
         if (pendingJobs == null) {
             return null;
         }
diff --git a/work/work-runtime/src/schemas/androidx.work.impl.WorkDatabase/22.json b/work/work-runtime/src/schemas/androidx.work.impl.WorkDatabase/22.json
new file mode 100644
index 0000000..619a536
--- /dev/null
+++ b/work/work-runtime/src/schemas/androidx.work.impl.WorkDatabase/22.json
@@ -0,0 +1,517 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 22,
+    "identityHash": "347835753abee7989f767d3ba5a5a2dd",
+    "entities": [
+      {
+        "tableName": "Dependency",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `prerequisite_id` TEXT NOT NULL, PRIMARY KEY(`work_spec_id`, `prerequisite_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`prerequisite_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "prerequisiteId",
+            "columnName": "prerequisite_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "work_spec_id",
+            "prerequisite_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_Dependency_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          },
+          {
+            "name": "index_Dependency_prerequisite_id",
+            "unique": false,
+            "columnNames": [
+              "prerequisite_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_prerequisite_id` ON `${TABLE_NAME}` (`prerequisite_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "prerequisite_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkSpec",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` INTEGER NOT NULL, `worker_class_name` TEXT NOT NULL, `input_merger_class_name` TEXT NOT NULL, `input` BLOB NOT NULL, `output` BLOB NOT NULL, `initial_delay` INTEGER NOT NULL, `interval_duration` INTEGER NOT NULL, `flex_duration` INTEGER NOT NULL, `run_attempt_count` INTEGER NOT NULL, `backoff_policy` INTEGER NOT NULL, `backoff_delay_duration` INTEGER NOT NULL, `last_enqueue_time` INTEGER NOT NULL DEFAULT -1, `minimum_retention_duration` INTEGER NOT NULL, `schedule_requested_at` INTEGER NOT NULL, `run_in_foreground` INTEGER NOT NULL, `out_of_quota_policy` INTEGER NOT NULL, `period_count` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, `next_schedule_time_override` INTEGER NOT NULL DEFAULT 9223372036854775807, `next_schedule_time_override_generation` INTEGER NOT NULL DEFAULT 0, `stop_reason` INTEGER NOT NULL DEFAULT -256, `required_network_type` INTEGER NOT NULL, `required_network_request` BLOB NOT NULL DEFAULT x'', `requires_charging` INTEGER NOT NULL, `requires_device_idle` INTEGER NOT NULL, `requires_battery_not_low` INTEGER NOT NULL, `requires_storage_not_low` INTEGER NOT NULL, `trigger_content_update_delay` INTEGER NOT NULL, `trigger_max_content_delay` INTEGER NOT NULL, `content_uri_triggers` BLOB NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "state",
+            "columnName": "state",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workerClassName",
+            "columnName": "worker_class_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "inputMergerClassName",
+            "columnName": "input_merger_class_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "input",
+            "columnName": "input",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "output",
+            "columnName": "output",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "initialDelay",
+            "columnName": "initial_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "intervalDuration",
+            "columnName": "interval_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "flexDuration",
+            "columnName": "flex_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "runAttemptCount",
+            "columnName": "run_attempt_count",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffPolicy",
+            "columnName": "backoff_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffDelayDuration",
+            "columnName": "backoff_delay_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastEnqueueTime",
+            "columnName": "last_enqueue_time",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "-1"
+          },
+          {
+            "fieldPath": "minimumRetentionDuration",
+            "columnName": "minimum_retention_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "scheduleRequestedAt",
+            "columnName": "schedule_requested_at",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "expedited",
+            "columnName": "run_in_foreground",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "outOfQuotaPolicy",
+            "columnName": "out_of_quota_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "periodCount",
+            "columnName": "period_count",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "generation",
+            "columnName": "generation",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "nextScheduleTimeOverride",
+            "columnName": "next_schedule_time_override",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "9223372036854775807"
+          },
+          {
+            "fieldPath": "nextScheduleTimeOverrideGeneration",
+            "columnName": "next_schedule_time_override_generation",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "stopReason",
+            "columnName": "stop_reason",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "-256"
+          },
+          {
+            "fieldPath": "constraints.requiredNetworkType",
+            "columnName": "required_network_type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.requiredNetworkRequestCompat",
+            "columnName": "required_network_request",
+            "affinity": "BLOB",
+            "notNull": true,
+            "defaultValue": "x''"
+          },
+          {
+            "fieldPath": "constraints.requiresCharging",
+            "columnName": "requires_charging",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.requiresDeviceIdle",
+            "columnName": "requires_device_idle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.requiresBatteryNotLow",
+            "columnName": "requires_battery_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.requiresStorageNotLow",
+            "columnName": "requires_storage_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.contentTriggerUpdateDelayMillis",
+            "columnName": "trigger_content_update_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.contentTriggerMaxDelayMillis",
+            "columnName": "trigger_max_content_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.contentUriTriggers",
+            "columnName": "content_uri_triggers",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_WorkSpec_schedule_requested_at",
+            "unique": false,
+            "columnNames": [
+              "schedule_requested_at"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_schedule_requested_at` ON `${TABLE_NAME}` (`schedule_requested_at`)"
+          },
+          {
+            "name": "index_WorkSpec_last_enqueue_time",
+            "unique": false,
+            "columnNames": [
+              "last_enqueue_time"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_last_enqueue_time` ON `${TABLE_NAME}` (`last_enqueue_time`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "WorkTag",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`tag`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "tag",
+            "work_spec_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_WorkTag_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkTag_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "SystemIdInfo",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, `system_id` INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`, `generation`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "generation",
+            "columnName": "generation",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "systemId",
+            "columnName": "system_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "work_spec_id",
+            "generation"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkName",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`name`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "name",
+            "work_spec_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_WorkName_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkName_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkProgress",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `progress` BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "progress",
+            "columnName": "progress",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "work_spec_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Preference",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `long_value` INTEGER, PRIMARY KEY(`key`))",
+        "fields": [
+          {
+            "fieldPath": "key",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value",
+            "columnName": "long_value",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "key"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '347835753abee7989f767d3ba5a5a2dd')"
+    ]
+  }
+}
\ No newline at end of file