aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBill Yi <byi@google.com>2018-11-28 18:34:27 -0800
committerBill Yi <byi@google.com>2018-11-28 18:34:27 -0800
commite847c546dd93bb9a6a5bd320e6108f2ff52171cd (patch)
tree1fa604cc39cd608146aa60c143ae29d2c1279994
parentae78fb8a99ef9b253681ce60ac5f3be0b9c77c0f (diff)
parente74a0e388018f7d217bd2a6efc73bede96dcd64a (diff)
downloadsupport-e847c546dd93bb9a6a5bd320e6108f2ff52171cd.tar.gz
Merge pi-qpr1-release PQ1A.181105.017.A1 to pi-platform-releasepie-platform-releasepie-cuttlefish-testing
Change-Id: I192eb70ee1f81a4e177a530061ec849044c27d4a
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt192
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt2
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt2
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/docs/ConcatenateFilesTask.kt58
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt4
-rw-r--r--car/build.gradle1
-rw-r--r--car/res/layout/preference_category_material_car.xml53
-rw-r--r--car/res/layout/preference_dropdown_material_car.xml63
-rw-r--r--car/res/layout/preference_material_car.xml67
-rw-r--r--car/res/layout/preference_material_car_child.xml76
-rw-r--r--car/res/layout/preference_widget_seekbar_material_car.xml88
-rw-r--r--car/res/values/dimens.xml4
-rw-r--r--car/res/values/styles.xml103
-rw-r--r--car/res/values/themes.xml25
-rw-r--r--car/src/androidTest/java/androidx/car/navigation/utils/BundlableTest.java131
-rw-r--r--car/src/androidTest/java/androidx/car/navigation/utils/BundleMarshallerTest.java219
-rw-r--r--car/src/androidTest/java/androidx/car/navigation/utils/TestBundlable.java107
-rw-r--r--car/src/androidTest/java/androidx/car/navigation/utils/TestBundlableNewVersion.java132
-rw-r--r--car/src/main/java/androidx/car/navigation/utils/Bundlable.java99
-rw-r--r--car/src/main/java/androidx/car/navigation/utils/BundleMarshaller.java514
-rwxr-xr-xjetifier/jetifier/source-transformer/rewriteMake.py1
-rw-r--r--leanback/src/main/res/values-bn/strings.xml4
-rw-r--r--samples/SupportPreferenceDemos/build.gradle1
-rw-r--r--samples/SupportPreferenceDemos/src/main/AndroidManifest.xml13
-rw-r--r--samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCar.java68
-rw-r--r--samples/SupportPreferenceDemos/src/main/res/values/strings.xml1
-rw-r--r--samples/SupportPreferenceDemos/src/main/res/values/styles.xml5
-rw-r--r--samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml7
-rw-r--r--work/integration-tests/testapp/build.gradle4
-rw-r--r--work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java10
-rw-r--r--work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java2
-rw-r--r--work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java2
-rw-r--r--work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java2
-rw-r--r--work/workmanager-ktx/src/androidTest/java/androidx/work/DataTest.kt4
-rw-r--r--work/workmanager/build.gradle6
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java205
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java65
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java6
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java44
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java1
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java2
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java24
-rw-r--r--work/workmanager/src/androidTest/java/androidx/work/worker/CheckLimitsWorker.java16
-rw-r--r--work/workmanager/src/main/java/androidx/work/Configuration.java14
-rw-r--r--work/workmanager/src/main/java/androidx/work/Constraints.java34
-rw-r--r--work/workmanager/src/main/java/androidx/work/ContentUriTriggers.java12
-rw-r--r--work/workmanager/src/main/java/androidx/work/Data.java64
-rw-r--r--work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java10
-rw-r--r--work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java4
-rw-r--r--work/workmanager/src/main/java/androidx/work/State.java16
-rw-r--r--work/workmanager/src/main/java/androidx/work/SynchronousWorkContinuation.java3
-rw-r--r--work/workmanager/src/main/java/androidx/work/SynchronousWorkManager.java13
-rw-r--r--work/workmanager/src/main/java/androidx/work/WorkContinuation.java18
-rw-r--r--work/workmanager/src/main/java/androidx/work/WorkManager.java54
-rw-r--r--work/workmanager/src/main/java/androidx/work/WorkRequest.java32
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/Schedulers.java45
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java10
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java17
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java60
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java68
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java59
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/RescheduleReceiver.java18
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java8
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java29
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/utils/Preferences.java29
-rw-r--r--work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java2
-rw-r--r--work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/3.json363
-rw-r--r--work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/4.json363
-rw-r--r--work/workmanager/src/test/java/androidx/work/DataTest.java5
-rw-r--r--work/workmanager/src/test/java/androidx/work/OverwritingInputMergerTest.java8
70 files changed, 3316 insertions, 475 deletions
diff --git a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
index 40ed9c4d5d6..e74ee0c5c82 100644
--- a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
@@ -27,6 +27,7 @@ import androidx.build.doclava.CHECK_API_CONFIG_DEVELOP
import androidx.build.doclava.CHECK_API_CONFIG_RELEASE
import androidx.build.doclava.CHECK_API_CONFIG_PATCH
import androidx.build.doclava.ChecksConfig
+import androidx.build.docs.ConcatenateFilesTask
import androidx.build.docs.GenerateDocsTask
import androidx.build.jdiff.JDiffTask
import com.android.build.gradle.AppExtension
@@ -46,6 +47,8 @@ import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import java.io.File
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
import kotlin.collections.Collection
import kotlin.collections.List
import kotlin.collections.MutableMap
@@ -69,7 +72,13 @@ object DiffAndDocs {
private lateinit var rules: List<PublishDocsRules>
private val docsTasks: MutableMap<String, GenerateDocsTask> = mutableMapOf()
+ private lateinit var aggregateOldApiTxtsTask: ConcatenateFilesTask
+ private lateinit var aggregateNewApiTxtsTask: ConcatenateFilesTask
+ private lateinit var generateDiffsTask: JDiffTask
+ /**
+ * Initialization that should happen only once (and on the root project)
+ */
@JvmStatic
fun configureDiffAndDocs(
root: Project,
@@ -82,6 +91,10 @@ object DiffAndDocs {
anchorTask = root.tasks.create("anchorDocsTask")
val doclavaConfiguration = root.configurations.getByName("doclava")
val generateSdkApiTask = createGenerateSdkApiTask(root, doclavaConfiguration)
+ val now = LocalDateTime.now()
+ // The diff output assumes that each library is of the same version, but our libraries may each be of different versions
+ // So, we display the date as the new version
+ val newVersion = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
rules.forEach {
val task = createGenerateDocsTask(
project = root, generateSdkApiTask = generateSdkApiTask,
@@ -95,7 +108,41 @@ object DiffAndDocs {
root.tasks.create("generateDocs").dependsOn(docsTasks[TIP_OF_TREE.name])
+ val docletClasspath = doclavaConfiguration.resolve()
+
+ aggregateOldApiTxtsTask = root.tasks.create("aggregateOldApiTxts", ConcatenateFilesTask::class.java)
+ aggregateOldApiTxtsTask.Output = File(root.docsDir(), "previous.txt")
+
+ val oldApisTask = root.tasks.createWithConfig("oldApisXml", ApiXmlConversionTask::class.java) {
+ classpath = root.files(docletClasspath)
+ dependsOn(doclavaConfiguration)
+
+ inputApiFile = aggregateOldApiTxtsTask.Output
+ dependsOn(aggregateOldApiTxtsTask)
+
+ outputApiXmlFile = File(root.docsDir(), "previous.xml")
+ }
+
+ aggregateNewApiTxtsTask = root.tasks.create("aggregateNewApiTxts", ConcatenateFilesTask::class.java)
+ aggregateNewApiTxtsTask.Output = File(root.docsDir(), "$newVersion")
+
+ val newApisTask = root.tasks.createWithConfig("newApisXml", ApiXmlConversionTask::class.java) {
+ classpath = root.files(docletClasspath)
+
+ inputApiFile = aggregateNewApiTxtsTask.Output
+ dependsOn(aggregateNewApiTxtsTask)
+
+ outputApiXmlFile = File(root.docsDir(), "$newVersion.xml")
+ }
+
+ val jdiffConfiguration = root.configurations.getByName("jdiff")
+ generateDiffsTask = createGenerateDiffsTask(root,
+ oldApisTask,
+ newApisTask,
+ jdiffConfiguration)
+
setupDocsProject()
+
return anchorTask
}
@@ -185,7 +232,7 @@ object DiffAndDocs {
}
/**
- * Registers a Java project for global docs generation, local API file generation, and
+ * Registers a Java project to be included in docs generation, local API file generation, and
* local API diff generation tasks.
*/
fun registerJavaProject(project: Project, extension: SupportLibraryExtension) {
@@ -205,16 +252,16 @@ object DiffAndDocs {
"ignoring API tasks.")
return
}
- val tasks = initializeApiChecksForProject(project)
+ val tasks = initializeApiChecksForProject(project, aggregateOldApiTxtsTask, aggregateNewApiTxtsTask)
registerJavaProjectForDocsTask(tasks.generateApi, compileJava)
- registerJavaProjectForDocsTask(tasks.generateDiffs, compileJava)
+ registerJavaProjectForDocsTask(generateDiffsTask, compileJava)
setupDocsTasks(project, tasks)
anchorTask.dependsOn(tasks.checkApiTask)
}
/**
- * Registers an Android project for global docs generation, local API file generation, and
- * local API diff generation tasks.
+ * Registers an Android project to be included in global docs generation, local API file
+ * generation, and local API diff generation tasks.
*/
fun registerAndroidProject(
project: Project,
@@ -248,9 +295,9 @@ object DiffAndDocs {
"an api folder, ignoring API tasks.")
return@all
}
- val tasks = initializeApiChecksForProject(project)
+ val tasks = initializeApiChecksForProject(project, aggregateOldApiTxtsTask, aggregateNewApiTxtsTask)
registerAndroidProjectForDocsTask(tasks.generateApi, variant)
- registerAndroidProjectForDocsTask(tasks.generateDiffs, variant)
+ registerAndroidProjectForDocsTask(generateDiffsTask, variant)
setupDocsTasks(project, tasks)
anchorTask.dependsOn(tasks.checkApiTask)
}
@@ -259,7 +306,7 @@ object DiffAndDocs {
private fun setupDocsTasks(project: Project, tasks: Tasks) {
docsTasks.values.forEach { docs ->
- tasks.generateDiffs.dependsOn(docs)
+ generateDiffsTask.dependsOn(docs)
// Track API change history.
docs.addSinceFilesFrom(project.projectDir)
// Associate current API surface with the Maven artifact.
@@ -281,21 +328,20 @@ private fun stripExtension(fileName: String) = fileName.substringBeforeLast('.')
private fun getLastReleasedApiFile(rootFolder: File, refVersion: Version?): File? {
val apiDir = File(rootFolder, "api")
- val lastFile = getLastReleasedApiFileFromDir(apiDir, refVersion)
- if (lastFile != null) {
- return lastFile
- }
-
- return null
+ return getLastReleasedApiFileFromDir(apiDir, refVersion)
}
+/**
+ * Returns the api file with highest version among those having version less than refVersion
+ */
private fun getLastReleasedApiFileFromDir(apiDir: File, refVersion: Version?): File? {
var lastFile: File? = null
var lastVersion: Version? = null
apiDir.listFiles().forEach { file ->
- Version.parseOrNull(file)?.let { version ->
- if ((lastFile == null || lastVersion!! < version) &&
- (refVersion == null || version < refVersion)) {
+ val parsed = Version.parseOrNull(file)
+ parsed?.let { version ->
+ if ((lastFile == null || lastVersion!! < version)
+ && (refVersion == null || version < refVersion)) {
lastFile = file
lastVersion = version
}
@@ -327,7 +373,8 @@ private fun getApiFile(rootDir: File, refVersion: Version, forceRelease: Boolean
return File(apiDir, "current.txt")
}
-// Generates API files
+
+// Creates a new task on the project for generating API files
private fun createGenerateApiTask(project: Project, docletpathParam: Collection<File>) =
project.tasks.createWithConfig("generateApi", DoclavaTask::class.java) {
setDocletpath(docletpathParam)
@@ -345,6 +392,7 @@ private fun createGenerateApiTask(project: Project, docletpathParam: Collection<
exclude("**/R.java")
}
+// Creates a new task on the project for verifying the API
private fun createCheckApiTask(
project: Project,
taskName: String,
@@ -437,57 +485,37 @@ private fun createUpdateApiTask(project: Project, checkApiRelease: CheckApiTask)
}
/**
- * Converts the <code>fromApi</code>.txt file (or the most recently released
- * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format
- * for use by JDiff.
+ * Returns the filepath of the previous API txt file (for computing diffs against)
*/
-private fun createOldApiXml(project: Project, doclavaConfig: Configuration) =
- project.tasks.createWithConfig("oldApiXml", ApiXmlConversionTask::class.java) {
- val toApi = project.processProperty("toApi")?.let {
- Version.parseOrNull(it)
- }
- val fromApi = project.processProperty("fromApi")
- classpath = project.files(doclavaConfig.resolve())
- val rootFolder = project.projectDir
- inputApiFile = if (fromApi != null) {
- // Use an explicit API file.
- File(rootFolder, "api/$fromApi.txt")
- } else {
- // Use the most recently released API file bounded by toApi.
- getLastReleasedApiFile(rootFolder, toApi)
- }
+private fun getOldApiTxt(project: Project): File? {
+ val toApi = project.processProperty("toApi")?.let {
+ Version.parseOrNull(it)
+ }
+ val fromApi = project.processProperty("fromApi")
+ val rootFolder = project.projectDir
+ if (fromApi != null) {
+ // Use an explicit API file.
+ return File(rootFolder, "api/$fromApi.txt")
+ } else {
+ // Use the most recently released API file bounded by toApi.
+ return getLastReleasedApiFile(rootFolder, toApi)
+ }
+}
- outputApiXmlFile = File(project.docsDir(),
- "release/${stripExtension(inputApiFile?.name ?: "creation")}.xml")
- dependsOn(doclavaConfig)
- }
+data class FileProvider(val file: File, val task: Task?)
-/**
- * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
- * defined using -PtoApi=<file>) to XML format for use by JDiff.
- */
-private fun createNewApiXmlTask(
- project: Project,
- generateApi: DoclavaTask,
- doclavaConfig: Configuration
-) =
- project.tasks.createWithConfig("newApiXml", ApiXmlConversionTask::class.java) {
- classpath = project.files(doclavaConfig.resolve())
- val toApi = project.processProperty("toApi")
-
- if (toApi != null) {
- // Use an explicit API file.
- inputApiFile = File(project.projectDir, "api/$toApi.txt")
- } else {
- // Use the current API file (e.g. current.txt).
- inputApiFile = generateApi.apiFile!!
- dependsOn(generateApi, doclavaConfig)
- }
+private fun getNewApiTxt(project: Project, generateApi: DoclavaTask): FileProvider {
+ val toApi = project.processProperty("toApi")
+ if (toApi != null) {
+ // Use an explicit API file.
+ return FileProvider(File(project.projectDir, "api/$toApi.txt"), null)
+ } else {
+ // Use the current API file (e.g. current.txt).
+ return FileProvider(generateApi.apiFile!!, generateApi)
+ }
- outputApiXmlFile = File(project.docsDir(),
- "release/${stripExtension(inputApiFile?.name ?: "creation")}.xml")
- }
+}
/**
* Generates API diffs.
@@ -532,7 +560,7 @@ private fun createGenerateDiffsTask(
newApiXmlFile = newApiTask.outputApiXmlFile
val newApi = newApiXmlFile.name.substringBeforeLast('.')
- val docsDir = project.rootProject.docsDir()
+ val docsDir = File(project.rootProject.docsDir(), "public")
newJavadocPrefix = "../../../../../reference/"
destinationDir = File(docsDir, "online/sdk/support_api_diff/${project.name}/$newApi")
@@ -543,6 +571,9 @@ private fun createGenerateDiffsTask(
exclude("**/BuildConfig.java", "**/R.java")
dependsOn(oldApiTask, newApiTask, jdiffConfig)
+ doLast {
+ project.logger.lifecycle("generated diffs into $destinationDir")
+ }
}
// Generates a distribution artifact for online docs.
@@ -642,11 +673,13 @@ private fun createGenerateDocsTask(
private data class Tasks(
val generateApi: DoclavaTask,
- val generateDiffs: JDiffTask,
val checkApiTask: CheckApiTask
)
-private fun initializeApiChecksForProject(project: Project): Tasks {
+/**
+ * Sets up api tasks for the given project
+ */
+private fun initializeApiChecksForProject(project: Project, aggregateOldApiTxtsTask: ConcatenateFilesTask, aggregateNewApiTxtsTask:ConcatenateFilesTask): Tasks {
if (!project.hasProperty("docsDir")) {
project.extensions.add("docsDir", File(project.rootProject.docsDir(), project.name))
}
@@ -658,7 +691,7 @@ private fun initializeApiChecksForProject(project: Project): Tasks {
val generateApi = createGenerateApiTask(project, docletClasspath)
generateApi.dependsOn(doclavaConfiguration)
- // Make sure the API surface has not broken since the last release.
+ // for verifying that the API surface has not broken since the last release
val lastReleasedApiFile = getLastReleasedApiFile(workingDir, version)
val whitelistFile = lastReleasedApiFile?.let { apiFile ->
@@ -696,15 +729,20 @@ private fun initializeApiChecksForProject(project: Project): Tasks {
val updateApiTask = createUpdateApiTask(project, checkApiRelease)
updateApiTask.dependsOn(checkApiRelease)
- val newApiTask = createNewApiXmlTask(project, generateApi, doclavaConfiguration)
- val oldApiTask = createOldApiXml(project, doclavaConfiguration)
-
- val jdiffConfiguration = project.rootProject.configurations.getByName("jdiff")
- val generateDiffTask = createGenerateDiffsTask(project,
- oldApiTask,
- newApiTask,
- jdiffConfiguration)
- return Tasks(generateApi, generateDiffTask, checkApi)
+
+
+ val oldApiTxt = getOldApiTxt(project)
+ if (oldApiTxt != null) {
+ aggregateOldApiTxtsTask.addInput(project.name, oldApiTxt)
+ }
+ val newApiTxtProvider = getNewApiTxt(project, generateApi)
+ aggregateNewApiTxtsTask.inputs.file(newApiTxtProvider.file)
+ aggregateNewApiTxtsTask.addInput(project.name, newApiTxtProvider.file)
+ if (newApiTxtProvider.task != null) {
+ aggregateNewApiTxtsTask.dependsOn(newApiTxtProvider.task)
+ }
+
+ return Tasks(generateApi, checkApi)
}
fun hasApiTasks(project: Project, extension: SupportLibraryExtension): Boolean {
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 12160e1c94f..7d81cd8cd68 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -87,7 +87,7 @@ object LibraryVersions {
/**
* Version code for WorkManager
*/
- val WORKMANAGER = Version("1.0.0-alpha02")
+ val WORKMANAGER = Version("1.0.0-alpha04")
/**
* Version code for Jetifier
diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt
index f66706ca732..b191d56ec2f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt
@@ -23,7 +23,7 @@ import org.gradle.api.tasks.OutputFile
import java.io.File
/**
- * Task that converts the given API file to XML format.
+ * Task that converts the given API txt file to XML format.
*/
open class ApiXmlConversionTask : JavaExec() {
@Optional
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/ConcatenateFilesTask.kt b/buildSrc/src/main/kotlin/androidx/build/docs/ConcatenateFilesTask.kt
new file mode 100644
index 00000000000..d97c7019df8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/ConcatenateFilesTask.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.build.docs
+
+import org.gradle.api.Action
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.OutputFile
+import java.io.File
+import java.util.SortedMap
+
+open class ConcatenateFilesTask : DefaultTask() {
+ private var keyedInputs: MutableMap<String, File> = mutableMapOf()
+
+ @get:OutputFile
+ lateinit var Output: File
+
+ // Adds the given input file
+ // The order that files are concatenated in is based on sorting the corresponding keys
+ fun addInput(key: String, inputFile: File) {
+ if (this.keyedInputs.containsKey(key)) {
+ throw IllegalArgumentException("Key $key already exists")
+ }
+ this.inputs.file(inputFile)
+ this.keyedInputs[key] = inputFile
+ }
+
+ @TaskAction
+ fun aggregate() {
+ val destFile = this.Output
+
+ // sort the input files to make sure this task always concatenates them in the same order
+ val sortedInputs = this.keyedInputs.toSortedMap()
+
+ val inputFiles = sortedInputs.values
+ if (inputFiles.contains(destFile)) {
+ throw IllegalArgumentException("Output file $destFile is also an input file")
+ }
+
+ val text = inputFiles.joinToString(separator = "") { file -> file.readText() }
+ this.project.logger.info("Joining ${inputFiles.count()} files, and storing the result in ${destFile.path}")
+ destFile.writeText(text)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt b/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt
index 10466ea5182..54e9ec6c033 100644
--- a/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt
@@ -33,7 +33,7 @@ import java.util.ArrayList
open class JDiffTask : Javadoc() {
/**
- * Sets the doclet path which has the `com.google.doclava.Doclava` class.
+ * Sets the doclet path, which will be used to locate the `com.google.doclava.Doclava` class.
*
*
* This option will override any doclet path set in this instance's
@@ -82,7 +82,7 @@ open class JDiffTask : Javadoc() {
}
/**
- * "Configures" this JDiffTask with parameters that might not be at their final values
+ * Configures this JDiffTask with parameters that might not be at their final values
* until this task is run.
*/
private fun configureJDiffTask() {
diff --git a/car/build.gradle b/car/build.gradle
index 2c65ab4bdac..1eacc0e6573 100644
--- a/car/build.gradle
+++ b/car/build.gradle
@@ -13,6 +13,7 @@ dependencies {
api(project(":legacy-support-v4"))
api(project(":recyclerview"))
api(project(":gridlayout"))
+ api(project(":preference"))
api(SUPPORT_DESIGN, libs.exclude_for_material)
androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
diff --git a/car/res/layout/preference_category_material_car.xml b/car/res/layout/preference_category_material_car.xml
new file mode 100644
index 00000000000..2012c69f50d
--- /dev/null
+++ b/car/res/layout/preference_category_material_car.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/car_preference_list_margin"
+ android:background="@drawable/car_card_ripple_background"
+ android:focusable="true"
+ android:orientation="horizontal">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.Car.Subheader"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/car/res/layout/preference_dropdown_material_car.xml b/car/res/layout/preference_dropdown_material_car.xml
new file mode 100644
index 00000000000..932274a775e
--- /dev/null
+++ b/car/res/layout/preference_dropdown_material_car.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/car_preference_list_margin"
+ android:background="@drawable/car_card_ripple_background"
+ android:focusable="true"
+ android:orientation="horizontal">
+
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="0dp"
+ android:layout_weight="0"
+ android:layout_height="wrap_content"
+ android:visibility="invisible" />
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/car_preference_list_margin"
+ android:paddingBottom="@dimen/car_preference_list_margin">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Car.Body1" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Car.Body2" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/car/res/layout/preference_material_car.xml b/car/res/layout/preference_material_car.xml
new file mode 100644
index 00000000000..c3049a2b03f
--- /dev/null
+++ b/car/res/layout/preference_material_car.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/car_preference_list_margin"
+ android:background="@drawable/car_card_ripple_background"
+ android:focusable="true">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_toEndOf="@android:id/icon"
+ android:layout_centerVertical="true">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Car.Body1" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Car.Body2" />
+
+ </LinearLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentEnd="true" />
+
+</RelativeLayout>
diff --git a/car/res/layout/preference_material_car_child.xml b/car/res/layout/preference_material_car_child.xml
new file mode 100644
index 00000000000..eec2d0b501f
--- /dev/null
+++ b/car/res/layout/preference_material_car_child.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/car_preference_list_margin"
+ android:background="@drawable/car_card_ripple_background"
+ android:focusable="true">
+
+ <!-- Wrap in a FrameLayout so the full list item has a touch ripple -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?android:attr/listPreferredItemPaddingStart">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_small_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_small_icon_size"
+ android:layout_centerVertical="true"
+ android:layout_alignParentStart="true"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_toEndOf="@android:id/icon"
+ android:layout_centerVertical="true">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Car.Label1" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Car.Body3" />
+
+ </LinearLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentEnd="true" />
+
+ </RelativeLayout>
+
+</FrameLayout>
+
diff --git a/car/res/layout/preference_widget_seekbar_material_car.xml b/car/res/layout/preference_widget_seekbar_material_car.xml
new file mode 100644
index 00000000000..00640450036
--- /dev/null
+++ b/car/res/layout/preference_widget_seekbar_material_car.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<!-- Layout used by SeekBarPreference for the seekbar widget style. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingVertical="@dimen/car_preference_list_margin"
+ android:orientation="horizontal">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size" />
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Car.Body1"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:textAppearance="@style/TextAppearance.Car.Body2"/>
+
+ <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
+ to the children of this container layout. Otherwise, the animated pressed state will also
+ play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
+ The background of the SeekBar is also set to null to disable the ripple background -->
+ <androidx.preference.UnPressableLinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/summary"
+ android:layout_alignStart="@android:id/title"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ <SeekBar
+ android:id="@+id/seekbar"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/preference_seekbar_padding_start"
+ android:paddingEnd="@dimen/preference_seekbar_padding_end"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" />
+
+ <TextView
+ android:id="@+id/seekbar_value"
+ android:layout_width="@dimen/preference_seekbar_value_width"
+ android:layout_height="match_parent"
+ android:gravity="right|center_vertical"
+ android:textAppearance="@style/TextAppearance.Car.Body2"
+ android:fadingEdge="horizontal"
+ android:scrollbars="none"/>
+ </androidx.preference.UnPressableLinearLayout>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
index fd8d54d11cc..94c8edc39da 100644
--- a/car/res/values/dimens.xml
+++ b/car/res/values/dimens.xml
@@ -215,4 +215,8 @@ limitations under the License.
<dimen name="speed_bump_lock_out_message_height">96dp</dimen>
<dimen name="speed_bump_lock_out_drawable_margin_bottom">8dp</dimen>
+ <!-- Preferences -->
+ <!-- list margin won't be necessary after PagedListView is brought in -->
+ <dimen name="car_preference_list_margin">8dp</dimen>
+
</resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index 67ad199a30a..5db209e1ca4 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -415,4 +415,107 @@ limitations under the License.
<item name="android:background">@drawable/car_action_button_background</item>
<item name="android:tint">@color/car_tint</item>
</style>
+
+ <!-- ================= -->
+ <!-- Preference Themes -->
+ <!-- ================= -->
+ <eat-comment />
+
+ <style name="CarPreference">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">true</item>
+ <item name="iconSpaceReserved">false</item>
+ </style>
+
+ <style name="CarPreference.Information">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="android:enabled">false</item>
+ <item name="android:shouldDisableView">false</item>
+ </style>
+
+ <style name="CarPreference.Category">
+ <item name="android:layout">@layout/preference_category_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ </style>
+
+ <style name="CarPreference.CheckBoxPreference">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:widgetLayout">@layout/preference_widget_checkbox</item>
+ </style>
+
+ <style name="CarPreference.SwitchPreferenceCompat">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:widgetLayout">@layout/preference_widget_switch_compat</item>
+ </style>
+
+ <style name="CarPreference.SwitchPreference">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:widgetLayout">@layout/preference_widget_switch</item>
+ </style>
+
+ <style name="CarPreference.SeekBarPreference">
+ <item name="android:layout">@layout/preference_widget_seekbar_material_car</item>
+ <item name="adjustable">true</item>
+ <item name="showSeekBarValue">true</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:widgetLayout">@layout/preference_widget_seekbar</item>
+ </style>
+
+ <style name="CarPreference.PreferenceScreen">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ </style>
+
+ <style name="CarPreference.DialogPreference">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:positiveButtonText">@android:string/ok</item>
+ <item name="android:negativeButtonText">@android:string/cancel</item>
+ </style>
+
+ <style name="CarPreference.DialogPreference.EditTextPreference">
+ <item name="android:layout">@layout/preference_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">true</item>
+ <item name="iconSpaceReserved">false</item>
+ <item name="android:dialogLayout">@layout/preference_dialog_edittext</item>
+ </style>
+
+ <style name="CarPreference.DropDown">
+ <item name="android:layout">@layout/preference_dropdown_material_car</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">false</item>
+ </style>
+
+ <style name="CarPreferenceFragment">
+ <item name="android:divider">@drawable/car_list_divider</item>
+ <item name="allowDividerAfterLastItem">false</item>
+ </style>
+
+ <style name="CarPreferenceFragmentList">
+ <item name="android:paddingLeft">0dp</item>
+ <item name="android:paddingRight">0dp</item>
+ </style>
</resources>
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
index 7f862d75cab..30303969d1a 100644
--- a/car/res/values/themes.xml
+++ b/car/res/values/themes.xml
@@ -187,4 +187,29 @@ limitations under the License.
<item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1.Light</item>
<item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2.Light</item>
</style>
+
+
+ <!-- ================ -->
+ <!-- Preference Theme -->
+ <!-- ================ -->
+ <eat-comment />
+
+ <!-- Car theme for support library PreferenceFragments -->
+ <style name="PreferenceThemeOverlayCar">
+ <item name="preferenceScreenStyle">@style/CarPreference.PreferenceScreen</item>
+ <item name="preferenceFragmentCompatStyle">@style/CarPreferenceFragment</item>
+ <item name="preferenceFragmentStyle">@style/CarPreferenceFragment</item>
+ <item name="preferenceCategoryStyle">@style/CarPreference.Category</item>
+ <item name="preferenceStyle">@style/CarPreference</item>
+ <item name="preferenceInformationStyle">@style/CarPreference.Information</item>
+ <item name="checkBoxPreferenceStyle">@style/CarPreference.CheckBoxPreference</item>
+ <item name="switchPreferenceCompatStyle">@style/CarPreference.SwitchPreferenceCompat</item>
+ <item name="switchPreferenceStyle">@style/CarPreference.SwitchPreference</item>
+ <item name="seekBarPreferenceStyle">@style/CarPreference.SeekBarPreference</item>
+ <item name="dialogPreferenceStyle">@style/CarPreference.DialogPreference</item>
+ <item name="editTextPreferenceStyle">@style/CarPreference.DialogPreference.EditTextPreference</item>
+ <item name="dropdownPreferenceStyle">@style/CarPreference.DropDown</item>
+ <item name="preferenceFragmentListStyle">@style/CarPreferenceFragmentList</item>
+ <item name="android:preferenceLayoutChild">@layout/preference_material_car_child</item>
+ </style>
</resources>
diff --git a/car/src/androidTest/java/androidx/car/navigation/utils/BundlableTest.java b/car/src/androidTest/java/androidx/car/navigation/utils/BundlableTest.java
new file mode 100644
index 00000000000..7f149d0b210
--- /dev/null
+++ b/car/src/androidTest/java/androidx/car/navigation/utils/BundlableTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.car.navigation.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link Bundlable}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BundlableTest {
+ /**
+ * Serialization test value.
+ */
+ private static final TestBundlable TEST_VALUE = new TestBundlable()
+ .setInt(123)
+ .setString("TEST")
+ .setEnumValue(TestBundlable.TestEnum.VALUE1)
+ .setListValue(Arrays.asList(
+ new TestBundlable()
+ .setString("TEST2")
+ .setEnumValue(TestBundlable.TestEnum.VALUE2),
+ new TestBundlable()
+ .setString("TEST3")
+ ))
+ .setBundlableValue(
+ new TestBundlable()
+ .setString("TEST4")
+ );
+
+ /**
+ * Equivalent to {@link #TEST_VALUE} after a schema change (see
+ * {@link TestBundlableNewVersion}). In this new schema, {@link TestBundlable} has its
+ * {@link TestBundlable#mStringValue} field deprecated, and a new
+ * {@link TestBundlableNewVersion#mNewValue} non-null field was added.
+ */
+ private static final TestBundlableNewVersion TEST_VALUE_NEW_VERSION =
+ new TestBundlableNewVersion()
+ .setInt(123)
+ .setEnumValue(TestBundlableNewVersion.TestEnum.VALUE1)
+ .setListValue(Arrays.asList(
+ new TestBundlableNewVersion()
+ .setEnumValue(TestBundlableNewVersion.TestEnum.VALUE2),
+ new TestBundlableNewVersion()
+ ))
+ .setBundlableValue(
+ new TestBundlableNewVersion()
+ );
+
+ /**
+ * Expected value when interpreting {@link #TEST_VALUE_NEW_VERSION} using the same schema as
+ * {@link TestBundlable}. Given that {@link TestBundlableNewVersion#mNewValue} doesn't exist
+ * the old schema, and {@link TestBundlable#mStringValue} doesn't exist in the new schema,
+ * both values are dropped during serialization/deserialization.
+ */
+ private static final TestBundlable TEST_VALUE_NEW_VERSION_OLD_SCHEMA = new TestBundlable()
+ .setInt(123)
+ .setEnumValue(TestBundlable.TestEnum.VALUE1)
+ .setListValue(Arrays.asList(
+ new TestBundlable()
+ .setEnumValue(TestBundlable.TestEnum.VALUE2),
+ new TestBundlable()
+ ))
+ .setBundlableValue(
+ new TestBundlable()
+ );
+
+ private BundleMarshaller mBundleMarshaller = new BundleMarshaller();
+
+ /**
+ * Asserts that serializing and deserializing a {@link Bundlable} produces the same content.
+ * This includes testing instances with null values in them.
+ */
+ @Test
+ public void testSerializationDeserializationMaintainsContent() {
+ TestBundlable output = new TestBundlable();
+
+ TEST_VALUE.toBundle(mBundleMarshaller);
+ output.fromBundle(mBundleMarshaller);
+ assertEquals(TEST_VALUE, output);
+ }
+
+ /**
+ * Asserts that serialization and deserialization works in a forward compatible way, as long as
+ * they follow the versioning rules listed in {@link Bundlable}.
+ */
+ @Test
+ public void testForwardCompatibleChangesMaintainsCommonContent() {
+ TestBundlableNewVersion output = new TestBundlableNewVersion();
+
+ TEST_VALUE.toBundle(mBundleMarshaller);
+ output.fromBundle(mBundleMarshaller);
+ assertEquals(TEST_VALUE_NEW_VERSION, output);
+ }
+
+ /**
+ * Asserts that serialization and deserialization works in a backwards compatible way, as long
+ * as they follow the versioning rules listed in {@link Bundlable}.
+ */
+ @Test
+ public void testBackwardCompatibleChangesMaintainsCommonContent() {
+ TestBundlable output = new TestBundlable();
+
+ TEST_VALUE_NEW_VERSION.toBundle(mBundleMarshaller);
+ output.fromBundle(mBundleMarshaller);
+ assertEquals(TEST_VALUE_NEW_VERSION_OLD_SCHEMA, output);
+ }
+}
diff --git a/car/src/androidTest/java/androidx/car/navigation/utils/BundleMarshallerTest.java b/car/src/androidTest/java/androidx/car/navigation/utils/BundleMarshallerTest.java
new file mode 100644
index 00000000000..cf5ac15c2db
--- /dev/null
+++ b/car/src/androidTest/java/androidx/car/navigation/utils/BundleMarshallerTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.car.navigation.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Unit tests for {@link BundleMarshaller}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BundleMarshallerTest {
+ private final BundleMarshaller mBundleMarshaller = new BundleMarshaller();
+
+ /**
+ * A random test value with a mix of primitives, null and non-null data.
+ */
+ private static final TestBundlable TEST_VALUE = new TestBundlable()
+ .setInt(1)
+ .setString("TEST")
+ .setBundlableValue(new TestBundlable()
+ .setEnumValue(TestBundlable.TestEnum.VALUE1));
+
+ /**
+ * Tests that null values are serialized as expected.
+ */
+ @Test
+ public void serialization_nullCase() {
+ new TestBundlable().toBundle(mBundleMarshaller);
+
+ Bundle data = mBundleMarshaller.getBundle();
+ assertTrue(data.containsKey("intValue"));
+ assertEquals(0, data.getInt("intValue"));
+ assertNull(data.getString("stringValue"));
+ assertNull(data.getString("enumValue"));
+ assertTrue(data.getBoolean("bundlableValue._isNull"));
+ assertEquals(-1, data.getInt("bundlableListValue._size"));
+ }
+
+ /**
+ * Tests that nested {@link Bundlable}s are serialized as expected.
+ */
+ @Test
+ public void serialization_nestedBundlable() {
+ String stringValue = "TEST";
+ int intValue = 1;
+
+ new TestBundlable()
+ .setBundlableValue(new TestBundlable()
+ .setInt(intValue)
+ .setString(stringValue))
+ .toBundle(mBundleMarshaller);
+
+ Bundle data = mBundleMarshaller.getBundle();
+ assertTrue(data.containsKey("bundlableValue._isNull"));
+ assertFalse(data.getBoolean("bundlableValue._isNull"));
+ assertEquals(intValue, data.getInt("bundlableValue.intValue"));
+ assertEquals(stringValue, data.getString("bundlableValue.stringValue"));
+ }
+
+ /**
+ * Tests the correct serialization of a list of size 0.
+ */
+ @Test
+ public void listSerialization_listOfSize0() {
+ TestBundlable value = new TestBundlable().setListValue(new ArrayList<>());
+ value.toBundle(mBundleMarshaller);
+ Bundle data = mBundleMarshaller.getBundle();
+ assertEquals(0, data.getInt("bundlableListValue._size"));
+ }
+
+ /**
+ * Tests the correct serialization of a list of size n.
+ */
+ @Test
+ public void listSerialization_listOfSizeN() {
+ TestBundlable value = new TestBundlable().setListValue(Arrays.asList(
+ new TestBundlable().setInt(1),
+ new TestBundlable().setInt(2),
+ new TestBundlable().setInt(3)));
+ Bundle data = mBundleMarshaller.getBundle();
+ value.toBundle(mBundleMarshaller);
+ assertEquals(3, data.getInt("bundlableListValue._size"));
+ assertEquals(1, data.getInt("bundlableListValue.0.intValue"));
+ assertEquals(2, data.getInt("bundlableListValue.1.intValue"));
+ assertEquals(3, data.getInt("bundlableListValue.2.intValue"));
+ }
+
+ @Test
+ public void listSerialization_removingElementsInPlace() {
+ // Serialize and deserialize a list of a certain size
+ List<TestBundlable> mutableList = new ArrayList<>(Arrays.asList(
+ new TestBundlable().setInt(1),
+ new TestBundlable().setInt(2),
+ new TestBundlable().setInt(3)));
+ TestBundlable out = new TestBundlable().setListValue(mutableList);
+ TestBundlable in = new TestBundlable();
+ out.toBundle(mBundleMarshaller);
+ in.fromBundle(mBundleMarshaller);
+ assertEquals(out, in);
+
+ // Remove some elements and check that they are correctly removed during deserialization
+ mutableList.remove(0);
+ out.toBundle(mBundleMarshaller);
+ in.fromBundle(mBundleMarshaller);
+ assertEquals(out, in);
+ }
+
+ /**
+ * Tests that {@link BundleMarshaller#getDelta()} returns the same value as
+ * {@link BundleMarshaller#getBundle()} during the initial serialization.
+ */
+ @Test
+ public void deltaSerialization_equalsFullDataIfNotReset() {
+ TEST_VALUE.toBundle(mBundleMarshaller);
+ Bundle data = mBundleMarshaller.getBundle();
+ Bundle delta = mBundleMarshaller.getDelta();
+ assertBundlesEqual(data, delta);
+ }
+
+ /**
+ * Tests that {@link BundleMarshaller#getDelta()} is empty if no data is modified between
+ * serializations.
+ */
+ @Test
+ public void deltaSerialization_emptyIfNoDataIsModified() {
+ TEST_VALUE.toBundle(mBundleMarshaller);
+ mBundleMarshaller.resetDelta();
+ TEST_VALUE.toBundle(mBundleMarshaller);
+ Bundle delta = mBundleMarshaller.getDelta();
+ assertEquals(0, delta.size());
+ }
+
+ /**
+ * Tests that {@link BundleMarshaller#getDelta()} returns only the data that has been modified
+ * between two serializations.
+ */
+ @Test
+ public void deltaSerialization_onlyContainsModifiedData() {
+ // Serialize some base data
+ TestBundlable testValue = new TestBundlable()
+ .setInt(1)
+ .setString("TEST")
+ .setBundlableValue(new TestBundlable()
+ .setEnumValue(TestBundlable.TestEnum.VALUE1));
+ testValue.toBundle(mBundleMarshaller);
+
+ // Reset change tracking and re-serialize after making some changes.
+ mBundleMarshaller.resetDelta();
+ testValue.setInt(2).setString(null);
+ testValue.mBundableValue.setEnumValue(TestBundlable.TestEnum.VALUE2);
+ testValue.toBundle(mBundleMarshaller);
+
+ Bundle expectedDelta = new Bundle();
+ expectedDelta.putInt("intValue", testValue.mIntValue);
+ expectedDelta.putString("stringValue", testValue.mStringValue);
+ expectedDelta.putString("bundlableValue.enumValue",
+ testValue.mBundableValue.mEnumValue.name());
+
+ Bundle delta = mBundleMarshaller.getDelta();
+ assertBundlesEqual(expectedDelta, delta);
+ }
+
+ /**
+ * Asserts that the provided {@link Bundle}s are equal. It throws {@link AssertionError}
+ * otherwise.
+ */
+ private void assertBundlesEqual(Bundle expected, Bundle actual) {
+ if (expected == null && actual == null) {
+ return;
+ }
+ if (expected == null || actual == null) {
+ fail(String.format("Expected %s value but found %s",
+ expected != null ? "non-null" : "null",
+ actual != null ? "non-null" : "null"));
+ }
+ if (!expected.keySet().equals(actual.keySet())) {
+ fail(String.format("Expected keys: %s, but found keys: %s",
+ expected.keySet().stream().sorted().collect(Collectors.joining(",")),
+ actual.keySet().stream().sorted().collect(Collectors.joining(","))));
+ }
+ for (String key : expected.keySet()) {
+ assertEquals(String.format("Expected '%s' at key '%s' but found '%s",
+ expected.get(key), key, actual.get(key)),
+ expected.get(key),
+ actual.get(key));
+ }
+ }
+}
diff --git a/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlable.java b/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlable.java
new file mode 100644
index 00000000000..1ca19d31129
--- /dev/null
+++ b/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlable.java
@@ -0,0 +1,107 @@
+/*
+ * 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.car.navigation.utils;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Reference implementation of a {@link Bundlable}
+ */
+class TestBundlable implements Bundlable {
+ private static final String INT_VALUE_KEY = "intValue";
+ private static final String STRING_VALUE_KEY = "stringValue";
+ private static final String ENUM_VALUE_KEY = "enumValue";
+ private static final String BUNDLABLE_VALUE_KEY = "bundlableValue";
+ private static final String BUNDLABLE_LIST_VALUE_KEY = "bundlableListValue";
+
+ enum TestEnum {
+ VALUE1,
+ VALUE2,
+ VALUE3,
+ }
+
+ int mIntValue;
+ String mStringValue;
+ TestEnum mEnumValue;
+ TestBundlable mBundableValue;
+ List<TestBundlable> mListValue;
+
+ @Override
+ public void toBundle(BundleMarshaller out) {
+ out.putInt(INT_VALUE_KEY, mIntValue);
+ out.putString(STRING_VALUE_KEY, mStringValue);
+ out.putEnum(ENUM_VALUE_KEY, mEnumValue);
+ out.putBundlable(BUNDLABLE_VALUE_KEY, mBundableValue);
+ out.putBundlableList(BUNDLABLE_LIST_VALUE_KEY, mListValue);
+ }
+
+ @Override
+ public void fromBundle(BundleMarshaller in) {
+ mIntValue = in.getInt(INT_VALUE_KEY);
+ mStringValue = in.getString(STRING_VALUE_KEY);
+ mEnumValue = in.getEnum(ENUM_VALUE_KEY, TestEnum.class);
+ mBundableValue = in.getBundlable(BUNDLABLE_VALUE_KEY, mBundableValue, TestBundlable::new);
+ mListValue = in.getBundlableList(BUNDLABLE_LIST_VALUE_KEY, mListValue, TestBundlable::new);
+ }
+
+ public TestBundlable setInt(int value) {
+ mIntValue = value;
+ return this;
+ }
+
+ public TestBundlable setString(String value) {
+ mStringValue = value;
+ return this;
+ }
+
+ public TestBundlable setEnumValue(TestEnum value) {
+ mEnumValue = value;
+ return this;
+ }
+
+ public TestBundlable setBundlableValue(TestBundlable value) {
+ mBundableValue = value;
+ return this;
+ }
+
+ public TestBundlable setListValue(List<TestBundlable> value) {
+ mListValue = value;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ TestBundlable that = (TestBundlable) o;
+ return mIntValue == that.mIntValue
+ && Objects.equals(mStringValue, that.mStringValue)
+ && mEnumValue == that.mEnumValue
+ && Objects.equals(mBundableValue, that.mBundableValue)
+ && Objects.equals(mListValue, that.mListValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIntValue, mStringValue, mEnumValue, mBundableValue, mListValue);
+ }
+}
diff --git a/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlableNewVersion.java b/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlableNewVersion.java
new file mode 100644
index 00000000000..58ee6f61ad3
--- /dev/null
+++ b/car/src/androidTest/java/androidx/car/navigation/utils/TestBundlableNewVersion.java
@@ -0,0 +1,132 @@
+/*
+ * 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.car.navigation.utils;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Example of a version change. This is a copy of {@link TestBundlable} but with one field
+ * deprecated ({@link TestBundlable#mStringValue}) and one field added
+ * ({@link TestBundlableNewVersion#mNewValue})
+ */
+class TestBundlableNewVersion implements Bundlable {
+ private static final String INT_VALUE_KEY = "intValue";
+ // In this version of TestBundlable, mStringValue field has been deprecated, and its key should
+ // not be re-used.
+ // private static final String STRING_VALUE_KEY = "stringValue";
+ private static final String ENUM_VALUE_KEY = "enumValue";
+ private static final String BUNDLABLE_VALUE_KEY = "bundlableValue";
+ private static final String BUNDLABLE_LIST_VALUE_KEY = "bundlableListValue";
+ private static final String NEW_VALUE_KEY = "newValue";
+
+ enum TestEnum {
+ VALUE1,
+ VALUE2,
+ VALUE3,
+ }
+
+ public static final String DEFAULT_NEW_VALUE = "TEST_V2";
+
+ int mIntValue;
+ TestEnum mEnumValue;
+ // In this version of TestBundlable, we simulate mStringValue being deprecated.
+ // String mStringValue;
+ TestBundlableNewVersion mBundableValue;
+ List<TestBundlableNewVersion> mListValue;
+ // In this version of TestBundlable, we simulate the creation of a new mandatory field.
+ @NonNull
+ String mNewValue;
+
+ TestBundlableNewVersion() {
+ mNewValue = DEFAULT_NEW_VALUE;
+ }
+
+ @Override
+ public void toBundle(@NonNull BundleMarshaller out) {
+ out.putInt(INT_VALUE_KEY, mIntValue);
+ // dest.putString(STRING_VALUE_KEY, mStringValue) is now deprecated.
+ out.putEnum(ENUM_VALUE_KEY, mEnumValue);
+ out.putBundlable(BUNDLABLE_VALUE_KEY, mBundableValue);
+ out.putBundlableList(BUNDLABLE_LIST_VALUE_KEY, mListValue);
+ // new required value
+ out.putString(NEW_VALUE_KEY, mNewValue);
+ }
+
+ @Override
+ public void fromBundle(@NonNull BundleMarshaller in) {
+ mIntValue = in.getInt(INT_VALUE_KEY);
+ // mStringValue = in.getString(STRING_VALUE_KEY) is now deprecated.
+ mEnumValue = in.getEnum(ENUM_VALUE_KEY, TestEnum.class);
+ mBundableValue = in.getBundlable(BUNDLABLE_VALUE_KEY, mBundableValue,
+ TestBundlableNewVersion::new);
+ mListValue = in.getBundlableList(BUNDLABLE_LIST_VALUE_KEY, mListValue,
+ TestBundlableNewVersion::new);
+ // new required value with a mandatory default
+ mNewValue = in.getStringNonNull(NEW_VALUE_KEY, DEFAULT_NEW_VALUE);
+ }
+
+ public TestBundlableNewVersion setInt(int value) {
+ mIntValue = value;
+ return this;
+ }
+
+ public TestBundlableNewVersion setEnumValue(TestEnum value) {
+ mEnumValue = value;
+ return this;
+ }
+
+ public TestBundlableNewVersion setBundlableValue(TestBundlableNewVersion value) {
+ mBundableValue = value;
+ return this;
+ }
+
+ public TestBundlableNewVersion setListValue(List<TestBundlableNewVersion> value) {
+ mListValue = value;
+ return this;
+ }
+
+ public TestBundlableNewVersion setNewValue(@NonNull String value) {
+ Preconditions.checkNotNull(value);
+ mNewValue = value;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ TestBundlableNewVersion that = (TestBundlableNewVersion) o;
+ return mIntValue == that.mIntValue
+ && mEnumValue == that.mEnumValue
+ && Objects.equals(mBundableValue, that.mBundableValue)
+ && Objects.equals(mListValue, that.mListValue)
+ && Objects.equals(mNewValue, that.mNewValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIntValue, mEnumValue, mBundableValue, mListValue, mNewValue);
+ }
+}
diff --git a/car/src/main/java/androidx/car/navigation/utils/Bundlable.java b/car/src/main/java/androidx/car/navigation/utils/Bundlable.java
new file mode 100644
index 00000000000..65e8bf20cc7
--- /dev/null
+++ b/car/src/main/java/androidx/car/navigation/utils/Bundlable.java
@@ -0,0 +1,99 @@
+/*
+ * 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.car.navigation.utils;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Interface for classes whose instances can be written to and restored from a {@link Bundle}.
+ * Classes implementing the {@link Bundlable} interface must also provide a public default
+ * constructor, or a constructor that accepts {@link BundleMarshaller} as its only parameter.
+ * <p>
+ * This serialization protocol is designed to:
+ * <ul>
+ * <li>provide backward/forward compatibility between producers and consumers (following the
+ * Protocol Buffers pattern).
+ * <li>minimize the number of objects being allocated during both serialization and deserialization.
+ * </ul>
+ * <p>
+ * Implementations of this interface should comply to the following rules:
+ * <ul>
+ * <li>Fields should be serialized and deserialized using {@link BundleMarshaller} "put" and
+ * "get" methods.
+ * <li>Marshalling keys must be lower camel case alphanumerical identifiers (i.e.: "distanceUnit").
+ * (symbols such as "." and "_" are reserved by the system).
+ * <li>Field types should not be modified between versions of {@link Bundlable} objects to provide
+ * backward and forward compatibility. Only deprecations and additions are allowed.
+ * <li>When a field is deprecated, its marshalling key shouldn't be reused by any new field.
+ * <li>Enums are marshalled using {@link Enum#name()}. Because of this, enum values should not be
+ * renamed. Because enum values could be added or deprecated, clients must be prepared to accept
+ * null or a default value in case the server sends a value it doesn't know.
+ * <li>Fields annotated with {@link androidx.annotation.NonNull} should not be deprecated
+ * (as clients might not be prepared for their absence). Implementations of this interface should
+ * enforce this constraint, i.e. by initializing these fields at class instantiation, and using
+ * {@link androidx.core.util.Preconditions#checkNotNull(Object)} to prevent null values. If a new
+ * {@link androidx.annotation.NonNull} field is added on an existing {@link Bundlable}, the
+ * deserialization must provide a default value for it (as existing services won't provide values
+ * for it until they are updated).
+ * </ul>
+ * The following is an example of the suggested implementation:
+ * <pre>
+ * public class MyClass implements Bundlable {
+ * private static final String FOO_VALUE_KEY = "fooValue";
+ * private static final String BAR_VALUE_KEY = "barValue";
+ *
+ * public enum MyEnum {
+ * VALUE_1,
+ * VALUE_2
+ * }
+ *
+ * public String mFooValue;
+ * public MyEnum mBarValue;
+ *
+ * &#064;Override
+ * public void toBundle(@NonNull BundleMarshaller out) {
+ * out.putString(FOO_VALUE_KEY, mFooValue);
+ * out.putEnum(BAR_VALUE_KEY, mBarValue);
+ * }
+ *
+ * &#064;Override
+ * public void fromBundle(@NonNull BundleMarshaller in) {
+ * mFooValue = in.getString(FOO_VALUE_KEY);
+ * mBarValue = in.getEnum(BAR_VALUE_KEY, MyEnum.class);
+ * }
+ * }
+ * </pre>
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface Bundlable {
+ /**
+ * Serializes this object into a {@link BundleMarshaller} by writing all its fields to it.
+ */
+ void toBundle(@NonNull BundleMarshaller out);
+
+ /**
+ * Deserializes this object from a {@link BundleMarshaller} by reading all its fields from it.
+ */
+ void fromBundle(@NonNull BundleMarshaller in);
+}
diff --git a/car/src/main/java/androidx/car/navigation/utils/BundleMarshaller.java b/car/src/main/java/androidx/car/navigation/utils/BundleMarshaller.java
new file mode 100644
index 00000000000..1e6178a39d2
--- /dev/null
+++ b/car/src/main/java/androidx/car/navigation/utils/BundleMarshaller.java
@@ -0,0 +1,514 @@
+/*
+ * 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.car.navigation.utils;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Class responsible for serializing and deserializing data into a {@link Bundle}. It also
+ * provides a way to detect what items in the {@link Bundle} have been modified during
+ * marshalling.
+ * <p>
+ * A single {@link BundleMarshaller} can be re-used to serialize or deserialize data multiple times.
+ * Similarity, deserialization can be done in-place, updating existing {@link Bundlable}s. This
+ * reduces the number of instances being allocated.
+ * <p>
+ * When serializing, use {@link #resetBundle()} before marshalling and {@link #getBundle()} to
+ * obtain an snap-shot of the serialized content. Or use {@link #resetDelta()} and
+ * {@link #getDelta()} to obtain a {@link Bundle} representing the patch between the last and
+ * the new serialized data.
+ * <p>
+ * When deserializing, use {@link #setBundle(Bundle)} to deserialize a {@link Bundle} containing an
+ * snap-shot, or {@link #applyDelta(Bundle)} to process a patch from the last deserialized data.
+ * <p>
+ * Keys used in the "get" and "put" methods must be lower camel case alphanumerical identifiers
+ * (e.g.: "distanceUnit"). Symbols like "." and "_" are reserved by the system.
+ * <p>
+ * When deserializing {@link List} objects, this class assumes that they implement random access
+ * (e.g. {@link ArrayList}), or they are relatively small (see more details at
+ * {@link #trimList(List, int)})
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class BundleMarshaller {
+ /**
+ * Separator used to concatenate identifiers when marshalling non-primitive types (e.g. lists
+ * or {@link Bundlable}s).
+ */
+ private static final String KEY_SEPARATOR = ".";
+ /**
+ * Identifier used to record if a given non-primitive field is null or not. This allows
+ * serializing null objects without the need of using reflection or static methods.
+ */
+ private static final String IS_NULL_KEY = "_isNull";
+ /**
+ * Identifier used to record the length of a collection. This allows serializing changes to the
+ * length of a collection without having to remove elements or having to iterate over every
+ * possible collection key.
+ */
+ private static final String SIZE_KEY = "_size";
+ /**
+ * Special value for {@link #SIZE_KEY} to serialize a null collection.
+ */
+ private static final int NULL_SIZE = -1;
+
+ private Bundle mBundle = new Bundle();
+ private String mKeyPrefix = "";
+ private final Bundle mBundleDelta = new Bundle();
+
+ /**
+ * Returns data serialized since the last time this instance was constructed, or
+ * {@link #resetBundle()} was called.
+ */
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Resets this {@link BundleMarshaller} causing {@link #getBundle()} to return an empty
+ * {@link Bundle} until the next marshalling is executed. This can be used occasionally to
+ * remove unused keys in the {@link Bundle}.
+ */
+ public void resetBundle() {
+ mBundle.clear();
+ }
+
+ /**
+ * Replaces the {@link Bundle} to serialize into or deserialize from.
+ */
+ public void setBundle(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets a {@link Bundle} containing only the entries of {@link #getBundle()} that were modified
+ * since this instance was constructed, or {@link #resetDelta()} was called.
+ */
+ public Bundle getDelta() {
+ return mBundleDelta;
+ }
+
+ /**
+ * Merges the provided {@link Bundle} on top of the one stored in this {@link BundleMarshaller}.
+ *
+ * @param delta a {@link Bundle} containing entries to be updated on one stored in this
+ * {@link BundleMarshaller} instance. Such {@link Bundle} can be produced by
+ * using the {@link #resetDelta()} and {@link #getDelta()} methods during data
+ * serialization.
+ */
+ public void applyDelta(Bundle delta) {
+ mBundle.putAll(delta);
+ }
+
+ /**
+ * Resets tracking of modified entries, causing {@link #getDelta()} to return an empty
+ * {@link Bundle} until the next marshalling is executed. This can be used between
+ * serializations make {@link #getDelta()} return only the differences.
+ */
+ public void resetDelta() {
+ mBundleDelta.clear();
+ }
+
+ /**
+ * Inserts an int value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value an int
+ */
+ public void putInt(@NonNull String key, int value) {
+ String mangledKey = getMangledKey(key);
+ if (!mBundle.containsKey(mangledKey) || mBundle.getInt(mangledKey) != value) {
+ mBundleDelta.putInt(mangledKey, value);
+ mBundle.putInt(mangledKey, value);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0 if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @return an int
+ */
+ public int getInt(@NonNull String key) {
+ return mBundle.getInt(getMangledKey(key));
+ }
+
+ /**
+ * Inserts a float value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value a float
+ */
+ public void putFloat(@NonNull String key, float value) {
+ String mangledKey = getMangledKey(key);
+ if (!mBundle.containsKey(mangledKey)
+ || Float.compare(mBundle.getFloat(mangledKey), value) != 0) {
+ mBundleDelta.putFloat(mangledKey, value);
+ mBundle.putFloat(mangledKey, value);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0.0f if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @return a float
+ */
+ public float getFloat(@NonNull String key) {
+ return mBundle.getFloat(getMangledKey(key));
+ }
+
+ /**
+ * Inserts a double value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value a double
+ */
+ public void putDouble(@NonNull String key, double value) {
+ String mangledKey = getMangledKey(key);
+ if (!mBundle.containsKey(mangledKey)
+ || Double.compare(mBundle.getDouble(mangledKey), value) != 0) {
+ mBundleDelta.putDouble(mangledKey, value);
+ mBundle.putDouble(mangledKey, value);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0.0 if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @return a double
+ */
+ public double getDouble(@NonNull String key) {
+ return mBundle.getDouble(getMangledKey(key));
+ }
+
+ /**
+ * Inserts a boolean value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value a boolean
+ */
+ public void putBoolean(@NonNull String key, boolean value) {
+ String mangledKey = getMangledKey(key);
+ if (!mBundle.containsKey(mangledKey) || mBundle.getBoolean(mangledKey) != value) {
+ mBundleDelta.putBoolean(mangledKey, value);
+ mBundle.putBoolean(mangledKey, value);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or false if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @return a boolean
+ */
+ public boolean getBoolean(@NonNull String key) {
+ return mBundle.getBoolean(getMangledKey(key));
+ }
+
+ /**
+ * Inserts a string value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value a string, or null
+ */
+ public void putString(@NonNull String key, @Nullable String value) {
+ String mangledKey = getMangledKey(key);
+ if (!mBundle.containsKey(mangledKey)
+ || !Objects.equals(mBundle.getString(mangledKey), value)) {
+ mBundleDelta.putString(mangledKey, value);
+ mBundle.putString(mangledKey, value);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @return a string, or null
+ */
+ @Nullable
+ public String getString(@NonNull String key) {
+ return mBundle.getString(getMangledKey(key));
+ }
+
+ /**
+ * Returns the value associated with the given key, or the provided default value if no mapping
+ * of the desired type exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param defaultValue value to return if key does not exist or if a null value is associated
+ * with the given key.
+ * @return a string
+ */
+ @NonNull
+ public String getStringNonNull(@NonNull String key, @NonNull String defaultValue) {
+ return mBundle.getString(getMangledKey(key), defaultValue);
+ }
+
+ /**
+ * Inserts an enum value, replacing any existing value for the given key. The provided enum
+ * will be serialized as a string using {@link Enum#name()}.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value an enum, or null
+ */
+ public <T extends Enum<T>> void putEnum(@NonNull String key, @Nullable T value) {
+ putString(key, value != null ? value.name() : null);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of the desired type
+ * exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param clazz {@link Enum} class to be used to deserialize the value.
+ * @param <T> {@link Enum} type to be returned.
+ * @return an enum, or null
+ */
+ @Nullable
+ public <T extends Enum<T>> T getEnum(@NonNull String key, @NonNull Class<T> clazz) {
+ String name = getString(key);
+ try {
+ return name != null ? Enum.valueOf(clazz, name) : null;
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or the provided default value if no mapping
+ * of the desired type exists for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param clazz {@link Enum} class to be used to deserialize the value.
+ * @param defaultValue value to return if key does not exist or if a null value is associated
+ * with the given key.
+ * @param <T> {@link Enum} type to be returned.
+ * @return an enum
+ */
+ @NonNull
+ public <T extends Enum<T>> T getEnumNonNull(@NonNull String key, @NonNull Class<T> clazz,
+ @NonNull T defaultValue) {
+ T result = getEnum(key, clazz);
+ return result != null ? result : defaultValue;
+ }
+
+ /**
+ * Inserts a {@link Bundlable} value, replacing any existing value for the given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param value a {@link Bundlable}, or null
+ */
+ public <T extends Bundlable> void putBundlable(@NonNull String key, @Nullable T value) {
+ withKeyPrefix(key, () -> {
+ putBoolean(IS_NULL_KEY, value == null);
+ if (value != null) {
+ value.toBundle(this);
+ }
+ });
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of the desired type
+ * exists for the given key. If a non-null "current" instance is provided, then the
+ * deserialization would be done in place. Otherwise, a new instance will be created using the
+ * provided factory.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param current current value (if available) to perform in-place deserialization, or null
+ * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
+ * type T. The suggested implementation is to pass a reference to the default
+ * constructor of that class.
+ * @param <T> {@link Bundlable} type to be returned.
+ * @return an instance of type T, or null
+ */
+ @Nullable
+ public <T extends Bundlable> T getBundlable(@NonNull String key, @Nullable T current,
+ @NonNull Supplier<T> factory) {
+ return withKeyPrefix(key, () -> {
+ if (getBoolean(IS_NULL_KEY)) {
+ return null;
+ }
+ T result = current != null ? current : factory.get();
+ result.fromBundle(this);
+ return result;
+ });
+ }
+
+ /**
+ * Returns the value associated with the given key, or a default value if no mapping of the
+ * desired type exists for the given key. If a non-null value is available, then such value
+ * will be deserialized in-place on the given "current" instance. Otherwise, a default value
+ * will be generated using the provided factory.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param current current value to perform in-place deserialization
+ * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
+ * type T. The suggested implementation is to pass a reference to the default
+ * constructor of that class.
+ * @param <T> {@link Bundlable} type to be returned.
+ * @return an instance of type T
+ */
+ @NonNull
+ public <T extends Bundlable> T getBundlableNonNull(@NonNull String key, @NonNull T current,
+ @NonNull Supplier<T> factory) {
+ T result = getBundlable(key, current, factory);
+ return result != null ? result : factory.get();
+ }
+
+ /**
+ * Inserts a {@link List} of {@link Bundlable} values, replacing any existing value for the
+ * given key.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param values a {@link List} of {@link Bundlable} values, or null
+ */
+ public <T extends Bundlable> void putBundlableList(@NonNull String key,
+ @Nullable List<T> values) {
+ withKeyPrefix(key, () -> {
+ putInt(SIZE_KEY, values != null ? values.size() : NULL_SIZE);
+ if (values != null) {
+ int pos = 0;
+ // Using for-each as the provided list might not implement random access (e.g. it
+ // might be a linked list).
+ for (T value : values) {
+ putBundlable(String.valueOf(pos), value);
+ pos++;
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of the desired type
+ * exists for the given key. If a non-null "current" list is provided, then the deserialization
+ * would be done in place. Otherwise, a new list will be created and items will be instantiated
+ * using the provided factory.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param current current value (if available) to perform in-place deserialization, or null
+ * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
+ * type T. The suggested implementation is to pass a reference to the default
+ * constructor of that class.
+ * @param <T> {@link Bundlable} type to be returned.
+ * @return a list of instances of type T, or null. The resulting list might contain null
+ * elements.
+ */
+ @Nullable
+ public <T extends Bundlable> List<T> getBundlableList(@NonNull String key,
+ @Nullable List<T> current, @NonNull Supplier<T> factory) {
+ return withKeyPrefix(key, () -> {
+ int listSize = getInt(SIZE_KEY);
+ if (listSize == NULL_SIZE) {
+ return null;
+ }
+ List<T> result = current != null ? current : new ArrayList<>(listSize);
+ if (result.size() > listSize) {
+ result.subList(listSize, result.size()).clear();
+ }
+ for (int pos = 0; pos < listSize; pos++) {
+ String subKey = String.valueOf(pos);
+ if (pos < result.size()) {
+ result.set(pos, getBundlable(subKey, result.get(pos), factory));
+ } else {
+ result.add(getBundlable(String.valueOf(pos),
+ null /* force the creation of a new instance */,
+ factory));
+ }
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Returns the value associated with the given key, or an empty list if no mapping of the
+ * desired type exists for the given key. If a non-null "current" list is provided, then the
+ * deserialization would be done in place. Otherwise, a new list will be created and items will
+ * be instantiated using the provided factory.
+ *
+ * @param key lower camel case alphanumerical identifier
+ * @param current current value (if available) to perform in-place deserialization, or null
+ * @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
+ * type T. The suggested implementation is to pass a reference to the default
+ * constructor of that class.
+ * @param <T> {@link Bundlable} type to be returned.
+ * @return a list of instances of type T, or an empty list. The resulting list might contain
+ * null elements.
+ */
+ @NonNull
+ public <T extends Bundlable> List<T> getBundlableListNonNull(@NonNull String key,
+ @NonNull List<T> current, @NonNull Supplier<T> factory) {
+ List<T> result = getBundlableList(key, current, factory);
+ return result != null ? result : new ArrayList<>();
+ }
+
+ /**
+ * Executes the given {@link Runnable} in a context where {@link #getMangledKey(String)}
+ * includes the given key as part of the prefix. Calls to this method can be nested (the
+ * provided {@link Runnable} can call to this method if needed). This method should be used when
+ * serializing or deserializing nested objects.
+ * <p>
+ * For example: calling to {@link #withKeyPrefix(String, Runnable)} with "foo" as key and
+ * a {@link Runnable} that calls {@link #getMangledKey(String)} with "bar" as key, will
+ * cause such {@link #getMangledKey(String)} call to return "foo.bar".
+ */
+ private void withKeyPrefix(@NonNull String key, @NonNull Runnable runnable) {
+ String originalKeyPrefix = mKeyPrefix;
+ mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
+ runnable.run();
+ mKeyPrefix = originalKeyPrefix;
+ }
+
+ /**
+ * Similar to {@link #withKeyPrefix(String, Runnable)} but allows returning a value.
+ */
+ private <X> X withKeyPrefix(@NonNull String key, @NonNull Supplier<X> supplier) {
+ String originalKeyPrefix = mKeyPrefix;
+ mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
+ X res = supplier.get();
+ mKeyPrefix = originalKeyPrefix;
+ return res;
+ }
+
+ /**
+ * Returns a composed key based on the given one and the current serialization/deserialization
+ * key prefix (initially empty). This prefix can be temporarily changed with
+ * {@link #withKeyPrefix(String, Runnable)} or {@link #withKeyPrefix(String, Supplier)}.
+ */
+ private String getMangledKey(@NonNull String key) {
+ return mKeyPrefix + key;
+ }
+}
diff --git a/jetifier/jetifier/source-transformer/rewriteMake.py b/jetifier/jetifier/source-transformer/rewriteMake.py
index b33d4c1d469..a8ee1d702b4 100755
--- a/jetifier/jetifier/source-transformer/rewriteMake.py
+++ b/jetifier/jetifier/source-transformer/rewriteMake.py
@@ -88,6 +88,7 @@ android-arch-room-common,androidx.room_room-common
android-arch-room-migration,androidx.room_room-migration
android-arch-room-runtime,androidx.room_room-runtime
android-arch-room-testing,androidx.room_room-testing
+android-support-design,com.google.android.material_material
$(ANDROID_SUPPORT_DESIGN_TARGETS),com.google.android.material_material"""
reader = csv.reader(target_map.split('\n'), delimiter=',')
diff --git a/leanback/src/main/res/values-bn/strings.xml b/leanback/src/main/res/values-bn/strings.xml
index c7c7bedec43..a7ae625463e 100644
--- a/leanback/src/main/res/values-bn/strings.xml
+++ b/leanback/src/main/res/values-bn/strings.xml
@@ -18,7 +18,7 @@ limitations under the License.
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="lb_navigation_menu_contentDescription" msgid="8126335323963415494">"নেভিগেশন মেনু"</string>
- <string name="orb_search_action" msgid="7534843523462177008">"খোঁজার কার্যকলাপ"</string>
+ <string name="orb_search_action" msgid="7534843523462177008">"খোঁজার অ্যাক্টিভিটি"</string>
<string name="lb_search_bar_hint" msgid="4819380969103509861">"সার্চ"</string>
<string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"বলার মাধ্যমে খুঁজুন"</string>
<string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> খুঁজুন"</string>
@@ -33,7 +33,7 @@ limitations under the License.
<string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"%1$dX স্পিডে পিছিয়ে যান"</string>
<string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"সরাসরি পরেরটিতে চলে যান"</string>
<string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"সরাসরি আগেরটিতে চলে যান"</string>
- <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"আরও কার্যকলাপ"</string>
+ <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"আরও অ্যাক্টিভিটি"</string>
<string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"উপরের দিকে করা বুড়ো আঙ্গুলের চিহ্নকে বাদ দিন"</string>
<string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"উপরের দিকে করা বুড়ো আঙ্গুলের চিহ্নকে বেছে নিন"</string>
<string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"নিচের দিকে করা বুড়ো আঙ্গুলের চিহ্নকে বাদ দিন"</string>
diff --git a/samples/SupportPreferenceDemos/build.gradle b/samples/SupportPreferenceDemos/build.gradle
index d37bfa20cca..6aa83437417 100644
--- a/samples/SupportPreferenceDemos/build.gradle
+++ b/samples/SupportPreferenceDemos/build.gradle
@@ -8,4 +8,5 @@ dependencies {
implementation(project(":preference"))
implementation(project(":leanback"))
implementation(project(":leanback-preference"))
+ implementation(project(":car"))
}
diff --git a/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml b/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
index 0e9aa9b784a..dc7f3dc1e67 100644
--- a/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
@@ -20,7 +20,7 @@
package="com.example.android.supportpreference">
<uses-sdk
- tools:overrideLibrary="androidx.leanback.preference, androidx.leanback" />
+ tools:overrideLibrary="androidx.leanback.preference, androidx.leanback, androidx.car" />
<uses-feature android:name="android.software.Leanback" android:required="false" />
@@ -68,5 +68,14 @@
</intent-filter>
</activity>
+ <activity android:name=".FragmentSupportPreferencesCar"
+ android:label="@string/fragment_support_preferences_car_demo"
+ android:theme="@style/SupportPreferenceCar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="com.example.android.supportpreference.SAMPLE_CODE"/>
+ </intent-filter>
+ </activity>
+
</application>
-</manifest>
+</manifest> \ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCar.java b/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCar.java
new file mode 100644
index 00000000000..83cb38459d5
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCar.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 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 com.example.android.supportpreference;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+/**
+ * Demonstration of PreferenceFragment, showing a single fragment in an
+ * activity for the Car.
+ */
+public class FragmentSupportPreferencesCar extends AppCompatActivity
+ implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
+ new PrefsFragment()).commitNow();
+ }
+ }
+
+ @Override
+ public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
+ Fragment fragment = new PrefsFragment();
+ Bundle args = new Bundle();
+ args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.getKey());
+ fragment.setArguments(args);
+ getSupportFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .commitNow();
+ return true;
+ }
+
+ /**
+ * Create a PrefsFragment from the xml file of preferences
+ */
+ public static class PrefsFragment extends PreferenceFragmentCompat {
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ // Load the preferences from an XML resource
+ setPreferencesFromResource(R.xml.preferences, rootKey);
+ }
+ }
+}
+
diff --git a/samples/SupportPreferenceDemos/src/main/res/values/strings.xml b/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
index e6effdf3e7e..a3a81b19169 100644
--- a/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
+++ b/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
@@ -19,6 +19,7 @@
<string name="fragment_support_preferences_demo">Support PreferenceFragment</string>
<string name="fragment_support_preferences_compat_demo">Support PreferenceFragmentCompat</string>
<string name="fragment_support_preferences_leanback_demo">Support LeanbackPreferenceFragment</string>
+ <string name="fragment_support_preferences_car_demo">Support Car PreferenceFragment</string>
<string name="root_title">Demo Preferences</string>
diff --git a/samples/SupportPreferenceDemos/src/main/res/values/styles.xml b/samples/SupportPreferenceDemos/src/main/res/values/styles.xml
index 5194c9ff266..6b2837817c7 100644
--- a/samples/SupportPreferenceDemos/src/main/res/values/styles.xml
+++ b/samples/SupportPreferenceDemos/src/main/res/values/styles.xml
@@ -31,4 +31,9 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Leanback</item>
</style>
+ <style name="SupportPreferenceCar" parent="Theme.Car.NoActionBar">
+ <item name="preferenceTheme">@style/PreferenceThemeOverlayCar</item>
+ <item name="android:windowBackground">@color/car_card</item>
+ </style>
+
</resources>
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
index 4d1dca7105d..2f492023f00 100644
--- a/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
@@ -57,6 +57,13 @@
android:entries="@array/entries_list_preference"
android:entryValues="@array/entryvalues_list_preference" />
+ <SeekBarPreference
+ android:key="seekbar_preference"
+ android:title="Seekbar preference"
+ android:summary="This is a seekbar preference"
+ android:max="10"
+ android:defaultValue="5"/>
+
</PreferenceCategory>
<PreferenceCategory
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 7c51a4b1f84..1b84be5b5c8 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -37,8 +37,8 @@ dependencies {
implementation project(':work:work-runtime')
implementation project(':work:work-firebase')
implementation "android.arch.lifecycle:extensions:1.1.0"
- implementation "android.arch.persistence.room:runtime:1.1.0"
- annotationProcessor "android.arch.persistence.room:compiler:1.1.0"
+ implementation "android.arch.persistence.room:runtime:1.1.1-rc1"
+ annotationProcessor "android.arch.persistence.room:compiler:1.1.1-rc1"
implementation MULTIDEX
implementation "com.android.support:recyclerview-v7:26.1.0"
implementation "com.android.support:appcompat-v7:26.1.0"
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
index 37cd7fc8291..9f117ec8411 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
@@ -45,12 +45,16 @@ public class ToastWorker extends Worker {
@Override
public @NonNull Result doWork() {
Data input = getInputData();
- final String message = input.getString(ARG_MESSAGE, "completed!");
+ String message = input.getString(ARG_MESSAGE);
+ if (message == null) {
+ message = "completed!";
+ }
+ final String displayMessage = message;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
- Log.d("ToastWorker", message);
- Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+ Log.d("ToastWorker", displayMessage);
+ Toast.makeText(getApplicationContext(), displayMessage, Toast.LENGTH_SHORT).show();
}
});
return Result.SUCCESS;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
index 0fa1c5b1dbb..e28d94215c6 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
@@ -47,7 +47,7 @@ public class ImageProcessingWorker extends Worker {
public @NonNull Result doWork() {
Log.d(TAG, "Started");
- String uriString = getInputData().getString(URI_KEY, null);
+ String uriString = getInputData().getString(URI_KEY);
if (TextUtils.isEmpty(uriString)) {
Log.e(TAG, "Invalid URI!");
return Result.FAILURE;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
index 304e7fee852..1d3520ff50d 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
@@ -37,7 +37,7 @@ public class ImageSetupWorker extends Worker {
public @NonNull Result doWork() {
Log.d(TAG, "Started");
- String uriString = getInputData().getString(URI_KEY, null);
+ String uriString = getInputData().getString(URI_KEY);
if (TextUtils.isEmpty(uriString)) {
Log.e(TAG, "Invalid URI!");
return Result.FAILURE;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
index fcc754061e7..2fca4432b48 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
@@ -57,7 +57,7 @@ public class TextMappingWorker extends Worker {
@Override
public @NonNull Result doWork() {
Data input = getInputData();
- String inputFileName = input.getString(INPUT_FILE, null);
+ String inputFileName = input.getString(INPUT_FILE);
String outputFileName = "out_" + inputFileName;
AssetManager assetManager = getApplicationContext().getAssets();
diff --git a/work/workmanager-ktx/src/androidTest/java/androidx/work/DataTest.kt b/work/workmanager-ktx/src/androidTest/java/androidx/work/DataTest.kt
index 20b19f6f3bf..0cd1eda741c 100644
--- a/work/workmanager-ktx/src/androidTest/java/androidx/work/DataTest.kt
+++ b/work/workmanager-ktx/src/androidTest/java/androidx/work/DataTest.kt
@@ -32,10 +32,10 @@ class DataTest {
val data = map.toWorkData()
assertEquals(data.getInt("one", 0), 1)
assertEquals(data.getLong("two", 0L), 2L)
- assertEquals(data.getString("three", null), "Three")
+ assertEquals(data.getString("three"), "Three")
val longArray = data.getLongArray("four")
assertNotNull(longArray)
- assertEquals(longArray.size, 2)
+ assertEquals(longArray!!.size, 2)
assertEquals(longArray[0], 1L)
assertEquals(longArray[1], 2L)
}
diff --git a/work/workmanager/build.gradle b/work/workmanager/build.gradle
index fedc05213bc..172c5ffcf3c 100644
--- a/work/workmanager/build.gradle
+++ b/work/workmanager/build.gradle
@@ -43,10 +43,10 @@ android {
dependencies {
api "android.arch.lifecycle:extensions:1.1.0"
- implementation "android.arch.persistence.room:runtime:1.1.0"
- annotationProcessor "android.arch.persistence.room:compiler:1.1.0"
+ implementation "android.arch.persistence.room:runtime:1.1.1-rc1"
+ annotationProcessor "android.arch.persistence.room:compiler:1.1.1-rc1"
androidTestImplementation "android.arch.core:core-testing:1.1.0"
- androidTestImplementation "android.arch.persistence.room:testing:1.1.0"
+ androidTestImplementation "android.arch.persistence.room:testing:1.1.1-rc1"
androidTestImplementation(TEST_RUNNER)
androidTestImplementation(ESPRESSO_CORE)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has its own MockMaker
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index c1311173b1c..0cba3cf0c6c 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -18,6 +18,12 @@ package androidx.work;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL;
+import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_1;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_4;
+
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -25,16 +31,21 @@ import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
import android.arch.persistence.room.testing.MigrationTestHelper;
import android.content.ContentValues;
+import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
+import android.os.Build;
+import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkDatabaseMigrations;
+import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkTypeConverters;
+import androidx.work.impl.utils.Preferences;
import androidx.work.worker.TestWorker;
import org.junit.Before;
@@ -51,8 +62,6 @@ public class WorkDatabaseMigrationTest {
private static final String TEST_DATABASE = "workdatabase-test";
private static final boolean VALIDATE_DROPPED_TABLES = true;
- private static final int OLD_VERSION = 1;
- private static final int NEW_VERSION = 2;
private static final String COLUMN_WORKSPEC_ID = "work_spec_id";
private static final String COLUMN_SYSTEM_ID = "system_id";
private static final String COLUMN_ALARM_ID = "alarm_id";
@@ -69,6 +78,7 @@ public class WorkDatabaseMigrationTest {
private static final String TABLE_WORKTAG = "WorkTag";
private static final String TABLE_WORKNAME = "WorkName";
+ private Context mContext;
private File mDatabasePath;
@Rule
@@ -80,6 +90,7 @@ public class WorkDatabaseMigrationTest {
@Before
public void setUp() {
// Delete the database if it exists.
+ mContext = InstrumentationRegistry.getTargetContext();
mDatabasePath = InstrumentationRegistry.getContext().getDatabasePath(TEST_DATABASE);
if (mDatabasePath.exists()) {
mDatabasePath.delete();
@@ -90,34 +101,23 @@ public class WorkDatabaseMigrationTest {
@MediumTest
public void testMigrationVersion1To2() throws IOException {
SupportSQLiteDatabase database =
- mMigrationTestHelper.createDatabase(TEST_DATABASE, OLD_VERSION);
-
- String workSpecId0 = UUID.randomUUID().toString();
- ContentValues contentValues = new ContentValues();
- contentValues.put("id", workSpecId0);
- contentValues.put("state", WorkTypeConverters.StateIds.ENQUEUED);
- contentValues.put("worker_class_name", TestWorker.class.getName());
- contentValues.put("input_merger_class_name", OverwritingInputMerger.class.getName());
- contentValues.put("input", Data.toByteArray(Data.EMPTY));
- contentValues.put("output", Data.toByteArray(Data.EMPTY));
- contentValues.put("initial_delay", 0L);
- contentValues.put("interval_duration", 0L);
- contentValues.put("flex_duration", 0L);
- contentValues.put("required_network_type", false);
- contentValues.put("requires_charging", false);
- contentValues.put("requires_device_idle", false);
- contentValues.put("requires_battery_not_low", false);
- contentValues.put("requires_storage_not_low", false);
- contentValues.put("content_uri_triggers",
- WorkTypeConverters.contentUriTriggersToByteArray(new ContentUriTriggers()));
- contentValues.put("run_attempt_count", 0);
- contentValues.put("backoff_policy",
- WorkTypeConverters.backoffPolicyToInt(BackoffPolicy.EXPONENTIAL));
- contentValues.put("backoff_delay_duration", WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS);
- contentValues.put("period_start_time", 0L);
- contentValues.put("minimum_retention_duration", 0L);
- contentValues.put("schedule_requested_at", WorkSpec.SCHEDULE_NOT_REQUESTED_YET);
- database.insert("workspec", CONFLICT_FAIL, contentValues);
+ mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_1);
+
+ String[] prepopulatedWorkSpecIds = new String[]{
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString()
+ };
+ for (String workSpecId : prepopulatedWorkSpecIds) {
+ ContentValues contentValues = contentValues(workSpecId);
+ database.insert("workspec", CONFLICT_FAIL, contentValues);
+
+ if (workSpecId.equals(prepopulatedWorkSpecIds[0])) {
+ ContentValues tagValues = new ContentValues();
+ tagValues.put("tag", TestWorker.class.getName());
+ tagValues.put("work_spec_id", workSpecId);
+ database.insert("worktag", CONFLICT_FAIL, tagValues);
+ }
+ }
String workSpecId1 = UUID.randomUUID().toString();
String workSpecId2 = UUID.randomUUID().toString();
@@ -130,16 +130,28 @@ public class WorkDatabaseMigrationTest {
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
- NEW_VERSION,
+ VERSION_2,
VALIDATE_DROPPED_TABLES,
WorkDatabaseMigrations.MIGRATION_1_2);
Cursor tagCursor = database.query("SELECT * FROM worktag");
- assertThat(tagCursor.getCount(), is(1));
- tagCursor.moveToFirst();
- assertThat(tagCursor.getString(tagCursor.getColumnIndex("tag")),
- is(TestWorker.class.getName()));
- assertThat(tagCursor.getString(tagCursor.getColumnIndex("work_spec_id")), is(workSpecId0));
+ assertThat(tagCursor.getCount(), is(prepopulatedWorkSpecIds.length));
+ boolean[] foundWorkSpecId = new boolean[prepopulatedWorkSpecIds.length];
+ for (int i = 0; i < prepopulatedWorkSpecIds.length; ++i) {
+ tagCursor.moveToPosition(i);
+ assertThat(tagCursor.getString(tagCursor.getColumnIndex("tag")),
+ is(TestWorker.class.getName()));
+ String currentId = tagCursor.getString(tagCursor.getColumnIndex("work_spec_id"));
+ for (int j = 0; j < prepopulatedWorkSpecIds.length; ++j) {
+ if (prepopulatedWorkSpecIds[j].equals(currentId)) {
+ foundWorkSpecId[j] = true;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < prepopulatedWorkSpecIds.length; ++i) {
+ assertThat(foundWorkSpecId[i], is(true));
+ }
tagCursor.close();
Cursor cursor = database.query(CHECK_SYSTEM_ID_INFO);
@@ -161,89 +173,96 @@ public class WorkDatabaseMigrationTest {
@Test
@MediumTest
- public void testMigrationVersion2To1() throws IOException {
+ public void testMigrationVersion2To3() throws IOException {
SupportSQLiteDatabase database =
- mMigrationTestHelper.createDatabase(TEST_DATABASE, NEW_VERSION);
-
- String workSpecId1 = UUID.randomUUID().toString();
- String workSpecId2 = UUID.randomUUID().toString();
-
- // insert SystemIdInfo
- database.execSQL(INSERT_SYSTEM_ID_INFO, new Object[]{workSpecId1, 1});
- database.execSQL(INSERT_SYSTEM_ID_INFO, new Object[]{workSpecId2, 2});
-
- database.close();
-
+ mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_2);
+ WorkDatabaseMigrations.WorkMigration migration2To3 =
+ new WorkDatabaseMigrations.WorkMigration(mContext, VERSION_2, VERSION_3);
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
- OLD_VERSION,
+ VERSION_3,
VALIDATE_DROPPED_TABLES,
- WorkDatabaseMigrations.MIGRATION_2_1);
+ migration2To3);
- Cursor cursor = database.query(CHECK_ALARM_INFO);
- assertThat(cursor.getCount(), is(2));
- cursor.moveToFirst();
- assertThat(cursor.getString(cursor.getColumnIndex(COLUMN_WORKSPEC_ID)), is(workSpecId1));
- assertThat(cursor.getInt(cursor.getColumnIndex(COLUMN_ALARM_ID)), is(1));
- cursor.moveToNext();
- assertThat(cursor.getString(cursor.getColumnIndex(COLUMN_WORKSPEC_ID)), is(workSpecId2));
- assertThat(cursor.getInt(cursor.getColumnIndex(COLUMN_ALARM_ID)), is(2));
- cursor.close();
-
- assertThat(checkExists(database, TABLE_SYSTEM_ID_INFO), is(false));
- assertThat(checkExists(database, TABLE_WORKSPEC), is(true));
- assertThat(checkExists(database, TABLE_WORKTAG), is(true));
- assertThat(checkExists(database, TABLE_WORKNAME), is(true));
+ Preferences preferences = new Preferences(mContext);
+ assertThat(preferences.needsReschedule(), is(true));
database.close();
}
@Test
@MediumTest
- public void testMigrationVersion1To2To1() throws IOException {
+ public void testMigrationVersion3To4() throws IOException {
SupportSQLiteDatabase database =
- mMigrationTestHelper.createDatabase(TEST_DATABASE, OLD_VERSION);
+ mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_3);
- String workSpecId1 = UUID.randomUUID().toString();
- String workSpecId2 = UUID.randomUUID().toString();
+ String oneTimeWorkSpecId = UUID.randomUUID().toString();
+ long scheduleRequestedAt = System.currentTimeMillis();
+ ContentValues oneTimeWorkSpecContentValues = contentValues(oneTimeWorkSpecId);
+ oneTimeWorkSpecContentValues.put("schedule_requested_at", scheduleRequestedAt);
- // insert alarmInfos
- database.execSQL(INSERT_ALARM_INFO, new Object[]{workSpecId1, 1});
- database.execSQL(INSERT_ALARM_INFO, new Object[]{workSpecId2, 2});
+ String periodicWorkSpecId = UUID.randomUUID().toString();
+ ContentValues periodicWorkSpecContentValues = contentValues(periodicWorkSpecId);
+ periodicWorkSpecContentValues.put("interval_duration", 15 * 60 * 1000L);
- database.close();
+ database.insert("workspec", CONFLICT_FAIL, oneTimeWorkSpecContentValues);
+ database.insert("workspec", CONFLICT_FAIL, periodicWorkSpecContentValues);
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
- NEW_VERSION,
+ VERSION_4,
VALIDATE_DROPPED_TABLES,
- WorkDatabaseMigrations.MIGRATION_1_2);
-
- database.close();
+ MIGRATION_3_4);
- database = mMigrationTestHelper.runMigrationsAndValidate(
- TEST_DATABASE,
- OLD_VERSION,
- VALIDATE_DROPPED_TABLES,
- WorkDatabaseMigrations.MIGRATION_2_1);
-
- Cursor cursor = database.query(CHECK_ALARM_INFO);
+ Cursor cursor = database.query("SELECT * from workspec");
assertThat(cursor.getCount(), is(2));
cursor.moveToFirst();
- assertThat(cursor.getString(cursor.getColumnIndex(COLUMN_WORKSPEC_ID)), is(workSpecId1));
- assertThat(cursor.getInt(cursor.getColumnIndex(COLUMN_ALARM_ID)), is(1));
+ assertThat(cursor.getString(cursor.getColumnIndex("id")),
+ is(oneTimeWorkSpecId));
+ assertThat(cursor.getLong(cursor.getColumnIndex("schedule_requested_at")),
+ is(scheduleRequestedAt));
cursor.moveToNext();
- assertThat(cursor.getString(cursor.getColumnIndex(COLUMN_WORKSPEC_ID)), is(workSpecId2));
- assertThat(cursor.getInt(cursor.getColumnIndex(COLUMN_ALARM_ID)), is(2));
- cursor.close();
-
- assertThat(checkExists(database, TABLE_SYSTEM_ID_INFO), is(false));
- assertThat(checkExists(database, TABLE_WORKSPEC), is(true));
- assertThat(checkExists(database, TABLE_WORKTAG), is(true));
- assertThat(checkExists(database, TABLE_WORKNAME), is(true));
+ assertThat(cursor.getString(cursor.getColumnIndex("id")),
+ is(periodicWorkSpecId));
+ if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+ assertThat(cursor.getLong(cursor.getColumnIndex("schedule_requested_at")),
+ is(0L));
+ } else {
+ assertThat(cursor.getLong(cursor.getColumnIndex("schedule_requested_at")),
+ is(WorkSpec.SCHEDULE_NOT_REQUESTED_YET));
+ }
database.close();
}
+ @NonNull
+ private ContentValues contentValues(String workSpecId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put("id", workSpecId);
+ contentValues.put("state", WorkTypeConverters.StateIds.ENQUEUED);
+ contentValues.put("worker_class_name", TestWorker.class.getName());
+ contentValues.put("input_merger_class_name", OverwritingInputMerger.class.getName());
+ contentValues.put("input", Data.toByteArray(Data.EMPTY));
+ contentValues.put("output", Data.toByteArray(Data.EMPTY));
+ contentValues.put("initial_delay", 0L);
+ contentValues.put("interval_duration", 0L);
+ contentValues.put("flex_duration", 0L);
+ contentValues.put("required_network_type", false);
+ contentValues.put("requires_charging", false);
+ contentValues.put("requires_device_idle", false);
+ contentValues.put("requires_battery_not_low", false);
+ contentValues.put("requires_storage_not_low", false);
+ contentValues.put("content_uri_triggers",
+ WorkTypeConverters.contentUriTriggersToByteArray(new ContentUriTriggers()));
+ contentValues.put("run_attempt_count", 0);
+ contentValues.put("backoff_policy",
+ WorkTypeConverters.backoffPolicyToInt(BackoffPolicy.EXPONENTIAL));
+ contentValues.put("backoff_delay_duration", WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS);
+ contentValues.put("period_start_time", 0L);
+ contentValues.put("minimum_retention_duration", 0L);
+ contentValues.put("schedule_requested_at", WorkSpec.SCHEDULE_NOT_REQUESTED_YET);
+ return contentValues;
+ }
+
private boolean checkExists(SupportSQLiteDatabase database, String tableName) {
Cursor cursor = null;
try {
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index 72ebe944b00..23996eb2367 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -18,6 +18,7 @@ package androidx.work.impl;
import static androidx.work.worker.CheckLimitsWorker.KEY_EXCEEDS_SCHEDULER_LIMIT;
import static androidx.work.worker.CheckLimitsWorker.KEY_LIMIT_TO_ENFORCE;
+import static androidx.work.worker.CheckLimitsWorker.KEY_RECURSIVE;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -65,8 +66,7 @@ import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class WorkManagerImplLargeExecutorTest {
- private static final int NUM_WORKERS = 500;
- private static final int TEST_TIMEOUT_SECONDS = 30;
+ private static final int NUM_WORKERS = 200;
// ThreadPoolExecutor parameters.
private static final int MIN_POOL_SIZE = 0;
@@ -172,7 +172,66 @@ public class WorkManagerImplLargeExecutorTest {
});
continuation.enqueue();
- latch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ latch.await(120L, TimeUnit.SECONDS);
+ assertThat(latch.getCount(), is(0L));
+ }
+
+ @Test
+ @LargeTest
+ @SdkSuppress(maxSdkVersion = 22)
+ public void testSchedulerLimitsRecursive() throws InterruptedException {
+ List<OneTimeWorkRequest> workRequests = new ArrayList<>(NUM_WORKERS);
+ final Set<UUID> completed = new HashSet<>(NUM_WORKERS);
+ final int schedulerLimit = mWorkManagerImpl
+ .getConfiguration()
+ .getMaxSchedulerLimit();
+
+ final Data input = new Data.Builder()
+ .putBoolean(KEY_RECURSIVE, true)
+ .putInt(KEY_LIMIT_TO_ENFORCE, schedulerLimit)
+ .build();
+
+ for (int i = 0; i < NUM_WORKERS; i++) {
+ OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CheckLimitsWorker.class)
+ .setInputData(input)
+ .build();
+
+ workRequests.add(request);
+ }
+
+
+ final CountDownLatch latch = new CountDownLatch(NUM_WORKERS * 2); // recursive
+ WorkContinuation continuation = mWorkManagerImpl.beginWith(workRequests);
+
+ // There are more workers being enqueued recursively so use implicit tags.
+ mWorkManagerImpl.getStatusesByTag(CheckLimitsWorker.class.getName())
+ .observe(mLifecycleOwner, new Observer<List<WorkStatus>>() {
+ @Override
+ public void onChanged(@Nullable List<WorkStatus> workStatuses) {
+ if (workStatuses == null || workStatuses.isEmpty()) {
+ return;
+ }
+
+ for (WorkStatus workStatus: workStatuses) {
+ if (workStatus.getState().isFinished()) {
+
+ Data output = workStatus.getOutputData();
+
+ boolean exceededLimits = output.getBoolean(
+ KEY_EXCEEDS_SCHEDULER_LIMIT, true);
+
+ assertThat(exceededLimits, is(false));
+ if (!completed.contains(workStatus.getId())) {
+ completed.add(workStatus.getId());
+ latch.countDown();
+ }
+ }
+ }
+ }
+ });
+
+ continuation.enqueue();
+ latch.await(240L, TimeUnit.SECONDS);
assertThat(latch.getCount(), is(0L));
}
}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 3e01164c251..4e1dc8fa8c7 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -27,6 +27,7 @@ import static androidx.work.State.ENQUEUED;
import static androidx.work.State.FAILED;
import static androidx.work.State.RUNNING;
import static androidx.work.State.SUCCEEDED;
+import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -1405,6 +1406,7 @@ public class WorkManagerImplTest {
WorkDatabase.generateCleanupCallback().onOpen(db);
assertThat(workSpecDao.getState(work.getStringId()), is(ENQUEUED));
+ assertThat(work.getWorkSpec().scheduleRequestedAt, is(SCHEDULE_NOT_REQUESTED_YET));
}
@Test
@@ -1508,7 +1510,7 @@ public class WorkManagerImplTest {
WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
assertThat(workSpec.workerClassName, is(ConstraintTrackingWorker.class.getName()));
assertThat(workSpec.input.getString(
- ConstraintTrackingWorker.ARGUMENT_CLASS_NAME, null),
+ ConstraintTrackingWorker.ARGUMENT_CLASS_NAME),
is(TestWorker.class.getName()));
}
@@ -1526,7 +1528,7 @@ public class WorkManagerImplTest {
WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
assertThat(workSpec.workerClassName, is(ConstraintTrackingWorker.class.getName()));
assertThat(workSpec.input.getString(
- ConstraintTrackingWorker.ARGUMENT_CLASS_NAME, null),
+ ConstraintTrackingWorker.ARGUMENT_CLASS_NAME),
is(TestWorker.class.getName()));
}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 739fcf7b626..44e65f808f9 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -31,6 +31,7 @@ import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.greaterThan;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -38,6 +39,7 @@ import static org.mockito.Mockito.verify;
import android.content.Context;
import android.net.Uri;
+import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
@@ -507,6 +509,7 @@ public class WorkerWrapperTest extends DatabaseTest {
insertWork(periodicWork);
new WorkerWrapper.Builder(mContext, mConfiguration, mDatabase, periodicWorkId)
.withListener(mMockListener)
+ .withSchedulers(Collections.singletonList(mMockScheduler))
.build()
.run();
@@ -514,6 +517,15 @@ public class WorkerWrapperTest extends DatabaseTest {
verify(mMockListener).onExecuted(periodicWorkId, true, false);
assertThat(periodicWorkSpecAfterFirstRun.runAttemptCount, is(0));
assertThat(periodicWorkSpecAfterFirstRun.state, is(ENQUEUED));
+ // SystemAlarmScheduler needs to reschedule the same worker.
+ if (Build.VERSION.SDK_INT <= WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL) {
+ ArgumentCaptor<WorkSpec> captor = ArgumentCaptor.forClass(WorkSpec.class);
+ verify(mMockScheduler, atLeast(1))
+ .schedule(captor.capture());
+
+ WorkSpec workSpec = captor.getValue();
+ assertThat(workSpec.id, is(periodicWorkId));
+ }
}
@Test
@@ -563,16 +575,34 @@ public class WorkerWrapperTest extends DatabaseTest {
@Test
@SmallTest
public void testScheduler() {
- OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
- insertWork(work);
- Scheduler mockScheduler = mock(Scheduler.class);
+ OneTimeWorkRequest prerequisiteWork =
+ new OneTimeWorkRequest.Builder(TestWorker.class).build();
+ OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
+ .setInitialState(BLOCKED).build();
+ Dependency dependency = new Dependency(work.getStringId(), prerequisiteWork.getStringId());
- new WorkerWrapper.Builder(mContext, mConfiguration, mDatabase, work.getStringId())
- .withSchedulers(Collections.singletonList(mockScheduler))
+ mDatabase.beginTransaction();
+ try {
+ insertWork(prerequisiteWork);
+ insertWork(work);
+ mDependencyDao.insertDependency(dependency);
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ new WorkerWrapper.Builder(
+ mContext,
+ mConfiguration,
+ mDatabase,
+ prerequisiteWork.getStringId())
+ .withSchedulers(Collections.singletonList(mMockScheduler))
.build()
.run();
- verify(mockScheduler).schedule();
+ ArgumentCaptor<WorkSpec> captor = ArgumentCaptor.forClass(WorkSpec.class);
+ verify(mMockScheduler).schedule(captor.capture());
+ assertThat(captor.getValue().id, is(work.getStringId()));
}
@Test
@@ -603,7 +633,7 @@ public class WorkerWrapperTest extends DatabaseTest {
new Extras(input, Collections.<String>emptyList(), null, 1));
assertThat(worker, is(notNullValue()));
- assertThat(worker.getInputData().getString(key, null), is(expectedValue));
+ assertThat(worker.getInputData().getString(key), is(expectedValue));
work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
worker = WorkerWrapper.workerFromWorkSpec(
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index cfe2cef7db5..2944087cd21 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -389,6 +389,7 @@ public class SystemAlarmDispatcherTest extends DatabaseTest {
// Use a mocked scheduler in this test.
Scheduler scheduler = mock(Scheduler.class);
doCallRealMethod().when(mWorkManager).rescheduleEligibleWork();
+ when(mWorkManager.getApplicationContext()).thenReturn(mContext);
when(mWorkManager.getSchedulers()).thenReturn(Collections.singletonList(scheduler));
OneTimeWorkRequest failed = new OneTimeWorkRequest.Builder(TestWorker.class)
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
index 78986c3bcc3..700071ad784 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
@@ -49,7 +49,7 @@ public class WorkTimerTest {
public void testTimer_withListenerAndCleanUp() throws InterruptedException {
TestTimeLimitExceededListener listenerSpy = spy(mListener);
mWorkTimer.startTimer(WORKSPEC_ID_1, 0, listenerSpy);
- Thread.sleep(10); // introduce a small delay
+ Thread.sleep(100); // introduce a small delay
verify(listenerSpy, times(1)).onTimeLimitExceeded(WORKSPEC_ID_1);
assertThat(mWorkTimer.getTimerMap().size(), is(0));
assertThat(mWorkTimer.getListeners().size(), is(0));
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 323757a0010..3d6b4c09251 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -18,7 +18,6 @@ package androidx.work.impl.utils;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -76,37 +75,28 @@ public class ForceStopRunnableTest {
@Test
public void testReschedulesOnForceStop() {
ForceStopRunnable runnable = spy(mRunnable);
- when(runnable.shouldCancelPersistedJobs()).thenReturn(false);
+ when(runnable.shouldRescheduleWorkers()).thenReturn(false);
when(runnable.isForceStopped()).thenReturn(true);
runnable.run();
verify(mWorkManager, times(1)).rescheduleEligibleWork();
+ verify(mWorkManager, times(1)).onForceStopRunnableCompleted();
}
@Test
public void test_doNothingWhenNotForceStopped() {
ForceStopRunnable runnable = spy(mRunnable);
- when(runnable.shouldCancelPersistedJobs()).thenReturn(false);
+ when(runnable.shouldRescheduleWorkers()).thenReturn(false);
when(runnable.isForceStopped()).thenReturn(false);
runnable.run();
verify(mWorkManager, times(0)).rescheduleEligibleWork();
+ verify(mWorkManager, times(1)).onForceStopRunnableCompleted();
}
@Test
- public void test_cancelAllJobSchedulerJobs() {
+ public void test_rescheduleWorkers_updatesSharedPreferences() {
ForceStopRunnable runnable = spy(mRunnable);
- doNothing().when(runnable).cancelAllInJobScheduler();
- when(runnable.shouldCancelPersistedJobs()).thenReturn(true);
+ when(runnable.shouldRescheduleWorkers()).thenReturn(true);
runnable.run();
- verify(runnable, times(1)).cancelAllInJobScheduler();
- verify(mPreferences, times(1)).setMigratedPersistedJobs();
- }
-
- @Test
- public void test_doNothingWhenThereIsNothingToCancel() {
- ForceStopRunnable runnable = spy(mRunnable);
- doNothing().when(runnable).cancelAllInJobScheduler();
- when(runnable.shouldCancelPersistedJobs()).thenReturn(false);
- runnable.run();
- verify(runnable, times(0)).cancelAllInJobScheduler();
+ verify(mPreferences, times(1)).setNeedsReschedule(false);
}
}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/worker/CheckLimitsWorker.java b/work/workmanager/src/androidTest/java/androidx/work/worker/CheckLimitsWorker.java
index 11763641ed8..f0fb9a1d193 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/worker/CheckLimitsWorker.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/worker/CheckLimitsWorker.java
@@ -21,6 +21,7 @@ import static androidx.work.Worker.Result.SUCCESS;
import android.support.annotation.NonNull;
import androidx.work.Data;
+import androidx.work.OneTimeWorkRequest;
import androidx.work.Worker;
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
@@ -30,6 +31,7 @@ import java.util.List;
public class CheckLimitsWorker extends Worker {
/* The limit to enforce */
+ public static final String KEY_RECURSIVE = "recursive";
public static final String KEY_LIMIT_TO_ENFORCE = "limit";
/* The output key which tells us if we exceeded the scheduler limits. */
@@ -39,6 +41,7 @@ public class CheckLimitsWorker extends Worker {
@Override
public Result doWork() {
Data input = getInputData();
+ boolean isRecursive = input.getBoolean(KEY_RECURSIVE, false);
int limitToEnforce = input.getInt(KEY_LIMIT_TO_ENFORCE, Scheduler.MAX_SCHEDULER_LIMIT);
WorkManagerImpl workManager = WorkManagerImpl.getInstance();
List<WorkSpec> eligibleWorkSpecs = workManager.getWorkDatabase()
@@ -49,8 +52,19 @@ public class CheckLimitsWorker extends Worker {
Data output = new Data.Builder()
.putBoolean(KEY_EXCEEDS_SCHEDULER_LIMIT, exceedsLimits)
.build();
-
setOutputData(output);
+ if (isRecursive) {
+ // kick off another Worker, which is not recursive.
+ Data newRequestData = new Data.Builder()
+ .putAll(getInputData())
+ .putBoolean(KEY_RECURSIVE, false)
+ .build();
+
+ OneTimeWorkRequest newRequest = new OneTimeWorkRequest.Builder(CheckLimitsWorker.class)
+ .setInputData(newRequestData)
+ .build();
+ workManager.enqueue(newRequest);
+ }
return SUCCESS;
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/Configuration.java b/work/workmanager/src/main/java/androidx/work/Configuration.java
index 77f5ffa9b44..a74c4b5945f 100644
--- a/work/workmanager/src/main/java/androidx/work/Configuration.java
+++ b/work/workmanager/src/main/java/androidx/work/Configuration.java
@@ -103,7 +103,7 @@ public final class Configuration {
}
}
- private Executor createDefaultExecutor() {
+ private @NonNull Executor createDefaultExecutor() {
return Executors.newFixedThreadPool(
// This value is the same as the core pool size for AsyncTask#THREAD_POOL_EXECUTOR.
Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)));
@@ -125,7 +125,7 @@ public final class Configuration {
* @param executor An {@link Executor} for processing work
* @return This {@link Builder} instance
*/
- public Builder setExecutor(@NonNull Executor executor) {
+ public @NonNull Builder setExecutor(@NonNull Executor executor) {
mExecutor = executor;
return this;
}
@@ -139,7 +139,9 @@ public final class Configuration {
* @return This {@link Builder} instance
* @throws IllegalArgumentException when the size of the range is < 1000
*/
- public Builder setJobSchedulerJobIdRange(int minJobSchedulerId, int maxJobSchedulerId) {
+ public @NonNull Builder setJobSchedulerJobIdRange(
+ int minJobSchedulerId,
+ int maxJobSchedulerId) {
if ((maxJobSchedulerId - minJobSchedulerId) < 1000) {
throw new IllegalArgumentException(
"WorkManager needs a range of at least 1000 job ids.");
@@ -167,7 +169,7 @@ public final class Configuration {
* @throws IllegalArgumentException when the number of jobs <
* {@link Configuration#MIN_SCHEDULER_LIMIT}
*/
- public Builder setMaxSchedulerLimit(int maxSchedulerLimit) {
+ public @NonNull Builder setMaxSchedulerLimit(int maxSchedulerLimit) {
if (maxSchedulerLimit < MIN_SCHEDULER_LIMIT) {
throw new IllegalArgumentException(
"WorkManager needs to be able to schedule at least 20 jobs in "
@@ -185,7 +187,7 @@ public final class Configuration {
* @deprecated Use the {@link Configuration.Builder#setExecutor(Executor)} method instead
*/
@Deprecated
- public Builder withExecutor(@NonNull Executor executor) {
+ public @NonNull Builder withExecutor(@NonNull Executor executor) {
mExecutor = executor;
return this;
}
@@ -195,7 +197,7 @@ public final class Configuration {
*
* @return A {@link Configuration} object with this {@link Builder}'s parameters.
*/
- public Configuration build() {
+ public @NonNull Configuration build() {
return new Configuration(this);
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/Constraints.java b/work/workmanager/src/main/java/androidx/work/Constraints.java
index d6d6ad6ab48..8b8a740d12b 100644
--- a/work/workmanager/src/main/java/androidx/work/Constraints.java
+++ b/work/workmanager/src/main/java/androidx/work/Constraints.java
@@ -20,6 +20,7 @@ import android.arch.persistence.room.ColumnInfo;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
/**
@@ -29,23 +30,24 @@ public final class Constraints {
public static final Constraints NONE = new Constraints.Builder().build();
+ // TODO(sumir): Need to make this @NonNull, but that requires a db migration.
@ColumnInfo(name = "required_network_type")
- NetworkType mRequiredNetworkType;
+ private NetworkType mRequiredNetworkType;
@ColumnInfo(name = "requires_charging")
- boolean mRequiresCharging;
+ private boolean mRequiresCharging;
@ColumnInfo(name = "requires_device_idle")
- boolean mRequiresDeviceIdle;
+ private boolean mRequiresDeviceIdle;
@ColumnInfo(name = "requires_battery_not_low")
- boolean mRequiresBatteryNotLow;
+ private boolean mRequiresBatteryNotLow;
@ColumnInfo(name = "requires_storage_not_low")
- boolean mRequiresStorageNotLow;
+ private boolean mRequiresStorageNotLow;
@ColumnInfo(name = "content_uri_triggers")
- ContentUriTriggers mContentUriTriggers;
+ private @Nullable ContentUriTriggers mContentUriTriggers;
public Constraints() { // stub required for room
}
@@ -116,12 +118,12 @@ public final class Constraints {
}
@RequiresApi(24)
- public void setContentUriTriggers(ContentUriTriggers mContentUriTriggers) {
+ public void setContentUriTriggers(@Nullable ContentUriTriggers mContentUriTriggers) {
this.mContentUriTriggers = mContentUriTriggers;
}
@RequiresApi(24)
- public ContentUriTriggers getContentUriTriggers() {
+ public @Nullable ContentUriTriggers getContentUriTriggers() {
return mContentUriTriggers;
}
@@ -130,7 +132,7 @@ public final class Constraints {
*/
@RequiresApi(24)
public boolean hasContentUriTriggers() {
- return mContentUriTriggers.size() > 0;
+ return mContentUriTriggers != null && mContentUriTriggers.size() > 0;
}
@Override
@@ -180,7 +182,7 @@ public final class Constraints {
* @param requiresCharging true if device must be plugged in, false otherwise
* @return current builder
*/
- public Builder setRequiresCharging(boolean requiresCharging) {
+ public @NonNull Builder setRequiresCharging(boolean requiresCharging) {
this.mRequiresCharging = requiresCharging;
return this;
}
@@ -193,7 +195,7 @@ public final class Constraints {
* @return current builder
*/
@RequiresApi(23)
- public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
+ public @NonNull Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
this.mRequiresDeviceIdle = requiresDeviceIdle;
return this;
}
@@ -205,7 +207,7 @@ public final class Constraints {
* @param networkType type of network required
* @return current builder
*/
- public Builder setRequiredNetworkType(@NonNull NetworkType networkType) {
+ public @NonNull Builder setRequiredNetworkType(@NonNull NetworkType networkType) {
this.mRequiredNetworkType = networkType;
return this;
}
@@ -218,7 +220,7 @@ public final class Constraints {
* false otherwise
* @return current builder
*/
- public Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow) {
+ public @NonNull Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow) {
this.mRequiresBatteryNotLow = requiresBatteryNotLow;
return this;
}
@@ -231,7 +233,7 @@ public final class Constraints {
* threshold, false otherwise
* @return current builder
*/
- public Builder setRequiresStorageNotLow(boolean requiresStorageNotLow) {
+ public @NonNull Builder setRequiresStorageNotLow(boolean requiresStorageNotLow) {
this.mRequiresStorageNotLow = requiresStorageNotLow;
return this;
}
@@ -246,7 +248,7 @@ public final class Constraints {
* @return The current {@link Builder}
*/
@RequiresApi(24)
- public Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants) {
+ public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants) {
mContentUriTriggers.add(uri, triggerForDescendants);
return this;
}
@@ -256,7 +258,7 @@ public final class Constraints {
*
* @return new {@link Constraints} which can be attached to a {@link WorkRequest}
*/
- public Constraints build() {
+ public @NonNull Constraints build() {
return new Constraints(this);
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/ContentUriTriggers.java b/work/workmanager/src/main/java/androidx/work/ContentUriTriggers.java
index 0702cc05d1c..ca6dd308f98 100644
--- a/work/workmanager/src/main/java/androidx/work/ContentUriTriggers.java
+++ b/work/workmanager/src/main/java/androidx/work/ContentUriTriggers.java
@@ -27,6 +27,7 @@ import java.util.Set;
* Stores a set of {@link Trigger}s
*/
public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Trigger> {
+
private final Set<Trigger> mTriggers = new HashSet<>();
/**
@@ -35,7 +36,7 @@ public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Tri
* @param triggerForDescendants {@code true} if any changes in descendants cause this
* {@link WorkRequest} to run
*/
- public void add(Uri uri, boolean triggerForDescendants) {
+ public void add(@NonNull Uri uri, boolean triggerForDescendants) {
Trigger trigger = new Trigger(uri, triggerForDescendants);
mTriggers.add(trigger);
}
@@ -73,18 +74,15 @@ public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Tri
*/
public static final class Trigger {
- @NonNull
- private final Uri mUri;
+ private final @NonNull Uri mUri;
private final boolean mTriggerForDescendants;
- public Trigger(@NonNull Uri uri,
- boolean triggerForDescendants) {
+ Trigger(@NonNull Uri uri, boolean triggerForDescendants) {
mUri = uri;
mTriggerForDescendants = triggerForDescendants;
}
- @NonNull
- public Uri getUri() {
+ public @NonNull Uri getUri() {
return mUri;
}
diff --git a/work/workmanager/src/main/java/androidx/work/Data.java b/work/workmanager/src/main/java/androidx/work/Data.java
index 376a00d97cc..b687a367e15 100644
--- a/work/workmanager/src/main/java/androidx/work/Data.java
+++ b/work/workmanager/src/main/java/androidx/work/Data.java
@@ -18,6 +18,7 @@ package androidx.work;
import android.arch.persistence.room.TypeConverter;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.io.ByteArrayInputStream;
@@ -60,7 +61,7 @@ public final class Data {
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public boolean getBoolean(String key, boolean defaultValue) {
+ public boolean getBoolean(@NonNull String key, boolean defaultValue) {
Object value = mValues.get(key);
if (value instanceof Boolean) {
return (boolean) value;
@@ -75,7 +76,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public boolean[] getBooleanArray(String key) {
+ public @NonNull boolean[] getBooleanArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Boolean[]) {
Boolean[] array = (Boolean[]) value;
@@ -97,7 +98,7 @@ public final class Data {
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public int getInt(String key, int defaultValue) {
+ public int getInt(@NonNull String key, int defaultValue) {
Object value = mValues.get(key);
if (value instanceof Integer) {
return (int) value;
@@ -112,7 +113,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public int[] getIntArray(String key) {
+ public @NonNull int[] getIntArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Integer[]) {
Integer[] array = (Integer[]) value;
@@ -133,7 +134,7 @@ public final class Data {
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public long getLong(String key, long defaultValue) {
+ public long getLong(@NonNull String key, long defaultValue) {
Object value = mValues.get(key);
if (value instanceof Long) {
return (long) value;
@@ -148,7 +149,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public long[] getLongArray(String key) {
+ public @Nullable long[] getLongArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Long[]) {
Long[] array = (Long[]) value;
@@ -169,7 +170,7 @@ public final class Data {
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public float getFloat(String key, float defaultValue) {
+ public float getFloat(@NonNull String key, float defaultValue) {
Object value = mValues.get(key);
if (value instanceof Float) {
return (float) value;
@@ -184,7 +185,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public float[] getFloatArray(String key) {
+ public @Nullable float[] getFloatArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Float[]) {
Float[] array = (Float[]) value;
@@ -205,7 +206,7 @@ public final class Data {
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public double getDouble(String key, double defaultValue) {
+ public double getDouble(@NonNull String key, double defaultValue) {
Object value = mValues.get(key);
if (value instanceof Double) {
return (double) value;
@@ -220,7 +221,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public double[] getDoubleArray(String key) {
+ public @Nullable double[] getDoubleArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Double[]) {
Double[] array = (Double[]) value;
@@ -238,15 +239,14 @@ public final class Data {
* Get the String value for the given key.
*
* @param key The key for the argument
- * @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
- public String getString(String key, String defaultValue) {
+ public @Nullable String getString(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String) {
return (String) value;
} else {
- return defaultValue;
+ return null;
}
}
@@ -256,7 +256,7 @@ public final class Data {
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public String[] getStringArray(String key) {
+ public @Nullable String[] getStringArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String[]) {
return (String[]) value;
@@ -271,7 +271,7 @@ public final class Data {
* @return A {@link Map} of key-value pairs for this object; this Map is unmodifiable and should
* be used for reads only.
*/
- public Map<String, Object> getKeyValueMap() {
+ public @NonNull Map<String, Object> getKeyValueMap() {
return Collections.unmodifiableMap(mValues);
}
@@ -292,7 +292,7 @@ public final class Data {
* {@link #MAX_DATA_BYTES}
*/
@TypeConverter
- public static byte[] toByteArray(Data data) throws IllegalStateException {
+ public static @NonNull byte[] toByteArray(@NonNull Data data) throws IllegalStateException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = null;
try {
@@ -334,7 +334,7 @@ public final class Data {
* @throws IllegalStateException if bytes is bigger than {@link #MAX_DATA_BYTES}
*/
@TypeConverter
- public static Data fromByteArray(byte[] bytes) throws IllegalStateException {
+ public static @NonNull Data fromByteArray(@NonNull byte[] bytes) throws IllegalStateException {
if (bytes.length > MAX_DATA_BYTES) {
throw new IllegalStateException(
"Data cannot occupy more than " + MAX_DATA_BYTES + "KB when serialized");
@@ -438,7 +438,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putBoolean(String key, boolean value) {
+ public @NonNull Builder putBoolean(@NonNull String key, boolean value) {
mValues.put(key, value);
return this;
}
@@ -450,7 +450,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putBooleanArray(String key, boolean[] value) {
+ public @NonNull Builder putBooleanArray(@NonNull String key, boolean[] value) {
mValues.put(key, convertPrimitiveBooleanArray(value));
return this;
}
@@ -462,7 +462,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putInt(String key, int value) {
+ public @NonNull Builder putInt(@NonNull String key, int value) {
mValues.put(key, value);
return this;
}
@@ -474,7 +474,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putIntArray(String key, int[] value) {
+ public @NonNull Builder putIntArray(@NonNull String key, int[] value) {
mValues.put(key, convertPrimitiveIntArray(value));
return this;
}
@@ -486,7 +486,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putLong(String key, long value) {
+ public @NonNull Builder putLong(@NonNull String key, long value) {
mValues.put(key, value);
return this;
}
@@ -498,7 +498,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putLongArray(String key, long[] value) {
+ public @NonNull Builder putLongArray(@NonNull String key, long[] value) {
mValues.put(key, convertPrimitiveLongArray(value));
return this;
}
@@ -510,7 +510,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putFloat(String key, float value) {
+ public @NonNull Builder putFloat(@NonNull String key, float value) {
mValues.put(key, value);
return this;
}
@@ -522,7 +522,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putFloatArray(String key, float[] value) {
+ public @NonNull Builder putFloatArray(String key, float[] value) {
mValues.put(key, convertPrimitiveFloatArray(value));
return this;
}
@@ -534,7 +534,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putDouble(String key, double value) {
+ public @NonNull Builder putDouble(@NonNull String key, double value) {
mValues.put(key, value);
return this;
}
@@ -546,7 +546,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putDoubleArray(String key, double[] value) {
+ public @NonNull Builder putDoubleArray(@NonNull String key, double[] value) {
mValues.put(key, convertPrimitiveDoubleArray(value));
return this;
}
@@ -558,7 +558,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putString(String key, String value) {
+ public @NonNull Builder putString(@NonNull String key, String value) {
mValues.put(key, value);
return this;
}
@@ -570,7 +570,7 @@ public final class Data {
* @param value The value for this argument
* @return The {@link Builder}
*/
- public Builder putStringArray(String key, String[] value) {
+ public @NonNull Builder putStringArray(@NonNull String key, String[] value) {
mValues.put(key, value);
return this;
}
@@ -584,7 +584,7 @@ public final class Data {
* @param data {@link Data} containing key-value pairs to add
* @return The {@link Builder}
*/
- public Builder putAll(@NonNull Data data) {
+ public @NonNull Builder putAll(@NonNull Data data) {
putAll(data.mValues);
return this;
}
@@ -597,7 +597,7 @@ public final class Data {
* @param values A {@link Map} of key-value pairs to add
* @return The {@link Builder}
*/
- public Builder putAll(Map<String, Object> values) {
+ public @NonNull Builder putAll(@NonNull Map<String, Object> values) {
for (Map.Entry<String, Object> entry : values.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
@@ -643,7 +643,7 @@ public final class Data {
* @return The {@link Data} object containing all key-value pairs specified by this
* {@link Builder}.
*/
- public Data build() {
+ public @NonNull Data build() {
return new Data(mValues);
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
index 57849be187d..22778e4ba0e 100644
--- a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
@@ -83,7 +83,7 @@ public final class OneTimeWorkRequest extends WorkRequest {
* @param timeUnit The units of time for {@code duration}
* @return The current {@link Builder}
*/
- public Builder setInitialDelay(long duration, @NonNull TimeUnit timeUnit) {
+ public @NonNull Builder setInitialDelay(long duration, @NonNull TimeUnit timeUnit) {
mWorkSpec.initialDelay = timeUnit.toMillis(duration);
return this;
}
@@ -95,7 +95,7 @@ public final class OneTimeWorkRequest extends WorkRequest {
* @return The current {@link Builder}
*/
@RequiresApi(26)
- public Builder setInitialDelay(Duration duration) {
+ public @NonNull Builder setInitialDelay(@NonNull Duration duration) {
mWorkSpec.initialDelay = duration.toMillis();
return this;
}
@@ -110,14 +110,14 @@ public final class OneTimeWorkRequest extends WorkRequest {
* {@link OneTimeWorkRequest}
* @return The current {@link Builder}
*/
- public Builder setInputMerger(@NonNull Class<? extends InputMerger> inputMerger) {
+ public @NonNull Builder setInputMerger(@NonNull Class<? extends InputMerger> inputMerger) {
mWorkSpec.inputMergerClassName = inputMerger.getName();
return this;
}
@Override
- public OneTimeWorkRequest build() {
+ public @NonNull OneTimeWorkRequest build() {
if (mBackoffCriteriaSet
&& Build.VERSION.SDK_INT >= 23
&& mWorkSpec.constraints.requiresDeviceIdle()) {
@@ -128,7 +128,7 @@ public final class OneTimeWorkRequest extends WorkRequest {
}
@Override
- Builder getThis() {
+ @NonNull Builder getThis() {
return this;
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
index fe1a3870241..33f54924749 100644
--- a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
@@ -151,7 +151,7 @@ public final class PeriodicWorkRequest extends WorkRequest {
}
@Override
- public PeriodicWorkRequest build() {
+ public @NonNull PeriodicWorkRequest build() {
if (mBackoffCriteriaSet
&& Build.VERSION.SDK_INT >= 23
&& mWorkSpec.constraints.requiresDeviceIdle()) {
@@ -162,7 +162,7 @@ public final class PeriodicWorkRequest extends WorkRequest {
}
@Override
- Builder getThis() {
+ @NonNull Builder getThis() {
return this;
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/State.java b/work/workmanager/src/main/java/androidx/work/State.java
index a654456d056..3541facd009 100644
--- a/work/workmanager/src/main/java/androidx/work/State.java
+++ b/work/workmanager/src/main/java/androidx/work/State.java
@@ -17,45 +17,45 @@
package androidx.work;
/**
- * The current status of a unit of work.
+ * The current state of a unit of work.
*/
public enum State {
/**
- * The status for work that is enqueued (hasn't completed and isn't running)
+ * The state for work that is enqueued (hasn't completed and isn't running)
*/
ENQUEUED,
/**
- * The status for work that is currently being executed
+ * The state for work that is currently being executed
*/
RUNNING,
/**
- * The status for work that has completed successfully
+ * The state for work that has completed successfully
*/
SUCCEEDED,
/**
- * The status for work that has completed in a failure state
+ * The state for work that has completed in a failure state
*/
FAILED,
/**
- * The status for work that is currently blocked because its prerequisites haven't finished
+ * The state for work that is currently blocked because its prerequisites haven't finished
* successfully
*/
BLOCKED,
/**
- * The status for work that has been cancelled and will not execute
+ * The state for work that has been cancelled and will not execute
*/
CANCELLED;
/**
* Returns {@code true} if this State is considered finished.
*
- * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and {@link #CANCELLED} States
+ * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and {@link #CANCELLED} states
*/
public boolean isFinished() {
return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
diff --git a/work/workmanager/src/main/java/androidx/work/SynchronousWorkContinuation.java b/work/workmanager/src/main/java/androidx/work/SynchronousWorkContinuation.java
index 0b270211d7d..3a38ee767e9 100644
--- a/work/workmanager/src/main/java/androidx/work/SynchronousWorkContinuation.java
+++ b/work/workmanager/src/main/java/androidx/work/SynchronousWorkContinuation.java
@@ -16,6 +16,7 @@
package androidx.work;
+import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import java.util.List;
@@ -41,5 +42,5 @@ public interface SynchronousWorkContinuation {
* @return A {@link List} of {@link WorkStatus}es
*/
@WorkerThread
- List<WorkStatus> getStatusesSync();
+ @NonNull List<WorkStatus> getStatusesSync();
}
diff --git a/work/workmanager/src/main/java/androidx/work/SynchronousWorkManager.java b/work/workmanager/src/main/java/androidx/work/SynchronousWorkManager.java
index 9f633d8e3e0..3bb6b4d2ff3 100644
--- a/work/workmanager/src/main/java/androidx/work/SynchronousWorkManager.java
+++ b/work/workmanager/src/main/java/androidx/work/SynchronousWorkManager.java
@@ -17,6 +17,7 @@
package androidx.work;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import java.util.List;
@@ -124,7 +125,8 @@ public interface SynchronousWorkManager {
* own repository that must be updated or deleted in case someone cancels their work without
* their prior knowledge.
*
- * @return The timestamp in milliseconds when a method that cancelled all work was last invoked
+ * @return The timestamp in milliseconds when a method that cancelled all work was last invoked;
+ * this timestamp may be {@code 0L} if this never occurred.
*/
@WorkerThread
long getLastCancelAllTimeMillisSync();
@@ -148,10 +150,11 @@ public interface SynchronousWorkManager {
* expected to be called from a background thread.
*
* @param id The id of the work
- * @return A {@link WorkStatus} associated with {@code id}
+ * @return A {@link WorkStatus} associated with {@code id}, or {@code null} if {@code id} is not
+ * known to WorkManager
*/
@WorkerThread
- WorkStatus getStatusByIdSync(@NonNull UUID id);
+ @Nullable WorkStatus getStatusByIdSync(@NonNull UUID id);
/**
* Gets the {@link WorkStatus} for all work with a given tag in a synchronous fashion. This
@@ -161,7 +164,7 @@ public interface SynchronousWorkManager {
* @return A list of {@link WorkStatus} for work tagged with {@code tag}
*/
@WorkerThread
- List<WorkStatus> getStatusesByTagSync(@NonNull String tag);
+ @NonNull List<WorkStatus> getStatusesByTagSync(@NonNull String tag);
/**
* Gets the {@link WorkStatus} for all work for the chain of work with a given unique name in a
@@ -171,5 +174,5 @@ public interface SynchronousWorkManager {
* @return A list of {@link WorkStatus} for work in the chain named {@code uniqueWorkName}
*/
@WorkerThread
- List<WorkStatus> getStatusesForUniqueWorkSync(@NonNull String uniqueWorkName);
+ @NonNull List<WorkStatus> getStatusesForUniqueWorkSync(@NonNull String uniqueWorkName);
}
diff --git a/work/workmanager/src/main/java/androidx/work/WorkContinuation.java b/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
index 80acc8284da..5c781b35f92 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
@@ -36,7 +36,7 @@ public abstract class WorkContinuation {
* @return A {@link WorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- public final WorkContinuation then(@NonNull OneTimeWorkRequest... work) {
+ public final @NonNull WorkContinuation then(@NonNull OneTimeWorkRequest... work) {
return then(Arrays.asList(work));
}
@@ -48,7 +48,7 @@ public abstract class WorkContinuation {
* @return A {@link WorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- public abstract WorkContinuation then(@NonNull List<OneTimeWorkRequest> work);
+ public abstract @NonNull WorkContinuation then(@NonNull List<OneTimeWorkRequest> work);
/**
* Returns a {@link LiveData} list of {@link WorkStatus} that provides information about work,
@@ -57,7 +57,7 @@ public abstract class WorkContinuation {
*
* @return A {@link LiveData} containing a list of {@link WorkStatus}es
*/
- public abstract LiveData<List<WorkStatus>> getStatuses();
+ public abstract @NonNull LiveData<List<WorkStatus>> getStatuses();
/**
* Enqueues the instance of {@link WorkContinuation} on the background thread.
@@ -70,7 +70,7 @@ public abstract class WorkContinuation {
* @return A {@link SynchronousWorkContinuation} object, which gives access to synchronous
* methods
*/
- public abstract SynchronousWorkContinuation synchronous();
+ public abstract @NonNull SynchronousWorkContinuation synchronous();
/**
* Combines multiple {@link WorkContinuation}s to allow for complex chaining.
@@ -79,7 +79,7 @@ public abstract class WorkContinuation {
* return value
* @return A {@link WorkContinuation} that allows further chaining
*/
- public static WorkContinuation combine(@NonNull WorkContinuation... continuations) {
+ public static @NonNull WorkContinuation combine(@NonNull WorkContinuation... continuations) {
return combine(Arrays.asList(continuations));
}
@@ -90,7 +90,7 @@ public abstract class WorkContinuation {
* return value
* @return A {@link WorkContinuation} that allows further chaining
*/
- public static WorkContinuation combine(@NonNull List<WorkContinuation> continuations) {
+ public static @NonNull WorkContinuation combine(@NonNull List<WorkContinuation> continuations) {
if (continuations.size() < 2) {
throw new IllegalArgumentException(
"WorkContinuation.combine() needs at least 2 continuations.");
@@ -109,7 +109,7 @@ public abstract class WorkContinuation {
* {@link OneTimeWorkRequest} provided.
* @return A {@link WorkContinuation} that allows further chaining
*/
- public static WorkContinuation combine(
+ public static @NonNull WorkContinuation combine(
@NonNull OneTimeWorkRequest work,
@NonNull WorkContinuation... continuations) {
return combine(work, Arrays.asList(continuations));
@@ -125,7 +125,7 @@ public abstract class WorkContinuation {
* {@link OneTimeWorkRequest} provided.
* @return A {@link WorkContinuation} that allows further chaining
*/
- public static WorkContinuation combine(
+ public static @NonNull WorkContinuation combine(
@NonNull OneTimeWorkRequest work,
@NonNull List<WorkContinuation> continuations) {
return continuations.get(0).combineInternal(work, continuations);
@@ -135,7 +135,7 @@ public abstract class WorkContinuation {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- protected abstract WorkContinuation combineInternal(
+ protected abstract @NonNull WorkContinuation combineInternal(
@Nullable OneTimeWorkRequest work,
@NonNull List<WorkContinuation> continuations);
}
diff --git a/work/workmanager/src/main/java/androidx/work/WorkManager.java b/work/workmanager/src/main/java/androidx/work/WorkManager.java
index 5960e50fcab..939192a15c8 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkManager.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManager.java
@@ -121,15 +121,35 @@ public abstract class WorkManager {
/**
* Retrieves the {@code default} singleton instance of {@link WorkManager}.
*
- * @return The singleton instance of {@link WorkManager}
+ * @return The singleton instance of {@link WorkManager}; this may be {@code null} in unusual
+ * circumstances where you have disabled automatic initialization and have failed to
+ * manually call {@link #initialize(Context, Configuration)}.
+ * @throws IllegalStateException If WorkManager is not initialized properly. This is most
+ * likely because you disabled the automatic initialization but forgot to manually
+ * call {@link WorkManager#initialize(Context, Configuration)}.
*/
- public static WorkManager getInstance() {
- return WorkManagerImpl.getInstance();
+ public static @NonNull WorkManager getInstance() {
+ WorkManager workManager = WorkManagerImpl.getInstance();
+ if (workManager == null) {
+ throw new IllegalStateException("WorkManager is not initialized properly. The most "
+ + "likely cause is that you disabled WorkManagerInitializer in your manifest "
+ + "but forgot to call WorkManager#initialize in your Application#onCreate or a "
+ + "ContentProvider.");
+ } else {
+ return workManager;
+ }
}
/**
- * Used to do a one-time initialization of the {@link WorkManager} singleton with the default
- * configuration.
+ * Used to do a one-time initialization of the {@link WorkManager} singleton with a custom
+ * {@link Configuration}. By default, this method should not be called because WorkManager is
+ * automatically initialized. To initialize WorkManager yourself, please follow these steps:
+ * <p><ul>
+ * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest
+ * <li>In {@code Application#onCreate} or a {@code ContentProvider}, call this method before
+ * calling {@link WorkManager#getInstance()}
+ * </ul></p>
+ * This method has no effect if WorkManager is already initialized.
*
* @param context A {@link Context} object for configuration purposes. Internally, this class
* will call {@link Context#getApplicationContext()}, so you may safely pass in
@@ -164,7 +184,7 @@ public abstract class WorkManager {
* @return A {@link WorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- public final WorkContinuation beginWith(@NonNull OneTimeWorkRequest...work) {
+ public final @NonNull WorkContinuation beginWith(@NonNull OneTimeWorkRequest...work) {
return beginWith(Arrays.asList(work));
}
@@ -176,7 +196,7 @@ public abstract class WorkManager {
* @return A {@link WorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- public abstract WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);
+ public abstract @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);
/**
* This method allows you to begin unique chains of work for situations where you only want one
@@ -201,7 +221,7 @@ public abstract class WorkManager {
* as a child of all leaf nodes labelled with {@code uniqueWorkName}.
* @return A {@link WorkContinuation} that allows further chaining
*/
- public final WorkContinuation beginUniqueWork(
+ public final @NonNull WorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull OneTimeWorkRequest... work) {
@@ -231,7 +251,7 @@ public abstract class WorkManager {
* as a child of all leaf nodes labelled with {@code uniqueWorkName}.
* @return A {@link WorkContinuation} that allows further chaining
*/
- public abstract WorkContinuation beginUniqueWork(
+ public abstract @NonNull WorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work);
@@ -307,17 +327,19 @@ public abstract class WorkManager {
* must be updated or deleted in case someone cancels their work without their prior knowledge.
*
* @return A {@link LiveData} of the timestamp in milliseconds when method that cancelled all
- * work was last invoked
+ * work was last invoked; this timestamp may be {@code 0L} if this never occurred.
*/
- public abstract LiveData<Long> getLastCancelAllTimeMillis();
+ public abstract @NonNull LiveData<Long> getLastCancelAllTimeMillis();
/**
* Gets a {@link LiveData} of the {@link WorkStatus} for a given work id.
*
* @param id The id of the work
- * @return A {@link LiveData} of the {@link WorkStatus} associated with {@code id}
+ * @return A {@link LiveData} of the {@link WorkStatus} associated with {@code id}; note that
+ * this {@link WorkStatus} may be {@code null} if {@code id} is not known to
+ * WorkManager.
*/
- public abstract LiveData<WorkStatus> getStatusById(@NonNull UUID id);
+ public abstract @NonNull LiveData<WorkStatus> getStatusById(@NonNull UUID id);
/**
* Gets a {@link LiveData} of the {@link WorkStatus} for all work for a given tag.
@@ -325,7 +347,7 @@ public abstract class WorkManager {
* @param tag The tag of the work
* @return A {@link LiveData} list of {@link WorkStatus} for work tagged with {@code tag}
*/
- public abstract LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag);
+ public abstract @NonNull LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag);
/**
* Gets a {@link LiveData} of the {@link WorkStatus} for all work in a work chain with a given
@@ -335,7 +357,7 @@ public abstract class WorkManager {
* @return A {@link LiveData} of the {@link WorkStatus} for work in the chain named
* {@code uniqueWorkName}
*/
- public abstract LiveData<List<WorkStatus>> getStatusesForUniqueWork(
+ public abstract @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWork(
@NonNull String uniqueWorkName);
/**
@@ -343,7 +365,7 @@ public abstract class WorkManager {
*
* @return A {@link SynchronousWorkManager} object, which gives access to synchronous methods
*/
- public abstract SynchronousWorkManager synchronous();
+ public abstract @NonNull SynchronousWorkManager synchronous();
/**
* @hide
diff --git a/work/workmanager/src/main/java/androidx/work/WorkRequest.java b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
index cfb72b8f759..7a9602c73f6 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
@@ -68,7 +68,7 @@ public abstract class WorkRequest {
*
* @return The identifier for this unit of work
*/
- public UUID getId() {
+ public @NonNull UUID getId() {
return mId;
}
@@ -79,7 +79,7 @@ public abstract class WorkRequest {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public String getStringId() {
+ public @NonNull String getStringId() {
return mId.toString();
}
@@ -90,7 +90,7 @@ public abstract class WorkRequest {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkSpec getWorkSpec() {
+ public @NonNull WorkSpec getWorkSpec() {
return mWorkSpec;
}
@@ -101,7 +101,7 @@ public abstract class WorkRequest {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Set<String> getTags() {
+ public @NonNull Set<String> getTags() {
return mTags;
}
@@ -136,7 +136,7 @@ public abstract class WorkRequest {
* @param timeUnit The {@link TimeUnit} for {@code backoffDelay}
* @return The current {@link Builder}
*/
- public B setBackoffCriteria(
+ public @NonNull B setBackoffCriteria(
@NonNull BackoffPolicy backoffPolicy,
long backoffDelay,
@NonNull TimeUnit timeUnit) {
@@ -152,7 +152,7 @@ public abstract class WorkRequest {
* @param constraints The constraints for the work
* @return The current {@link Builder}
*/
- public B setConstraints(@NonNull Constraints constraints) {
+ public @NonNull B setConstraints(@NonNull Constraints constraints) {
mWorkSpec.constraints = constraints;
return getThis();
}
@@ -163,7 +163,7 @@ public abstract class WorkRequest {
* @param inputData key/value pairs that will be provided to the {@link Worker} class
* @return The current {@link Builder}
*/
- public B setInputData(@NonNull Data inputData) {
+ public @NonNull B setInputData(@NonNull Data inputData) {
mWorkSpec.input = inputData;
return getThis();
}
@@ -175,7 +175,7 @@ public abstract class WorkRequest {
* @param tag A tag for identifying the work in queries.
* @return The current {@link Builder}
*/
- public B addTag(@NonNull String tag) {
+ public @NonNull B addTag(@NonNull String tag) {
mTags.add(tag);
return getThis();
}
@@ -196,7 +196,7 @@ public abstract class WorkRequest {
* @param timeUnit The unit of time for {@code duration}
* @return The current {@link Builder}
*/
- public B keepResultsForAtLeast(long duration, @NonNull TimeUnit timeUnit) {
+ public @NonNull B keepResultsForAtLeast(long duration, @NonNull TimeUnit timeUnit) {
mWorkSpec.minimumRetentionDuration = timeUnit.toMillis(duration);
return getThis();
}
@@ -216,7 +216,7 @@ public abstract class WorkRequest {
* @return The current {@link Builder}
*/
@RequiresApi(26)
- public B keepResultsForAtLeast(@NonNull Duration duration) {
+ public @NonNull B keepResultsForAtLeast(@NonNull Duration duration) {
mWorkSpec.minimumRetentionDuration = duration.toMillis();
return getThis();
}
@@ -226,9 +226,9 @@ public abstract class WorkRequest {
*
* @return The concrete implementation of the work associated with this builder
*/
- public abstract W build();
+ public abstract @NonNull W build();
- abstract B getThis();
+ abstract @NonNull B getThis();
/**
* Set the initial state for this work. Used in testing only.
@@ -239,7 +239,7 @@ public abstract class WorkRequest {
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
- public B setInitialState(@NonNull State state) {
+ public @NonNull B setInitialState(@NonNull State state) {
mWorkSpec.state = state;
return getThis();
}
@@ -253,7 +253,7 @@ public abstract class WorkRequest {
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
- public B setInitialRunAttemptCount(int runAttemptCount) {
+ public @NonNull B setInitialRunAttemptCount(int runAttemptCount) {
mWorkSpec.runAttemptCount = runAttemptCount;
return getThis();
}
@@ -268,7 +268,7 @@ public abstract class WorkRequest {
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
- public B setPeriodStartTime(long periodStartTime, @NonNull TimeUnit timeUnit) {
+ public @NonNull B setPeriodStartTime(long periodStartTime, @NonNull TimeUnit timeUnit) {
mWorkSpec.periodStartTime = timeUnit.toMillis(periodStartTime);
return getThis();
}
@@ -283,7 +283,7 @@ public abstract class WorkRequest {
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
- public B setScheduleRequestedAt(
+ public @NonNull B setScheduleRequestedAt(
long scheduleRequestedAt,
@NonNull TimeUnit timeUnit) {
mWorkSpec.scheduleRequestedAt = timeUnit.toMillis(scheduleRequestedAt);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/Schedulers.java b/work/workmanager/src/main/java/androidx/work/impl/Schedulers.java
index 4944549d6fa..041f7880ce8 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/Schedulers.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/Schedulers.java
@@ -65,41 +65,38 @@ public class Schedulers {
@NonNull Configuration configuration,
@NonNull WorkDatabase workDatabase,
List<Scheduler> schedulers) {
-
- WorkSpecDao workSpecDao = workDatabase.workSpecDao();
- List<WorkSpec> eligibleWorkSpecs =
- workSpecDao.getEligibleWorkForScheduling(
- configuration.getMaxSchedulerLimit());
- scheduleInternal(workDatabase, schedulers, eligibleWorkSpecs);
- }
-
- private static void scheduleInternal(
- @NonNull WorkDatabase workDatabase,
- List<Scheduler> schedulers,
- List<WorkSpec> workSpecs) {
-
- if (workSpecs == null || schedulers == null) {
+ if (schedulers == null || schedulers.size() == 0) {
return;
}
- long now = System.currentTimeMillis();
WorkSpecDao workSpecDao = workDatabase.workSpecDao();
- // Mark all the WorkSpecs as scheduled.
- // Calls to Scheduler#schedule() could potentially result in more schedules
- // on a separate thread. Therefore, this needs to be done first.
+ List<WorkSpec> eligibleWorkSpecs;
+
workDatabase.beginTransaction();
try {
- for (WorkSpec workSpec : workSpecs) {
- workSpecDao.markWorkSpecScheduled(workSpec.id, now);
+ eligibleWorkSpecs = workSpecDao.getEligibleWorkForScheduling(
+ configuration.getMaxSchedulerLimit());
+ if (eligibleWorkSpecs != null && eligibleWorkSpecs.size() > 0) {
+ long now = System.currentTimeMillis();
+
+ // Mark all the WorkSpecs as scheduled.
+ // Calls to Scheduler#schedule() could potentially result in more schedules
+ // on a separate thread. Therefore, this needs to be done first.
+ for (WorkSpec workSpec : eligibleWorkSpecs) {
+ workSpecDao.markWorkSpecScheduled(workSpec.id, now);
+ }
}
workDatabase.setTransactionSuccessful();
} finally {
workDatabase.endTransaction();
}
- WorkSpec[] eligibleWorkSpecsArray = workSpecs.toArray(new WorkSpec[0]);
- // Delegate to the underlying scheduler.
- for (Scheduler scheduler : schedulers) {
- scheduler.schedule(eligibleWorkSpecsArray);
+
+ if (eligibleWorkSpecs != null && eligibleWorkSpecs.size() > 0) {
+ WorkSpec[] eligibleWorkSpecsArray = eligibleWorkSpecs.toArray(new WorkSpec[0]);
+ // Delegate to the underlying scheduler.
+ for (Scheduler scheduler : schedulers) {
+ scheduler.schedule(eligibleWorkSpecsArray);
+ }
}
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
index 3713dca4850..6fdc18634b2 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
@@ -148,7 +148,7 @@ public class WorkContinuationImpl extends WorkContinuation
}
@Override
- public WorkContinuation then(List<OneTimeWorkRequest> work) {
+ public @NonNull WorkContinuation then(List<OneTimeWorkRequest> work) {
// TODO (rahulrav@) We need to decide if we want to allow chaining of continuations after
// an initial call to enqueue()
return new WorkContinuationImpl(mWorkManagerImpl,
@@ -159,12 +159,12 @@ public class WorkContinuationImpl extends WorkContinuation
}
@Override
- public LiveData<List<WorkStatus>> getStatuses() {
+ public @NonNull LiveData<List<WorkStatus>> getStatuses() {
return mWorkManagerImpl.getStatusesById(mAllIds);
}
@Override
- public List<WorkStatus> getStatusesSync() {
+ public @NonNull List<WorkStatus> getStatusesSync() {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
throw new IllegalStateException("Cannot getStatusesSync on main thread!");
}
@@ -201,12 +201,12 @@ public class WorkContinuationImpl extends WorkContinuation
}
@Override
- public SynchronousWorkContinuation synchronous() {
+ public @NonNull SynchronousWorkContinuation synchronous() {
return this;
}
@Override
- protected WorkContinuation combineInternal(
+ protected @NonNull WorkContinuation combineInternal(
@Nullable OneTimeWorkRequest work,
@NonNull List<WorkContinuation> continuations) {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
index e5d5f9d0f98..b8bc72c3119 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
@@ -16,6 +16,9 @@
package androidx.work.impl;
+import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
import static androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES;
import static androidx.work.impl.model.WorkTypeConverters.StateIds.ENQUEUED;
import static androidx.work.impl.model.WorkTypeConverters.StateIds.RUNNING;
@@ -56,12 +59,14 @@ import java.util.concurrent.TimeUnit;
WorkTag.class,
SystemIdInfo.class,
WorkName.class},
- version = 2)
+ version = 4)
@TypeConverters(value = {Data.class, WorkTypeConverters.class})
public abstract class WorkDatabase extends RoomDatabase {
private static final String DB_NAME = "androidx.work.workdb";
- private static final String CLEANUP_SQL = "UPDATE workspec SET state=" + ENQUEUED
+ private static final String CLEANUP_SQL = "UPDATE workspec "
+ + "SET state=" + ENQUEUED + ","
+ + " schedule_requested_at=" + WorkSpec.SCHEDULE_NOT_REQUESTED_YET
+ " WHERE state=" + RUNNING;
// Delete rows in the workspec table that...
@@ -82,7 +87,7 @@ public abstract class WorkDatabase extends RoomDatabase {
/**
* Creates an instance of the WorkDatabase.
*
- * @param context A context (this method will use the application context from it)
+ * @param context A context (this method will use the application context from it)
* @param useTestDatabase {@code true} to generate an in-memory database that allows main thread
* access
* @return The created WorkDatabase
@@ -95,9 +100,13 @@ public abstract class WorkDatabase extends RoomDatabase {
} else {
builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME);
}
+
return builder.addCallback(generateCleanupCallback())
.addMigrations(WorkDatabaseMigrations.MIGRATION_1_2)
- .addMigrations(WorkDatabaseMigrations.MIGRATION_2_1)
+ .addMigrations(
+ new WorkDatabaseMigrations.WorkMigration(context, VERSION_2, VERSION_3))
+ .addMigrations(MIGRATION_3_4)
+ .fallbackToDestructiveMigration()
.build();
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
index 82a26ac58cd..d75b51473ea 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
@@ -18,9 +18,15 @@ package androidx.work.impl;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.migration.Migration;
+import android.content.Context;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.model.WorkTypeConverters;
+import androidx.work.impl.utils.Preferences;
+
/**
* Migration helpers for {@link androidx.work.impl.WorkDatabase}.
*
@@ -34,30 +40,27 @@ public class WorkDatabaseMigrations {
}
// Known WorkDatabase versions
- private static final int VERSION_1 = 1;
- private static final int VERSION_2 = 2;
+ public static final int VERSION_1 = 1;
+ public static final int VERSION_2 = 2;
+ public static final int VERSION_3 = 3;
+ public static final int VERSION_4 = 4;
private static final String CREATE_SYSTEM_ID_INFO =
"CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`"
+ " INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)"
+ " REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )";
- private static final String CREATE_ALARM_INFO =
- "CREATE TABLE IF NOT EXISTS `alarmInfo` (`work_spec_id` TEXT NOT NULL, `alarm_id`"
- + " INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY"
- + "(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE "
- + "CASCADE )";
-
private static final String MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO =
"INSERT INTO SystemIdInfo(work_spec_id, system_id) "
+ "SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo";
- private static final String MIGRATE_SYSTEM_ID_INFO_TO_ALARM_INFO =
- "INSERT INTO alarmInfo(work_spec_id, alarm_id) "
- + "SELECT work_spec_id, system_id AS alarm_id FROM SystemIdInfo";
+ private static final String PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT =
+ "UPDATE workspec SET schedule_requested_at=0"
+ + " WHERE state NOT IN " + WorkTypeConverters.StateIds.COMPLETED_STATES
+ + " AND schedule_requested_at=" + WorkSpec.SCHEDULE_NOT_REQUESTED_YET
+ + " AND interval_duration<>0";
private static final String REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo";
- private static final String REMOVE_SYSTEM_ID_INFO = "DROP TABLE IF EXISTS SystemIdInfo";
/**
* Removes the {@code alarmInfo} table and substitutes it for a more general
@@ -70,22 +73,39 @@ public class WorkDatabaseMigrations {
database.execSQL(CREATE_SYSTEM_ID_INFO);
database.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO);
database.execSQL(REMOVE_ALARM_INFO);
- database.execSQL("INSERT INTO worktag(tag, work_spec_id) "
+ database.execSQL("INSERT OR IGNORE INTO worktag(tag, work_spec_id) "
+ "SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec");
}
};
/**
- * Removes the {@code alarmInfo} table and substitutes it for a more general
- * {@code SystemIdInfo} table.
+ * A {@link WorkDatabase} migration that reschedules all eligible Workers.
+ */
+ public static class WorkMigration extends Migration {
+ final Context mContext;
+
+ public WorkMigration(@NonNull Context context, int startVersion, int endVersion) {
+ super(startVersion, endVersion);
+ mContext = context;
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ Preferences preferences = new Preferences(mContext);
+ preferences.setNeedsReschedule(true);
+ }
+ }
+
+ /**
+ * Marks {@code SCHEDULE_REQUESTED_AT} to something other than
+ * {@code SCHEDULE_NOT_REQUESTED_AT}.
*/
- public static Migration MIGRATION_2_1 = new Migration(VERSION_2, VERSION_1) {
+ public static Migration MIGRATION_3_4 = new Migration(VERSION_3, VERSION_4) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_ALARM_INFO);
- database.execSQL(MIGRATE_SYSTEM_ID_INFO_TO_ALARM_INFO);
- database.execSQL(REMOVE_SYSTEM_ID_INFO);
- // Don't remove implicit tags; they may have been added by the developer.
+ if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+ database.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT);
+ }
}
};
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 0dcdda349c6..763a4544437 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -18,7 +18,9 @@ package androidx.work.impl;
import android.arch.core.util.Function;
import android.arch.lifecycle.LiveData;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.os.Build;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -37,6 +39,7 @@ import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import androidx.work.WorkStatus;
import androidx.work.impl.background.greedy.GreedyScheduler;
+import androidx.work.impl.background.systemjob.SystemJobScheduler;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
import androidx.work.impl.utils.CancelWorkRunnable;
@@ -72,6 +75,8 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
private List<Scheduler> mSchedulers;
private Processor mProcessor;
private Preferences mPreferences;
+ private boolean mForceStopRunnableCompleted;
+ private BroadcastReceiver.PendingResult mRescheduleReceiverResult;
private static WorkManagerImpl sDelegatedInstance = null;
private static WorkManagerImpl sDefaultInstance = null;
@@ -97,7 +102,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static WorkManagerImpl getInstance() {
+ public static @Nullable WorkManagerImpl getInstance() {
synchronized (sLock) {
if (sDelegatedInstance != null) {
return sDelegatedInstance;
@@ -172,6 +177,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
getSchedulers(),
configuration.getExecutor());
mPreferences = new Preferences(mContext);
+ mForceStopRunnableCompleted = false;
// Checks for app force stops.
mTaskExecutor.executeOnBackgroundThread(new ForceStopRunnable(context, this));
@@ -265,12 +271,12 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work) {
+ public @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work) {
return new WorkContinuationImpl(this, work);
}
@Override
- public WorkContinuation beginUniqueWork(
+ public @NonNull WorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work) {
@@ -370,7 +376,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public LiveData<Long> getLastCancelAllTimeMillis() {
+ public @NonNull LiveData<Long> getLastCancelAllTimeMillis() {
return mPreferences.getLastCancelAllTimeMillisLiveData();
}
@@ -392,7 +398,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public LiveData<WorkStatus> getStatusById(@NonNull UUID id) {
+ public @NonNull LiveData<WorkStatus> getStatusById(@NonNull UUID id) {
WorkSpecDao dao = mWorkDatabase.workSpecDao();
LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
dao.getWorkStatusPojoLiveDataForIds(Collections.singletonList(id.toString()));
@@ -423,7 +429,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag) {
+ public @NonNull LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
workSpecDao.getWorkStatusPojoLiveDataForTag(tag);
@@ -431,7 +437,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public List<WorkStatus> getStatusesByTagSync(@NonNull String tag) {
+ public @NonNull List<WorkStatus> getStatusesByTagSync(@NonNull String tag) {
assertBackgroundThread("Cannot call getStatusesByTagSync on main thread!");
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
List<WorkSpec.WorkStatusPojo> input = workSpecDao.getWorkStatusPojoForTag(tag);
@@ -439,7 +445,8 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public LiveData<List<WorkStatus>> getStatusesForUniqueWork(@NonNull String uniqueWorkName) {
+ public @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWork(
+ @NonNull String uniqueWorkName) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
workSpecDao.getWorkStatusPojoLiveDataForName(uniqueWorkName);
@@ -447,7 +454,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public List<WorkStatus> getStatusesForUniqueWorkSync(@NonNull String uniqueWorkName) {
+ public @NonNull List<WorkStatus> getStatusesForUniqueWorkSync(@NonNull String uniqueWorkName) {
assertBackgroundThread("Cannot call getStatusesByNameBlocking on main thread!");
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
List<WorkSpec.WorkStatusPojo> input = workSpecDao.getWorkStatusPojoForName(uniqueWorkName);
@@ -455,7 +462,7 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
}
@Override
- public SynchronousWorkManager synchronous() {
+ public @NonNull SynchronousWorkManager synchronous() {
return this;
}
@@ -510,6 +517,11 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void rescheduleEligibleWork() {
+ // TODO (rahulrav@) Make every scheduler do its own cancelAll().
+ if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+ SystemJobScheduler.jobSchedulerCancelAll(getApplicationContext());
+ }
+
// Reset scheduled state.
getWorkDatabase().workSpecDao().resetScheduledState();
@@ -519,6 +531,42 @@ public class WorkManagerImpl extends WorkManager implements SynchronousWorkManag
Schedulers.schedule(getConfiguration(), getWorkDatabase(), getSchedulers());
}
+ /**
+ * A way for {@link ForceStopRunnable} to tell {@link WorkManagerImpl} that it has completed.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public void onForceStopRunnableCompleted() {
+ synchronized (sLock) {
+ mForceStopRunnableCompleted = true;
+ if (mRescheduleReceiverResult != null) {
+ mRescheduleReceiverResult.finish();
+ mRescheduleReceiverResult = null;
+ }
+ }
+ }
+
+ /**
+ * This method is invoked by
+ * {@link androidx.work.impl.background.systemalarm.RescheduleReceiver}
+ * after a call to {@link BroadcastReceiver#goAsync()}. Once {@link ForceStopRunnable} is done,
+ * we can safely call {@link BroadcastReceiver.PendingResult#finish()}.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public void setReschedulePendingResult(
+ @NonNull BroadcastReceiver.PendingResult rescheduleReceiverResult) {
+ synchronized (sLock) {
+ mRescheduleReceiverResult = rescheduleReceiverResult;
+ if (mForceStopRunnableCompleted) {
+ mRescheduleReceiverResult.finish();
+ mRescheduleReceiverResult = null;
+ }
+ }
+ }
+
private void assertBackgroundThread(String errorMessage) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
throw new IllegalStateException(errorMessage);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index 0b8c44c83e7..87f922fc6bb 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -21,8 +21,10 @@ import static androidx.work.State.ENQUEUED;
import static androidx.work.State.FAILED;
import static androidx.work.State.RUNNING;
import static androidx.work.State.SUCCEEDED;
+import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
import android.content.Context;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
@@ -94,18 +96,28 @@ public class WorkerWrapper implements Runnable {
return;
}
- mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
- if (mWorkSpec == null) {
- Log.e(TAG, String.format("Didn't find WorkSpec for id %s", mWorkSpecId));
- notifyListener(false, false);
- return;
- }
+ mWorkDatabase.beginTransaction();
+ try {
+ mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
+ if (mWorkSpec == null) {
+ Log.e(TAG, String.format("Didn't find WorkSpec for id %s", mWorkSpecId));
+ notifyListener(false, false);
+ return;
+ }
- // Do a quick check to make sure we don't need to bail out in case this work is already
- // running, finished, or is blocked.
- if (mWorkSpec.state != ENQUEUED) {
- notifyIncorrectStatus();
- return;
+ // Do a quick check to make sure we don't need to bail out in case this work is already
+ // running, finished, or is blocked.
+ if (mWorkSpec.state != ENQUEUED) {
+ notifyIncorrectStatus();
+ mWorkDatabase.setTransactionSuccessful();
+ return;
+ }
+
+ // Needed for nested transactions, such as when we're in a dependent work request when
+ // using a SynchronousExecutor.
+ mWorkDatabase.setTransactionSuccessful();
+ } finally {
+ mWorkDatabase.endTransaction();
}
// Merge inputs. This can be potentially expensive code, so this should not be done inside
@@ -157,6 +169,11 @@ public class WorkerWrapper implements Runnable {
result = mWorker.doWork();
} catch (Exception | Error e) {
result = Worker.Result.FAILURE;
+ Log.e(TAG,
+ String.format(
+ "Worker %s failed because it threw an exception/error",
+ mWorkSpecId),
+ e);
}
try {
@@ -275,9 +292,9 @@ public class WorkerWrapper implements Runnable {
if (currentState == ENQUEUED) {
mWorkSpecDao.setState(RUNNING, mWorkSpecId);
mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId);
- mWorkDatabase.setTransactionSuccessful();
setToRunning = true;
}
+ mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
@@ -339,11 +356,29 @@ public class WorkerWrapper implements Runnable {
mWorkSpecDao.setPeriodStartTime(mWorkSpecId, nextPeriodStartTime);
mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
mWorkSpecDao.resetWorkSpecRunAttemptCount(mWorkSpecId);
+ if (Build.VERSION.SDK_INT < WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+ // We only need to reset the schedule_requested_at bit for the AlarmManager
+ // implementation because AlarmManager does not know about periodic WorkRequests.
+ // Otherwise we end up double scheduling the Worker with an identical jobId, and
+ // JobScheduler treats it as the first schedule for a PeriodicWorker. With the
+ // AlarmManager implementation, this is not an problem as AlarmManager only cares
+ // about the actual alarm itself.
+
+ // We need to tell the schedulers that this WorkSpec is no longer occupying a slot.
+ mWorkSpecDao.markWorkSpecScheduled(mWorkSpecId, SCHEDULE_NOT_REQUESTED_YET);
+ }
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
notifyListener(isSuccessful, false);
}
+
+ // We need to tell the Schedulers to pick up this newly ENQUEUED Worker.
+ // TODO (rahulrav@) Move this into the Scheduler itself.
+ if (Build.VERSION.SDK_INT <= WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL) {
+ // Reschedule the periodic work.
+ Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers);
+ }
}
private void setSucceededAndNotify() {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/RescheduleReceiver.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/RescheduleReceiver.java
index a331479a5bc..b8adec2ab40 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/RescheduleReceiver.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/RescheduleReceiver.java
@@ -20,15 +20,10 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
-import androidx.work.WorkManager;
import androidx.work.impl.WorkManagerImpl;
-import java.util.concurrent.TimeUnit;
-
/**
* Reschedules alarms on BOOT_COMPLETED and other similar scenarios.
*/
@@ -39,22 +34,15 @@ public class RescheduleReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
- if (WorkManager.getInstance() == null) {
+ WorkManagerImpl workManager = WorkManagerImpl.getInstance();
+ if (workManager == null) {
// WorkManager has not already been initialized.
Log.e(TAG,
"Cannot reschedule jobs. WorkManager needs to be initialized via a "
+ "ContentProvider#onCreate() or an Application#onCreate().");
} else {
- // This helps set up rescheduling of Jobs with JobScheduler. We are doing nothing
- // for 10 seconds, to give ForceStopRunnable a chance to reschedule.
- Handler handler = new Handler(Looper.getMainLooper());
final PendingResult pendingResult = goAsync();
- handler.postDelayed(new Runnable() {
- @Override
- public void run() {
- pendingResult.finish();
- }
- }, TimeUnit.SECONDS.toMillis(10));
+ workManager.setReschedulePendingResult(pendingResult);
}
} else {
Intent reschedule = CommandHandler.createRescheduleIntent(context);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
index 6f5120da5bf..52d30903aea 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
@@ -114,6 +114,14 @@ public class WorkSpec {
@ColumnInfo(name = "minimum_retention_duration")
public long minimumRetentionDuration;
+ /**
+ * This field tells us if this {@link WorkSpec} instance, is actually currently scheduled and
+ * being counted against the {@code SCHEDULER_LIMIT}. This bit is reset for PeriodicWorkRequests
+ * in API < 23, because AlarmManager does not know of PeriodicWorkRequests. So for the next
+ * request to be rescheduled this field has to be reset to {@code SCHEDULE_NOT_REQUESTED_AT}.
+ * For the JobScheduler implementation, we don't reset this field because JobScheduler natively
+ * supports PeriodicWorkRequests.
+ */
@ColumnInfo(name = "schedule_requested_at")
public long scheduleRequestedAt = SCHEDULE_NOT_REQUESTED_YET;
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 87de105e455..c01e4fdbd81 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -20,10 +20,8 @@ import static android.app.AlarmManager.RTC_WAKEUP;
import static android.app.PendingIntent.FLAG_NO_CREATE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
-import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -34,7 +32,6 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;
import androidx.work.impl.WorkManagerImpl;
-import androidx.work.impl.background.systemjob.SystemJobScheduler;
import java.util.concurrent.TimeUnit;
@@ -67,16 +64,16 @@ public class ForceStopRunnable implements Runnable {
@Override
public void run() {
- if (shouldCancelPersistedJobs()) {
- cancelAllInJobScheduler();
- Log.d(TAG, "Migrating persisted jobs.");
+ if (shouldRescheduleWorkers()) {
+ Log.d(TAG, "Rescheduling Workers.");
mWorkManager.rescheduleEligibleWork();
// Mark the jobs as migrated.
- mWorkManager.getPreferences().setMigratedPersistedJobs();
+ mWorkManager.getPreferences().setNeedsReschedule(false);
} else if (isForceStopped()) {
Log.d(TAG, "Application was force-stopped, rescheduling.");
mWorkManager.rescheduleEligibleWork();
}
+ mWorkManager.onForceStopRunnableCompleted();
}
/**
@@ -98,12 +95,11 @@ public class ForceStopRunnable implements Runnable {
}
/**
- * @return {@code true} If persisted jobs in JobScheduler need to be cancelled.
+ * @return {@code true} If we need to reschedule Workers.
*/
@VisibleForTesting
- public boolean shouldCancelPersistedJobs() {
- return Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL
- && mWorkManager.getPreferences().shouldMigratePersistedJobs();
+ public boolean shouldRescheduleWorkers() {
+ return mWorkManager.getPreferences().needsReschedule();
}
/**
@@ -128,16 +124,7 @@ public class ForceStopRunnable implements Runnable {
return intent;
}
- /**
- * Cancels all the persisted jobs in {@link JobScheduler}.
- */
- @VisibleForTesting
- @TargetApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
- public void cancelAllInJobScheduler() {
- SystemJobScheduler.jobSchedulerCancelAll(mContext);
- }
-
- private void setAlarm(int alarmId) {
+ void setAlarm(int alarmId) {
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
// Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm.
PendingIntent pendingIntent = getPendingIntent(alarmId, FLAG_UPDATE_CURRENT);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/Preferences.java b/work/workmanager/src/main/java/androidx/work/impl/utils/Preferences.java
index 49cd262ffcb..d8b9484cb27 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/Preferences.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/Preferences.java
@@ -20,7 +20,9 @@ import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.content.Context;
import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
/**
* Preferences for WorkManager.
@@ -35,13 +37,17 @@ public class Preferences {
private static final String PREFERENCES_FILE_NAME = "androidx.work.util.preferences";
private static final String KEY_LAST_CANCEL_ALL_TIME_MS = "last_cancel_all_time_ms";
- private static final String KEY_MIGRATE_PERSISTED_JOBS = "migrate_persisted_jobs";
+ private static final String KEY_RESCHEDULE_NEEDED = "reschedule_needed";
private SharedPreferences mSharedPreferences;
- public Preferences(Context context) {
- mSharedPreferences =
- context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
+ public Preferences(@NonNull Context context) {
+ this(context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE));
+ }
+
+ @VisibleForTesting
+ public Preferences(@NonNull SharedPreferences preferences) {
+ mSharedPreferences = preferences;
}
/**
@@ -69,20 +75,19 @@ public class Preferences {
}
/**
- * @return {@code true} When we should migrate from persisted jobs to non-persisted jobs in
- * {@link android.app.job.JobScheduler}
+ * @return {@code true} When we should reschedule workers.
*/
- public boolean shouldMigratePersistedJobs() {
+ public boolean needsReschedule() {
+ // This preference is being set by a Room Migration.
// TODO Remove this before WorkManager 1.0 beta.
- return mSharedPreferences.getBoolean(KEY_MIGRATE_PERSISTED_JOBS, true);
+ return mSharedPreferences.getBoolean(KEY_RESCHEDULE_NEEDED, false);
}
/**
- * Updates the key which indicates that we have migrated all our persisted jobs in
- * {@link android.app.job.JobScheduler}.
+ * Updates the key which indicates that we have rescheduled jobs.
*/
- public void setMigratedPersistedJobs() {
- mSharedPreferences.edit().putBoolean(KEY_MIGRATE_PERSISTED_JOBS, true).apply();
+ public void setNeedsReschedule(boolean needsReschedule) {
+ mSharedPreferences.edit().putBoolean(KEY_RESCHEDULE_NEEDED, needsReschedule).apply();
}
/**
diff --git a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
index 116e96cc3fe..e023fed6ff5 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
@@ -64,7 +64,7 @@ public class ConstraintTrackingWorker extends Worker implements WorkConstraintsC
@Override
public @NonNull Result doWork() {
- String className = getInputData().getString(ARGUMENT_CLASS_NAME, null);
+ String className = getInputData().getString(ARGUMENT_CLASS_NAME);
if (TextUtils.isEmpty(className)) {
Log.d(TAG, "No worker to delegate to.");
return Result.FAILURE;
diff --git a/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/3.json b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/3.json
new file mode 100644
index 00000000000..69e73eb68f0
--- /dev/null
+++ b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/3.json
@@ -0,0 +1,363 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "c45e5fcbdf3824dead9778f19e2fd8af",
+ "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": {
+ "columnNames": [
+ "work_spec_id",
+ "prerequisite_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Dependency_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `index_Dependency_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+ },
+ {
+ "name": "index_Dependency_prerequisite_id",
+ "unique": false,
+ "columnNames": [
+ "prerequisite_id"
+ ],
+ "createSql": "CREATE INDEX `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, `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, `period_start_time` INTEGER NOT NULL, `minimum_retention_duration` INTEGER NOT NULL, `schedule_requested_at` INTEGER NOT NULL, `required_network_type` INTEGER, `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, `content_uri_triggers` BLOB, 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": false
+ },
+ {
+ "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": "periodStartTime",
+ "columnName": "period_start_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "minimumRetentionDuration",
+ "columnName": "minimum_retention_duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduleRequestedAt",
+ "columnName": "schedule_requested_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiredNetworkType",
+ "columnName": "required_network_type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "constraints.mRequiresCharging",
+ "columnName": "requires_charging",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresDeviceIdle",
+ "columnName": "requires_device_idle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresBatteryNotLow",
+ "columnName": "requires_battery_not_low",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresStorageNotLow",
+ "columnName": "requires_storage_not_low",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mContentUriTriggers",
+ "columnName": "content_uri_triggers",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkSpec_schedule_requested_at",
+ "unique": false,
+ "columnNames": [
+ "schedule_requested_at"
+ ],
+ "createSql": "CREATE INDEX `index_WorkSpec_schedule_requested_at` ON `${TABLE_NAME}` (`schedule_requested_at`)"
+ }
+ ],
+ "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": {
+ "columnNames": [
+ "tag",
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkTag_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `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, `system_id` INTEGER 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": "systemId",
+ "columnName": "system_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "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": {
+ "columnNames": [
+ "name",
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkName_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `index_WorkName_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "WorkSpec",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "work_spec_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, \"c45e5fcbdf3824dead9778f19e2fd8af\")"
+ ]
+ }
+} \ No newline at end of file
diff --git a/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/4.json b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/4.json
new file mode 100644
index 00000000000..63c3005deda
--- /dev/null
+++ b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/4.json
@@ -0,0 +1,363 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "c45e5fcbdf3824dead9778f19e2fd8af",
+ "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": {
+ "columnNames": [
+ "work_spec_id",
+ "prerequisite_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Dependency_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `index_Dependency_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+ },
+ {
+ "name": "index_Dependency_prerequisite_id",
+ "unique": false,
+ "columnNames": [
+ "prerequisite_id"
+ ],
+ "createSql": "CREATE INDEX `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, `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, `period_start_time` INTEGER NOT NULL, `minimum_retention_duration` INTEGER NOT NULL, `schedule_requested_at` INTEGER NOT NULL, `required_network_type` INTEGER, `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, `content_uri_triggers` BLOB, 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": false
+ },
+ {
+ "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": "periodStartTime",
+ "columnName": "period_start_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "minimumRetentionDuration",
+ "columnName": "minimum_retention_duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduleRequestedAt",
+ "columnName": "schedule_requested_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiredNetworkType",
+ "columnName": "required_network_type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "constraints.mRequiresCharging",
+ "columnName": "requires_charging",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresDeviceIdle",
+ "columnName": "requires_device_idle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresBatteryNotLow",
+ "columnName": "requires_battery_not_low",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mRequiresStorageNotLow",
+ "columnName": "requires_storage_not_low",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "constraints.mContentUriTriggers",
+ "columnName": "content_uri_triggers",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkSpec_schedule_requested_at",
+ "unique": false,
+ "columnNames": [
+ "schedule_requested_at"
+ ],
+ "createSql": "CREATE INDEX `index_WorkSpec_schedule_requested_at` ON `${TABLE_NAME}` (`schedule_requested_at`)"
+ }
+ ],
+ "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": {
+ "columnNames": [
+ "tag",
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkTag_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `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, `system_id` INTEGER 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": "systemId",
+ "columnName": "system_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "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": {
+ "columnNames": [
+ "name",
+ "work_spec_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_WorkName_work_spec_id",
+ "unique": false,
+ "columnNames": [
+ "work_spec_id"
+ ],
+ "createSql": "CREATE INDEX `index_WorkName_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "WorkSpec",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "work_spec_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, \"c45e5fcbdf3824dead9778f19e2fd8af\")"
+ ]
+ }
+} \ No newline at end of file
diff --git a/work/workmanager/src/test/java/androidx/work/DataTest.java b/work/workmanager/src/test/java/androidx/work/DataTest.java
index 3573b0a0ad1..04b502b7d62 100644
--- a/work/workmanager/src/test/java/androidx/work/DataTest.java
+++ b/work/workmanager/src/test/java/androidx/work/DataTest.java
@@ -19,6 +19,7 @@ package androidx.work;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
@@ -125,14 +126,14 @@ public class DataTest {
Data data = dataBuilder.build();
assertThat(data.getInt("int", 0), is(1));
assertThat(data.getFloat("float", 0f), is(99f));
- assertThat(data.getString("String", null), is("two"));
+ assertThat(data.getString("String"), is("two"));
long[] longArray = data.getLongArray("long array");
assertThat(longArray, is(notNullValue()));
assertThat(longArray.length, is(3));
assertThat(longArray[0], is(1L));
assertThat(longArray[1], is(2L));
assertThat(longArray[2], is(3L));
- assertThat(data.getString("null", "dummy"), is("dummy"));
+ assertThat(data.getString("null"), is(nullValue()));
}
@Test
diff --git a/work/workmanager/src/test/java/androidx/work/OverwritingInputMergerTest.java b/work/workmanager/src/test/java/androidx/work/OverwritingInputMergerTest.java
index 40e703c6b61..b210a6ddee2 100644
--- a/work/workmanager/src/test/java/androidx/work/OverwritingInputMergerTest.java
+++ b/work/workmanager/src/test/java/androidx/work/OverwritingInputMergerTest.java
@@ -42,7 +42,7 @@ public class OverwritingInputMergerTest {
Data output = getOutputFor(input);
assertThat(output.size(), is(1));
- assertThat(output.getString(key, null), is(value));
+ assertThat(output.getString(key), is(value));
}
@Test
@@ -67,9 +67,9 @@ public class OverwritingInputMergerTest {
Data output = getOutputFor(input1, input2);
assertThat(output.size(), is(3));
- assertThat(output.getString(key1, null), is(value1a));
- assertThat(output.getString(key2, null), is(value2));
- assertThat(output.getString(key3, null), is(value3));
+ assertThat(output.getString(key1), is(value1a));
+ assertThat(output.getString(key2), is(value2));
+ assertThat(output.getString(key3), is(value3));
}
private Data getOutputFor(Data... inputs) {