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