Merge "Standardize to more kt-compatible syntax" into androidx-main
diff --git a/buildSrc-tests/project-subsets/build.gradle b/buildSrc-tests/project-subsets/build.gradle
new file mode 100644
index 0000000..cfeacda
--- /dev/null
+++ b/buildSrc-tests/project-subsets/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.BuildServerConfigurationKt
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+dependencies {
+ implementation gradleTestKit()
+ testImplementation JUNIT
+}
+tasks["test"].configure { t ->
+ // The output of this task can potentially depend on the contents of settings.gradle
+ // and any files referenced by it, including every build.gradle and any scripts or plugins they
+ // apply too. We don't have a convenient way to identify the true inputs here, so we always
+ // run this task and leave details around incrementality up to the Gradle builds that we spawn
+ t.outputs.upToDateWhen { false }
+}
diff --git a/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
new file mode 100644
index 0000000..02cb205
--- /dev/null
+++ b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Test
+import java.io.File
+
+/**
+ * This class tests that each of the project subsets defined in settings.gradle can be built
+ * successfully and does not involve a project that attempts to reference
+ * another project defined only in a different subset, b/172277767 .
+ *
+ * This is implemented using the Gradle TestKit so it can be
+ * run in parallel with other tasks, b/180012150 .
+ */
+public class ProjectSubsetsTest {
+ @Test
+ fun testSubsetMain() {
+ validateSubset("main")
+ }
+
+ @Test
+ fun testSubsetCompose() {
+ validateSubset("compose")
+ }
+
+ @Test
+ fun testSubsetFlan() {
+ validateSubset("flan")
+ }
+
+ @Test
+ fun testSubsetMedia() {
+ validateSubset("media")
+ }
+
+ @Test
+ fun testSubsetWear() {
+ validateSubset("wear")
+ }
+
+ /**
+ * Validates a specific project subset
+ */
+ fun validateSubset(name: String) {
+ GradleRunner.create()
+ .withProjectDir(File("../..").normalize())
+ .withArguments("-Pandroidx.projects=$name", "tasks")
+ .build(); // fails the test if the build fails
+ }
+}
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
index ee7aa5d..07ca269 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
@@ -100,8 +100,8 @@
}
@Test
- fun testValidTestConfigXml_runFullTests() {
- builder.runFullTests(false)
+ fun testValidTestConfigXml_runAllTests() {
+ builder.runAllTests(false)
validate(builder.build())
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 37d6fe4..f5146b1885 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -64,6 +64,12 @@
const val ENABLE_DOCUMENTATION = "androidx.enableDocumentation"
/**
+ * Adjusts the set of projects participating in this build.
+ * See settings.gradle for more information
+ */
+const val PROJECT_SUBSET = "androidx.projects"
+
+/**
* Setting this property puts a summary of the relevant failure messages into standard error
*/
const val SUMMARIZE_STANDARD_ERROR = "androidx.summarizeStderr"
@@ -115,6 +121,7 @@
COVERAGE_ENABLED,
DISPLAY_TEST_OUTPUT,
ENABLE_DOCUMENTATION,
+ PROJECT_SUBSET,
STUDIO_TYPE,
SUMMARIZE_STANDARD_ERROR,
USE_MAX_DEP_VERSIONS,
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
index b123344..fe36f68 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
@@ -35,6 +35,7 @@
import androidx.build.resources.configurePublicResourcesStub
import androidx.build.studio.StudioTask
import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.addToTestZips
import androidx.build.testConfiguration.configureTestConfigGeneration
import com.android.build.api.extension.LibraryAndroidComponentsExtension
import com.android.build.gradle.AppExtension
@@ -491,18 +492,7 @@
return
}
- project.rootProject.tasks.named(ZIP_TEST_CONFIGS_WITH_APKS_TASK)
- .configure { task ->
- task as Zip
- task.from(packageTask.outputDirectory) {
- it.include("*.apk")
- it.duplicatesStrategy = DuplicatesStrategy.FAIL
- it.rename { fileName ->
- fileName.renameApkForTesting(project.path, project.hasBenchmarkPlugin())
- }
- }
- task.dependsOn(packageTask)
- }
+ addToTestZips(project, packageTask)
packageTask.doLast {
project.copy {
@@ -669,6 +659,7 @@
const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
const val REPORT_LIBRARY_METRICS_TASK = "reportLibraryMetrics"
const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
+ const val ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK = "zipConstrainedTestConfigsWithApks"
const val TASK_GROUP_API = "API"
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index 4aeaf7d..6420a50 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -16,6 +16,7 @@
package androidx.build
+import androidx.build.AndroidXPlugin.Companion.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
import androidx.build.AndroidXPlugin.Companion.ZIP_TEST_CONFIGS_WITH_APKS_TASK
import androidx.build.dependencyTracker.AffectedModuleDetector
import androidx.build.gradle.isRoot
@@ -142,16 +143,24 @@
val zipTestConfigsWithApks = project.tasks.register(
ZIP_TEST_CONFIGS_WITH_APKS_TASK, Zip::class.java
- )
- // can't chain this, or a kotlin.Unit gets added as dependency below instead of the task
- zipTestConfigsWithApks.configure {
+ ) {
it.destinationDirectory.set(project.getDistributionDirectory())
it.archiveFileName.set("androidTest.zip")
it.from(project.getTestConfigDirectory())
// We're mostly zipping a bunch of .apk files that are already compressed
it.entryCompression = ZipEntryCompression.STORED
}
+ val zipConstrainedTestConfigsWithApks = project.tasks.register(
+ ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK, Zip::class.java
+ ) {
+ it.destinationDirectory.set(project.getDistributionDirectory())
+ it.archiveFileName.set("constrainedAndroidTest.zip")
+ it.from(project.getConstrainedTestConfigDirectory())
+ // We're mostly zipping a bunch of .apk files that are already compressed
+ it.entryCompression = ZipEntryCompression.STORED
+ }
buildOnServerTask.dependsOn(zipTestConfigsWithApks)
+ buildOnServerTask.dependsOn(zipConstrainedTestConfigsWithApks)
AffectedModuleDetector.configure(gradle, this)
diff --git a/buildSrc/src/main/kotlin/androidx/build/BuildServerConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
index 7feac90..5cc80d4 100644
--- a/buildSrc/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
@@ -61,12 +61,20 @@
File(getDistributionDirectory(), "build-info")
/**
- * Directory for android test configuration files that get consumed by Tradefed in CI.
+ * Directory for android test configuration files that get consumed by Tradefed in CI. These
+ * configs cause all the tests to be run, except in cases where buildSrc changes.
*/
fun Project.getTestConfigDirectory(): File =
File(getDistributionDirectory(), "test-xml-configs")
/**
+ * Directory for android test configuration files that get consumed by Tradefed in CI. These
+ * "constrained" configs cause only small and medium tests to be run for dependent projects.
+ */
+fun Project.getConstrainedTestConfigDirectory(): File =
+ File(getDistributionDirectory(), "constrained-test-xml-configs")
+
+/**
* Directory to put release note files for generate release note tasks.
*/
fun Project.getReleaseNotesDirectory(): File =
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 54c4120..9b51d33 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -52,6 +52,7 @@
val CORE_ANIMATION = Version("1.0.0-alpha03")
val CORE_ANIMATION_TESTING = Version("1.0.0-alpha03")
val CORE_APPDIGEST = Version("1.0.0-alpha01")
+ val CORE_GOOGLE_SHORTCUTS = Version("1.0.0-alpha01")
val CORE_ROLE = Version("1.1.0-alpha02")
val CURSORADAPTER = Version("1.1.0-alpha01")
val CUSTOMVIEW = Version("1.2.0-alpha01")
@@ -142,7 +143,7 @@
val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha08")
val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha08")
val WEBKIT = Version("1.5.0-alpha01")
- val WINDOW = Version("1.0.0-alpha03")
+ val WINDOW = Version("1.0.0-alpha04")
val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
val WINDOW_SIDECAR = Version("0.1.0-alpha01")
val WORK = Version("2.6.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
index 7cf2af5a..a3aa541 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -22,7 +22,7 @@
var isBenchmark: Boolean = false
var isPostsubmit: Boolean = true
lateinit var minSdk: String
- var runFullTests: Boolean = true
+ var runAllTests: Boolean = true
val tags: MutableList<String> = mutableListOf()
lateinit var testApkName: String
lateinit var testRunner: String
@@ -32,7 +32,7 @@
fun isBenchmark(isBenchmark: Boolean) = apply { this.isBenchmark = isBenchmark }
fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
- fun runFullTests(runFullTests: Boolean) = apply { this.runFullTests = runFullTests }
+ fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
fun tag(tag: String) = apply { this.tags.add(tag) }
fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
@@ -63,7 +63,7 @@
.append(TEST_BLOCK_OPEN)
.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
.append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
- if (runFullTests) {
+ if (runAllTests) {
if (!isPostsubmit) {
sb.append(FLAKY_TEST_OPTION)
}
@@ -95,7 +95,7 @@
var isPostsubmit: Boolean = true
var isServicePrevious: Boolean = true
lateinit var minSdk: String
- var runFullTests: Boolean = true
+ var runAllTests: Boolean = true
lateinit var serviceApkName: String
lateinit var serviceApplicationId: String
var tags: MutableList<String> = mutableListOf()
@@ -112,7 +112,7 @@
this.isServicePrevious = isServicePrevious
}
fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
- fun runFullTests(runFullTests: Boolean) = apply { this.runFullTests = runFullTests }
+ fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
fun serviceApkName(serviceApkName: String) = apply { this.serviceApkName = serviceApkName }
fun serviceApplicationId(serviceApplicationId: String) =
apply { this.serviceApplicationId = serviceApplicationId }
@@ -158,7 +158,7 @@
.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
.append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
.append(mediaInstrumentationArgs())
- if (runFullTests) {
+ if (runAllTests) {
if (!isPostsubmit) {
sb.append(FLAKY_TEST_OPTION)
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
index 4bee86c..becea7a 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
@@ -96,6 +96,15 @@
@get:OutputFile
abstract val clientToTServiceToT: RegularFileProperty
+ @get:OutputFile
+ abstract val constrainedClientPreviousServiceToT: RegularFileProperty
+
+ @get:OutputFile
+ abstract val constrainedClientToTServicePrevious: RegularFileProperty
+
+ @get:OutputFile
+ abstract val constrainedClientToTServiceToT: RegularFileProperty
+
@TaskAction
fun generateAndroidTestZip() {
val clientToTApk = resolveApk(clientToTFolder, clientToTLoader)
@@ -116,6 +125,19 @@
clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
serviceToTPath.get(), clientPreviousServiceToT, true, false
)
+ // write constrained configs as well
+ writeConfigFileContent(
+ clientToTApk, serviceToTApk, clientToTPath.get(),
+ serviceToTPath.get(), constrainedClientToTServiceToT, false, false, true
+ )
+ writeConfigFileContent(
+ clientToTApk, servicePreviousApk, clientToTPath.get(),
+ servicePreviousPath.get(), constrainedClientToTServicePrevious, false, true, true
+ )
+ writeConfigFileContent(
+ clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
+ serviceToTPath.get(), constrainedClientPreviousServiceToT, true, false, true
+ )
}
private fun resolveApk(
@@ -138,7 +160,8 @@
servicePath: String,
outputFile: RegularFileProperty,
isClientPrevious: Boolean,
- isServicePrevious: Boolean
+ isServicePrevious: Boolean,
+ isConstrained: Boolean = false
) {
val configBuilder = MediaConfigBuilder()
configBuilder.clientApkName(resolveName(clientApk, clientPath))
@@ -154,15 +177,19 @@
when (affectedModuleDetectorSubset.get()) {
ProjectSubset.CHANGED_PROJECTS -> {
configBuilder.isPostsubmit(false)
- configBuilder.runFullTests(true)
+ configBuilder.runAllTests(true)
}
ProjectSubset.ALL_AFFECTED_PROJECTS -> {
configBuilder.isPostsubmit(true)
- configBuilder.runFullTests(true)
+ configBuilder.runAllTests(true)
}
ProjectSubset.DEPENDENT_PROJECTS -> {
configBuilder.isPostsubmit(false)
- configBuilder.runFullTests(false)
+ if (isConstrained) {
+ configBuilder.runAllTests(false)
+ } else {
+ configBuilder.runAllTests(false)
+ }
}
else -> {
throw IllegalStateException(
@@ -176,7 +203,7 @@
if (!resolvedOutputFile.exists()) {
if (!resolvedOutputFile.createNewFile()) {
throw RuntimeException(
- "Failed to create test configuration file: $outputFile"
+ "Failed to create test configuration file: $resolvedOutputFile"
)
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index ac32787..e4dd273 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -74,12 +74,19 @@
@get:OutputFile
abstract val outputXml: RegularFileProperty
+ @get:OutputFile
+ abstract val constrainedOutputXml: RegularFileProperty
+
@TaskAction
fun generateAndroidTestZip() {
- writeConfigFileContent()
+ writeConfigFileContent(constrainedOutputXml, true)
+ writeConfigFileContent(outputXml)
}
- private fun writeConfigFileContent() {
+ private fun writeConfigFileContent(
+ outputFile: RegularFileProperty,
+ isConstrained: Boolean = true
+ ) {
/*
Testing an Android Application project involves 2 APKS: an application to be instrumented,
and a test APK. Testing an Android Library project involves only 1 APK, since the library
@@ -93,7 +100,7 @@
// We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
val appName = appApk.elements.single().outputFile.substringAfterLast("/")
.renameApkForTesting(appProjectPath.get(), hasBenchmarkPlugin = false)
- // TODO(b/178776319)
+ // TODO(b/178776319): Clean up this hardcoded hack
if (appProjectPath.get().contains("macrobenchmark-target")) {
configBuilder.appApkName(appName.replace("debug-androidTest", "release"))
} else {
@@ -103,15 +110,20 @@
when (affectedModuleDetectorSubset.get()) {
ProjectSubset.CHANGED_PROJECTS -> {
configBuilder.isPostsubmit(false)
- configBuilder.runFullTests(true)
+ configBuilder.runAllTests(true)
}
ProjectSubset.ALL_AFFECTED_PROJECTS -> {
configBuilder.isPostsubmit(true)
- configBuilder.runFullTests(true)
+ configBuilder.runAllTests(true)
}
ProjectSubset.DEPENDENT_PROJECTS -> {
configBuilder.isPostsubmit(false)
- configBuilder.runFullTests(false)
+ // Don't ever run full tests of RV if it is dependent, since they take > 45 minutes
+ if (isConstrained || testProjectPath.get().contains("recyclerview")) {
+ configBuilder.runAllTests(false)
+ } else {
+ configBuilder.runAllTests(true)
+ }
}
else -> {
throw IllegalStateException(
@@ -145,11 +157,11 @@
.minSdk(minSdk.get().toString())
.testRunner(testRunner.get())
- val resolvedOutputFile: File = outputXml.asFile.get()
+ val resolvedOutputFile: File = outputFile.asFile.get()
if (!resolvedOutputFile.exists()) {
if (!resolvedOutputFile.createNewFile()) {
throw RuntimeException(
- "Failed to create test configuration file: $outputXml"
+ "Failed to create test configuration file: $resolvedOutputFile"
)
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 9312d82..807b3f7 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -19,18 +19,25 @@
package androidx.build.testConfiguration
import androidx.build.AndroidXPlugin
+import androidx.build.AndroidXPlugin.Companion.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
+import androidx.build.AndroidXPlugin.Companion.ZIP_TEST_CONFIGS_WITH_APKS_TASK
import androidx.build.asFilenamePrefix
import androidx.build.dependencyTracker.AffectedModuleDetector
+import androidx.build.getConstrainedTestConfigDirectory
import androidx.build.getTestConfigDirectory
import androidx.build.gradle.getByType
import androidx.build.hasAndroidTestSourceCode
import androidx.build.hasBenchmarkPlugin
+import androidx.build.renameApkForTesting
import com.android.build.api.artifact.ArtifactType
import com.android.build.api.artifact.Artifacts
import com.android.build.api.extension.AndroidComponentsExtension
import com.android.build.api.extension.ApplicationAndroidComponentsExtension
import com.android.build.gradle.TestedExtension
+import com.android.build.gradle.tasks.PackageAndroidArtifact
import org.gradle.api.Project
+import org.gradle.api.file.DuplicatesStrategy
+import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.TaskProvider
import java.io.File
@@ -61,6 +68,12 @@
"${this.path.asFilenamePrefix()}$variantName.xml"
)
)
+ task.constrainedOutputXml.fileValue(
+ File(
+ this.getConstrainedTestConfigDirectory(),
+ "${this.path.asFilenamePrefix()}$variantName.xml"
+ )
+ )
// Disable work tests on < API 18: b/178127496
if (this.path.startsWith(":work:")) {
task.minSdk.set(maxOf(18, minSdk))
@@ -85,6 +98,8 @@
}
this.rootProject.tasks.findByName(AndroidXPlugin.ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(generateTestConfigurationTask)
+ this.rootProject.tasks.findByName(AndroidXPlugin.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK)!!
+ .dependsOn(generateTestConfigurationTask)
}
/**
@@ -108,6 +123,34 @@
}
}
+/**
+ * Configures the test zip task to include the project's apk
+ */
+fun addToTestZips(project: Project, packageTask: PackageAndroidArtifact) {
+ project.rootProject.tasks.named(ZIP_TEST_CONFIGS_WITH_APKS_TASK) { task ->
+ task as Zip
+ task.from(packageTask.outputDirectory) {
+ it.include("*.apk")
+ it.duplicatesStrategy = DuplicatesStrategy.FAIL
+ it.rename { fileName ->
+ fileName.renameApkForTesting(project.path, project.hasBenchmarkPlugin())
+ }
+ }
+ task.dependsOn(packageTask)
+ }
+ project.rootProject.tasks.named(ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK) { task ->
+ task as Zip
+ task.from(packageTask.outputDirectory) {
+ it.include("*.apk")
+ it.duplicatesStrategy = DuplicatesStrategy.FAIL
+ it.rename { fileName ->
+ fileName.renameApkForTesting(project.path, project.hasBenchmarkPlugin())
+ }
+ }
+ task.dependsOn(packageTask)
+ }
+}
+
private fun getOrCreateMediaTestConfigTask(project: Project, isMedia2: Boolean):
TaskProvider<GenerateMediaTestConfigurationTask> {
val mediaPrefix = getMediaConfigTaskPrefix(isMedia2)
@@ -132,6 +175,9 @@
}
project.rootProject.tasks.findByName(AndroidXPlugin.ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(task)
+ project.rootProject.tasks.findByName(
+ AndroidXPlugin.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
+ )!!.dependsOn(task)
return task
} else {
return parentProject.tasks.withType(GenerateMediaTestConfigurationTask::class.java)
@@ -197,6 +243,24 @@
"${mediaPrefix}ClientToTServiceToT$variantName.xml"
)
)
+ it.constrainedClientPreviousServiceToT.fileValue(
+ File(
+ this.getConstrainedTestConfigDirectory(),
+ "${mediaPrefix}ClientPreviousServiceToT$variantName.xml"
+ )
+ )
+ it.constrainedClientToTServicePrevious.fileValue(
+ File(
+ this.getConstrainedTestConfigDirectory(),
+ "${mediaPrefix}ClientToTServicePrevious$variantName.xml"
+ )
+ )
+ it.constrainedClientToTServiceToT.fileValue(
+ File(
+ this.getConstrainedTestConfigDirectory(),
+ "${mediaPrefix}ClientToTServiceToT$variantName.xml"
+ )
+ )
it.minSdk.set(minSdk)
it.testRunner.set(testRunner)
AffectedModuleDetector.configureTaskGuard(it)
@@ -236,6 +300,12 @@
"${this.path.asFilenamePrefix()}$variantName.xml"
)
)
+ task.constrainedOutputXml.fileValue(
+ File(
+ this.getTestConfigDirectory(),
+ "${this.path.asFilenamePrefix()}$variantName.xml"
+ )
+ )
task.minSdk.set(minSdk)
task.hasBenchmarkPlugin.set(this.hasBenchmarkPlugin())
task.testRunner.set(testRunner)
@@ -255,6 +325,9 @@
}
this.rootProject.tasks.findByName(AndroidXPlugin.ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(configTask)
+ this.rootProject.tasks.findByName(
+ AndroidXPlugin.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
+ )!!.dependsOn(configTask)
} else if (path.endsWith("macrobenchmark-target")) {
configTask.configure { task ->
task.appFolder.set(artifacts.get(ArtifactType.APK))
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index f150ee3..d1b7f3c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -108,6 +108,7 @@
"testDebugUnitTest",
"stripArchiveForPartialDejetification",
"verifyDependencyVersions",
+ "zipConstrainedTestConfigsWithApks",
"zipTestConfigsWithApks",
"zipHtmlResultsOfTestDebugUnitTest",
"zipXmlResultsOfTestDebugUnitTest",
@@ -129,11 +130,11 @@
// Additional tasks that are expected to be temporarily out-of-date after running once
// Tasks in this set we don't even try to rerun, because they're known to be unnecessary
val DONT_TRY_RERUNNING_TASKS = setOf(
+ ":buildSrc-tests:project-subsets:test",
"listTaskOutputs",
"validateProperties",
"tasks",
"zipEcFiles",
-
// More information about the fact that these dokka tasks rerun can be found at b/167569304
"dokkaKotlinDocs",
"zipDokkaDocs",
diff --git a/busytown/androidx-studio-integration.sh b/busytown/androidx-studio-integration.sh
index c189fe7..a35614b 100755
--- a/busytown/androidx-studio-integration.sh
+++ b/busytown/androidx-studio-integration.sh
@@ -48,7 +48,7 @@
LOG_PROCESSOR="$SCRIPT_DIR/../development/build_log_processor.sh"
properties="-Pandroidx.summarizeStderr --no-daemon -Pandroidx.allWarningsAsErrors"
"$LOG_PROCESSOR" $gw $properties -p frameworks/support listTaskOutputs && \
- "$LOG_PROCESSOR" $gw $properties -p frameworks/support bOS -x lintDebug -x lint -x validateLint --stacktrace -PverifyUpToDate
+ "$LOG_PROCESSOR" $gw $properties -p frameworks/support bOS -x lintDebug -x lint -x validateLint -x verifyDependencyVersions --stacktrace -PverifyUpToDate
}
function exportTransformsDir() {
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 986efbd..c091b0e 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -7,10 +7,6 @@
# Run Gradle
impl/build.sh listTaskOutputs "$@"
-subsets="MAIN COMPOSE FLAN MEDIA WEAR"
-for subset in $subsets; do
- ANDROIDX_PROJECTS=$subset impl/build.sh tasks -Pandroidx.validateNoUnrecognizedMessages=false >/dev/null
-done
impl/build.sh buildOnServer checkExternalLicenses validateAllProperties \
--profile "$@"
diff --git a/busytown/androidx_device_tests.sh b/busytown/androidx_device_tests.sh
index 5f72d6f..a3db9d2 100755
--- a/busytown/androidx_device_tests.sh
+++ b/busytown/androidx_device_tests.sh
@@ -5,7 +5,7 @@
cd "$(dirname $0)"
-impl/build.sh zipTestConfigsWithApks \
+impl/build.sh zipTestConfigsWithApks zipConstrainedTestConfigsWithApks \
"$@"
echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
index 32674ba..51dfe49 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
@@ -26,6 +26,7 @@
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@@ -72,6 +73,7 @@
}
@Test
+ @Ignore("b/180539013: Test is currently flaky")
fun createExternalCameraGraph() {
val fakeRequestProcessor = FakeRequestProcessor()
val fakeCameraMetadata = FakeCameraMetadata()
diff --git a/car/app/app-aaos/api/current.txt b/car/app/app-aaos/api/current.txt
index 015dfc5..56a478a 100644
--- a/car/app/app-aaos/api/current.txt
+++ b/car/app/app-aaos/api/current.txt
@@ -22,13 +22,13 @@
method public void setSurfaceWrapper(androidx.car.app.aaos.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageCompat implements android.os.Parcelable {
- ctor public SurfacePackageCompat(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageCompat(android.view.SurfaceControlViewHost.SurfacePackage);
+ public final class SurfacePackageWrapper implements android.os.Parcelable {
+ ctor public SurfacePackageWrapper(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
method public int describeContents();
method public android.os.Parcelable? getSurfacePackage();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageCompat!> CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper!> CREATOR;
}
public final class SurfaceWrapper implements android.os.Parcelable {
diff --git a/car/app/app-aaos/api/public_plus_experimental_current.txt b/car/app/app-aaos/api/public_plus_experimental_current.txt
index 015dfc5..56a478a 100644
--- a/car/app/app-aaos/api/public_plus_experimental_current.txt
+++ b/car/app/app-aaos/api/public_plus_experimental_current.txt
@@ -22,13 +22,13 @@
method public void setSurfaceWrapper(androidx.car.app.aaos.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageCompat implements android.os.Parcelable {
- ctor public SurfacePackageCompat(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageCompat(android.view.SurfaceControlViewHost.SurfacePackage);
+ public final class SurfacePackageWrapper implements android.os.Parcelable {
+ ctor public SurfacePackageWrapper(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
method public int describeContents();
method public android.os.Parcelable? getSurfacePackage();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageCompat!> CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper!> CREATOR;
}
public final class SurfaceWrapper implements android.os.Parcelable {
diff --git a/car/app/app-aaos/api/restricted_current.txt b/car/app/app-aaos/api/restricted_current.txt
index 015dfc5..56a478a 100644
--- a/car/app/app-aaos/api/restricted_current.txt
+++ b/car/app/app-aaos/api/restricted_current.txt
@@ -22,13 +22,13 @@
method public void setSurfaceWrapper(androidx.car.app.aaos.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageCompat implements android.os.Parcelable {
- ctor public SurfacePackageCompat(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageCompat(android.view.SurfaceControlViewHost.SurfacePackage);
+ public final class SurfacePackageWrapper implements android.os.Parcelable {
+ ctor public SurfacePackageWrapper(androidx.car.app.aaos.renderer.surface.LegacySurfacePackage);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
method public int describeContents();
method public android.os.Parcelable? getSurfacePackage();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageCompat!> CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper!> CREATOR;
}
public final class SurfaceWrapper implements android.os.Parcelable {
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ICarAppActivity.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ICarAppActivity.aidl
index d06bdf2..86367cf 100644
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ICarAppActivity.aidl
+++ b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ICarAppActivity.aidl
@@ -16,12 +16,9 @@
package androidx.car.app.aaos.renderer;
-import androidx.car.app.aaos.renderer.IBackButtonListener;
-import androidx.car.app.aaos.renderer.IInputConnectionListener;
-import androidx.car.app.aaos.renderer.ILifecycleListener;
-import androidx.car.app.aaos.renderer.IRotaryEventListener;
+import androidx.car.app.aaos.renderer.IRendererCallback;
import androidx.car.app.aaos.renderer.surface.ISurfaceListener;
-import androidx.car.app.aaos.renderer.surface.SurfacePackageCompat;
+import androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper;
/**
* An interface to let renderer service communicate with the car activity.
@@ -30,36 +27,23 @@
*/
oneway interface ICarAppActivity {
/** Sets the surface package. */
- void setSurfacePackage(in SurfacePackageCompat surfacePackageCompat) = 1;
+ void setSurfacePackage(in SurfacePackageWrapper surfacePackageWrapper) = 1;
/** Registers the listener to get callbacks for surface events. */
void setSurfaceListener(ISurfaceListener listener) = 2;
- /** Registers the listener to get callbacks for lifecyle events. */
- void setLifecycleListener(ILifecycleListener listener) = 3;
-
- /** Registers the listener to get callbacks for back button events. */
- void setBackButtonListener(IBackButtonListener listener) = 4;
+ /** Registers the callback to get callbacks for renderer events. */
+ void registerRendererCallback(IRendererCallback callback) = 3;
/** Notifies to start the input, i.e. to show the keyboard. */
- void onStartInput() = 5;
+ void onStartInput() = 4;
/** Notifies to stop the input, i.e. to hide the keyboard. */
- void onStopInput() = 6;
-
- /**
- * Registers the listener for input connection.
- *
- * This listener can be used to establish an input connection with the host.
- */
- void setInputConnectionListener(IInputConnectionListener listener) = 7;
-
- /** Registers the listener to get rotary events. */
- void setRotaryEventListener(IRotaryEventListener listener) = 8;
+ void onStopInput() = 5;
/** Sends the Intent to be used to start a car app. */
- void startCarApp(in Intent intent) = 9;
+ void startCarApp(in Intent intent) = 6;
/** Requests the activity to finish itself. */
- void finishCarApp() = 10;
+ void finishCarApp() = 7;
}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IInputConnectionListener.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IInputConnectionListener.aidl
deleted file mode 100644
index 2fe9f33e..0000000
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IInputConnectionListener.aidl
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2021 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.app.aaos.renderer;
-
-import androidx.car.app.aaos.renderer.IProxyInputConnection;
-
-/**
- * Proxies input connection events from {@link CarAppActivity} to the host renderer.
- *
- * @hide
- */
-interface IInputConnectionListener {
-
- /**
- * Creates a proxy to a remote {@link InputConnection}.
- *
- * @params editorInfo the {@link EditorInfo} for which the input connection should be created
- *
- * @return an {@link IProxyInputConnection} through which communication to the
- * remote {@code InputConnection} should occur
- */
- IProxyInputConnection onCreateInputConnection(in EditorInfo editorInfo) = 1;
-}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ILifecycleListener.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ILifecycleListener.aidl
deleted file mode 100644
index 5c5d85fd..0000000
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/ILifecycleListener.aidl
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2021 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.app.aaos.renderer;
-
-/**
- * A lifecycle event listener interface.
- *
- * @hide
- */
-interface ILifecycleListener {
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onCreate()}.
- */
- void onCreate() = 1;
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onStart()}.
- */
- void onStart() = 2;
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onResume()}.
- */
- void onResume() = 3;
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onPause()}.
- */
- void onPause() = 4;
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onStop()}.
- */
- void onStop() = 5;
-
- /**
- * Notifies that {@link CarAppActivity} called {Activity#onDestroyed()}.
- */
- void onDestroyed() = 6;
-}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRendererCallback.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRendererCallback.aidl
new file mode 100644
index 0000000..38d6f7e
--- /dev/null
+++ b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRendererCallback.aidl
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 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.app.aaos.renderer;
+
+import androidx.car.app.aaos.renderer.IProxyInputConnection;
+
+/**
+ * Interface to events relevant to remote rendering.
+ *
+ * @hide
+ */
+interface IRendererCallback {
+ /** Notifies that the button was pressed. */
+ void onBackPressed() = 1;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onCreate()}.
+ */
+ void onCreate() = 2;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onStart()}.
+ */
+ void onStart() = 3;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onResume()}.
+ */
+ void onResume() = 4;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onPause()}.
+ */
+ void onPause() = 5;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onStop()}.
+ */
+ void onStop() = 6;
+
+ /**
+ * Notifies that {@link CarAppActivity} called {Activity#onDestroyed()}.
+ */
+ void onDestroyed() = 7;
+
+ /**
+ * Creates a proxy to a remote {@link InputConnection}.
+ *
+ * @params editorInfo the {@link EditorInfo} for which the input connection should be created
+ *
+ * @return an {@link IProxyInputConnection} through which communication to the
+ * remote {@code InputConnection} should occur
+ */
+ IProxyInputConnection onCreateInputConnection(in EditorInfo editorInfo) = 8;
+
+ /**
+ * Notifies of a rotary rotation.
+ *
+ * @param steps the number of rotation steps detected. Should be a positive number
+ * @param isClockwise true if the rotation direction is clockwise
+ */
+ void onRotate(int steps, boolean isClockwise) = 9;
+
+ /**
+ * Notifies of a nudge event.
+ *
+ * @param key the nudge key code. It can be {@code KEYCODE_DPAD_RIGHT}, {@code KEYCODE_DPAD_LEFT},
+ * {@code KEYCODE_DPAD_UP}, {@code KEYCODE_DPAD_DOWN}.
+ *
+ * @return true if handled successfully
+ */
+ boolean onNudge(int keyCode) = 10;
+
+ /** Will be called when rotary select is triggered. */
+ void onSelect() = 11;
+}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRotaryEventListener.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRotaryEventListener.aidl
deleted file mode 100644
index 755c641..0000000
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IRotaryEventListener.aidl
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2021 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.app.aaos.renderer;
-
-/**
- * Interface to receive rotary events.
- *
- * @hide
- */
-interface IRotaryEventListener {
-
- /**
- * Notifies of a rotary rotation.
- *
- * @param steps the number of rotation steps detected. Should be a positive number
- * @param isClockwise true if the rotation direction is clockwise
- */
- void onRotate(int steps, boolean isClockwise) = 1;
-
- /**
- * Notifies of a nudge event.
- *
- * @param key the nudge key code. It can be {@code KEYCODE_DPAD_RIGHT}, {@code KEYCODE_DPAD_LEFT},
- * {@code KEYCODE_DPAD_UP}, {@code KEYCODE_DPAD_DOWN}.
- *
- * @return true if handled successfully
- */
- boolean onNudge(int keyCode) = 2;
-
- /** Will be called when rotary select is triggered. */
- void onSelect() = 3;
-}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.aidl
deleted file mode 100644
index 41def00..0000000
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.aidl
+++ /dev/null
@@ -1,3 +0,0 @@
-package androidx.car.app.aaos.renderer.surface;
-
-parcelable SurfacePackageCompat;
\ No newline at end of file
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.aidl b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.aidl
new file mode 100644
index 0000000..e4de051
--- /dev/null
+++ b/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.aidl
@@ -0,0 +1,3 @@
+package androidx.car.app.aaos.renderer.surface;
+
+parcelable SurfacePackageWrapper;
\ No newline at end of file
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/ActivityLifecycleDelegate.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/ActivityLifecycleDelegate.java
index 0153bfa..a19dd46 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/ActivityLifecycleDelegate.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/ActivityLifecycleDelegate.java
@@ -26,26 +26,25 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.car.app.aaos.renderer.ILifecycleListener;
+import androidx.car.app.aaos.renderer.IRendererCallback;
import androidx.lifecycle.Lifecycle.Event;
/**
- * An activity lifecycle listener which dispatches the lifecycle events to the {@link
- * ILifecycleListener}.
+ * An activity lifecycle listener which dispatches the lifecycle events to a {@link
+ * IRendererCallback}.
*/
final class ActivityLifecycleDelegate implements ActivityLifecycleCallbacks {
public static final String TAG = "ActivityLifecycleListener";
@Nullable
- private ILifecycleListener mLifecycleListener;
+ private IRendererCallback mRendererCallback;
@NonNull
private Event mLastObservedEvent = Event.ON_ANY;
/**
- * Registers the lifecycle listener. This listener gets notified of lifecycle method
- * invocations.
+ * Registers a {@link IRendererCallback} that is notified of lifecycle method invocations.
*/
- void setLifecycleListener(@Nullable ILifecycleListener lifecycleListener) {
- mLifecycleListener = lifecycleListener;
+ void registerRendererCallback(@Nullable IRendererCallback rendererCallback) {
+ mRendererCallback = rendererCallback;
onActive();
}
@@ -97,29 +96,29 @@
private void notifyEvent(Event event) {
mLastObservedEvent = event;
- if (mLifecycleListener == null) {
+ if (mRendererCallback == null) {
return;
}
try {
switch (event) {
case ON_CREATE:
- mLifecycleListener.onCreate();
+ mRendererCallback.onCreate();
break;
case ON_START:
- mLifecycleListener.onStart();
+ mRendererCallback.onStart();
break;
case ON_RESUME:
- mLifecycleListener.onResume();
+ mRendererCallback.onResume();
break;
case ON_PAUSE:
- mLifecycleListener.onPause();
+ mRendererCallback.onPause();
break;
case ON_STOP:
- mLifecycleListener.onStop();
+ mRendererCallback.onStop();
break;
case ON_DESTROY:
- mLifecycleListener.onDestroyed();
+ mRendererCallback.onDestroyed();
break;
case ON_ANY:
break;
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/CarAppActivity.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/CarAppActivity.java
index 1d47918..76d9a38 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/CarAppActivity.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/CarAppActivity.java
@@ -18,6 +18,8 @@
import static android.content.pm.PackageManager.NameNotFoundException;
+import static androidx.car.app.aaos.LogTags.TAG_AAOS_HOST;
+
import static java.util.Objects.requireNonNull;
import android.annotation.SuppressLint;
@@ -38,15 +40,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.car.app.aaos.renderer.IBackButtonListener;
import androidx.car.app.aaos.renderer.ICarAppActivity;
-import androidx.car.app.aaos.renderer.IInputConnectionListener;
-import androidx.car.app.aaos.renderer.ILifecycleListener;
+import androidx.car.app.aaos.renderer.IRendererCallback;
import androidx.car.app.aaos.renderer.IRendererService;
-import androidx.car.app.aaos.renderer.IRotaryEventListener;
import androidx.car.app.aaos.renderer.surface.ISurfaceListener;
+import androidx.car.app.aaos.renderer.surface.OnBackPressedListener;
+import androidx.car.app.aaos.renderer.surface.RotaryEventCallback;
import androidx.car.app.aaos.renderer.surface.SurfaceHolderListener;
-import androidx.car.app.aaos.renderer.surface.SurfacePackageCompat;
+import androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper;
import androidx.car.app.aaos.renderer.surface.SurfaceWrapperProvider;
import androidx.car.app.aaos.renderer.surface.TemplateSurfaceView;
import androidx.car.app.utils.ThreadUtils;
@@ -78,7 +79,7 @@
SurfaceHolderListener mSurfaceHolderListener;
ActivityLifecycleDelegate mActivityLifecycleDelegate;
@Nullable
- IBackButtonListener mBackButtonListener;
+ OnBackPressedListener mOnBackPressedListener;
@Nullable
IRendererService mRendererService;
private int mDisplayId;
@@ -89,30 +90,82 @@
*/
private final ICarAppActivity.Stub mCarActivity = new ICarAppActivity.Stub() {
@Override
- public void setSurfacePackage(@NonNull SurfacePackageCompat surfacePackage) {
+ public void setSurfacePackage(@NonNull SurfacePackageWrapper surfacePackage) {
requireNonNull(surfacePackage);
ThreadUtils.runOnMain(() -> mSurfaceView.setSurfacePackage(surfacePackage));
}
@Override
+ public void registerRendererCallback(@NonNull IRendererCallback callback) {
+ requireNonNull(callback);
+ ThreadUtils.runOnMain(() -> {
+ mSurfaceView.registerRotaryEventCallback(new RotaryEventCallback() {
+ @Override
+ public void onRotate(int steps, boolean isClockwise) {
+ try {
+ callback.onRotate(steps, isClockwise);
+ } catch (RemoteException e) {
+ onServiceConnectionError("Failed to send rotary onRotate event to "
+ + "renderer: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean onNudge(int keyCode) {
+ try {
+ return callback.onNudge(keyCode);
+ } catch (RemoteException e) {
+ onServiceConnectionError("Failed to send rotary onNudge event to "
+ + "renderer: " + e.getMessage());
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onSelect() {
+ try {
+ callback.onSelect();
+ } catch (RemoteException e) {
+ onServiceConnectionError(
+ "Failed to send rotary onSelect event to renderer: "
+ + e.getMessage());
+ }
+ }
+ });
+
+ mSurfaceView.setOnCreateInputConnectionListener(editorInfo -> {
+ try {
+ return callback.onCreateInputConnection(editorInfo);
+ } catch (RemoteException e) {
+ onServiceConnectionError("Failed to send onCreateInputConnection event to "
+ + "renderer: " + e.getMessage());
+ }
+
+ return null;
+ });
+
+ mOnBackPressedListener = () -> {
+ try {
+ callback.onBackPressed();
+ } catch (RemoteException e) {
+ onServiceConnectionError(
+ "Failed to send onBackPressed event to renderer: "
+ + e.getMessage());
+ }
+ };
+ mSurfaceView.setOnBackPressedListener(mOnBackPressedListener);
+ mActivityLifecycleDelegate.registerRendererCallback(callback);
+ });
+ }
+
+ @Override
public void setSurfaceListener(@NonNull ISurfaceListener listener) {
requireNonNull(listener);
ThreadUtils.runOnMain(() -> mSurfaceHolderListener.setSurfaceListener(listener));
}
@Override
- public void setLifecycleListener(@NonNull ILifecycleListener listener) {
- requireNonNull(listener);
- ThreadUtils.runOnMain(() -> mActivityLifecycleDelegate.setLifecycleListener(listener));
- }
-
- @Override
- public void setBackButtonListener(@NonNull IBackButtonListener listener) {
- mBackButtonListener = requireNonNull(listener);
- mSurfaceView.setBackButtonListener(listener);
- }
-
- @Override
public void onStartInput() {
ThreadUtils.runOnMain(() -> mSurfaceView.onStartInput());
}
@@ -123,16 +176,6 @@
}
@Override
- public void setInputConnectionListener(@NonNull IInputConnectionListener listener) {
- mSurfaceView.setInputConnectionListener(requireNonNull(listener));
- }
-
- @Override
- public void setRotaryEventListener(@NonNull IRotaryEventListener listener) {
- mSurfaceView.setRotaryEventListener(requireNonNull(listener));
- }
-
- @Override
public void startCarApp(@NonNull Intent intent) {
startActivity(intent);
}
@@ -224,13 +267,8 @@
@Override
public void onBackPressed() {
- if (mBackButtonListener != null) {
- try {
- mBackButtonListener.onBackPressed();
- } catch (RemoteException e) {
- onServiceConnectionError(
- "Failed to send onBackPressed event to renderer: " + e.getMessage());
- }
+ if (mOnBackPressedListener != null) {
+ mOnBackPressedListener.onBackPressed();
}
}
@@ -266,7 +304,7 @@
activityInfo = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Unable to find component: " + getComponentName(), e);
+ Log.e(TAG_AAOS_HOST, "Unable to find component: " + getComponentName(), e);
}
if (activityInfo == null) {
@@ -275,11 +313,10 @@
String serviceName = activityInfo.metaData.getString(SERVICE_METADATA_KEY);
if (serviceName == null) {
- Log.e(LogTags.TAG_AAOS_HOST, String.format("Unable to find required metadata tag with "
- + "name %s. App manifest must include metadata tag with name %s and "
- + "the name of the car app service as the value",
- SERVICE_METADATA_KEY,
- SERVICE_METADATA_KEY));
+ Log.e(TAG_AAOS_HOST, "Unable to find required metadata tag with "
+ + "name " + SERVICE_METADATA_KEY + ". App manifest must include "
+ + "metadata tag with name " + SERVICE_METADATA_KEY + " and "
+ + "the name of the car app service as the value");
return null;
}
@@ -325,9 +362,9 @@
if (errorMessage != null) {
Log.e(TAG, errorMessage);
}
- // Remove the lifecycle listener since there is no need to communicate the state with
+ // Remove the renderer callback since there is no need to communicate the state with
// the host.
- mActivityLifecycleDelegate.setLifecycleListener(null);
+ mActivityLifecycleDelegate.registerRendererCallback(null);
finish();
}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/LogTags.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/LogTags.java
index 458c702..f1aec9d 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/LogTags.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/LogTags.java
@@ -21,13 +21,13 @@
import androidx.annotation.RestrictTo;
/**
- * Assorted tags for logging.
+ * Declares the log tags to use in the library.
*
* @hide
*/
@RestrictTo(LIBRARY)
public final class LogTags {
- public static final String TAG_AAOS_HOST = "CarApp.H";
+ public static final String TAG_AAOS_HOST = androidx.car.app.utils.LogTags.TAG + ".AAOS";
private LogTags() {
}
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnBackPressedListener.java
similarity index 70%
rename from car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl
rename to car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnBackPressedListener.java
index d48ca1b..576b2e7 100644
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnBackPressedListener.java
@@ -14,14 +14,19 @@
* limitations under the License.
*/
-package androidx.car.app.aaos.renderer;
+package androidx.car.app.aaos.renderer.surface;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.RestrictTo;
/**
* Classes that wish to listen for back button events should implement this.
*
* @hide
*/
-interface IBackButtonListener {
- /** Notifies that the button was pressed. */
- void onBackPressed() = 1;
+@RestrictTo(LIBRARY)
+public interface OnBackPressedListener {
+ /** Notifies that the button was pressed. */
+ void onBackPressed();
}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnCreateInputConnectionListener.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnCreateInputConnectionListener.java
new file mode 100644
index 0000000..e262db7
--- /dev/null
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/OnCreateInputConnectionListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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.app.aaos.renderer.surface;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.view.inputmethod.EditorInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.aaos.renderer.IProxyInputConnection;
+
+/**
+ * Proxies the {@link android.view.View#onCreateInputConnection} method invocation events from
+ * {@link TemplateSurfaceView} to the host renderer.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface OnCreateInputConnectionListener {
+ /**
+ * Creates a proxy to a remote {@link android.view.inputmethod.InputConnection}.
+ *
+ * @param editorInfo the {@link EditorInfo} for which the input connection should be created
+ * @return an {@link IProxyInputConnection} through which communication to the
+ * remote {@code InputConnection} should occur
+ */
+ @Nullable
+ IProxyInputConnection onCreateInputConnection(@NonNull EditorInfo editorInfo);
+}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RemoteProxyInputConnection.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RemoteProxyInputConnection.java
index 209a054..a99e4c1 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RemoteProxyInputConnection.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RemoteProxyInputConnection.java
@@ -16,6 +16,8 @@
package androidx.car.app.aaos.renderer.surface;
+import static androidx.car.app.aaos.LogTags.TAG_AAOS_HOST;
+
import static java.util.Objects.requireNonNull;
import android.os.Bundle;
@@ -31,7 +33,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.car.app.aaos.LogTags;
import androidx.car.app.aaos.renderer.IProxyInputConnection;
/** Proxies input connection calls to the provided {@link IProxyInputConnection}. */
@@ -50,7 +51,7 @@
try {
text = mProxyInputConnection.getTextBeforeCursor(n, flags);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
text = null;
}
return text;
@@ -63,7 +64,7 @@
try {
text = mProxyInputConnection.getTextAfterCursor(n, flags);
} catch (RemoteException e) {
- Log.w(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.w(TAG_AAOS_HOST, "Remote connection lost", e);
text = null;
}
@@ -77,7 +78,7 @@
try {
text = mProxyInputConnection.getSelectedText(flags);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
text = null;
}
@@ -90,7 +91,7 @@
try {
text = mProxyInputConnection.getCursorCapsMode(reqModes);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
text = 0;
}
@@ -105,7 +106,7 @@
try {
text = mProxyInputConnection.getExtractedText(request, flags);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
text = null;
}
@@ -118,7 +119,7 @@
try {
success = mProxyInputConnection.deleteSurroundingText(beforeLength, afterLength);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -132,7 +133,7 @@
try {
success = mProxyInputConnection.setComposingText(text, newCursorPosition);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -145,7 +146,7 @@
try {
success = mProxyInputConnection.setComposingRegion(start, end);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -158,7 +159,7 @@
try {
success = mProxyInputConnection.finishComposingText();
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -172,7 +173,7 @@
try {
success = mProxyInputConnection.commitText(text, newCursorPosition);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -186,7 +187,7 @@
try {
success = mProxyInputConnection.commitCompletion(text);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -200,7 +201,7 @@
try {
success = mProxyInputConnection.commitCorrection(correctionInfo);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -213,7 +214,7 @@
try {
success = mProxyInputConnection.setSelection(start, end);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -226,7 +227,7 @@
try {
success = mProxyInputConnection.performEditorAction(editorAction);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -239,7 +240,7 @@
try {
success = mProxyInputConnection.performContextMenuAction(id);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -252,7 +253,7 @@
try {
success = mProxyInputConnection.beginBatchEdit();
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -265,7 +266,7 @@
try {
success = mProxyInputConnection.endBatchEdit();
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -279,7 +280,7 @@
try {
success = mProxyInputConnection.sendKeyEvent(event);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -292,7 +293,7 @@
try {
success = mProxyInputConnection.clearMetaKeyStates(states);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -305,7 +306,7 @@
try {
success = mProxyInputConnection.reportFullscreenMode(enabled);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -320,7 +321,7 @@
try {
success = mProxyInputConnection.performPrivateCommand(action, data);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -333,7 +334,7 @@
try {
success = mProxyInputConnection.requestCursorUpdates(cursorUpdateMode);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
success = false;
}
@@ -345,7 +346,7 @@
try {
mProxyInputConnection.closeConnection();
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RotaryEventCallback.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RotaryEventCallback.java
new file mode 100644
index 0000000..7eea59b
--- /dev/null
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/RotaryEventCallback.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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.app.aaos.renderer.surface;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Interface to receive rotary events.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface RotaryEventCallback {
+ /**
+ * Notifies of a rotary rotation.
+ *
+ * @param steps the number of rotation steps detected. Should be a positive number
+ * @param isClockwise true if the rotation direction is clockwise
+ */
+ void onRotate(int steps, boolean isClockwise);
+
+ /**
+ * Notifies of a nudge event.
+ *
+ * @param keyCode the nudge key code. It can be {@code KEYCODE_DPAD_RIGHT}, {@code
+ * KEYCODE_DPAD_LEFT}, {@code KEYCODE_DPAD_UP}, {@code KEYCODE_DPAD_DOWN}.
+ *
+ * @return true if handled successfully
+ */
+ boolean onNudge(int keyCode);
+
+ /** Will be called when rotary select is triggered. */
+ void onSelect();
+}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfaceHolderListener.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfaceHolderListener.java
index 67cb801..6da4f0e 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfaceHolderListener.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfaceHolderListener.java
@@ -17,6 +17,7 @@
package androidx.car.app.aaos.renderer.surface;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.aaos.LogTags.TAG_AAOS_HOST;
import static java.util.Objects.requireNonNull;
@@ -27,7 +28,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.car.app.aaos.LogTags;
/**
* A listener of {@link SurfaceHolder}.
@@ -83,7 +83,7 @@
mSurfaceListener.onSurfaceAvailable(mSurfaceWrapperProvider.createSurfaceWrapper());
}
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
}
@@ -94,7 +94,7 @@
mSurfaceListener.onSurfaceChanged(mSurfaceWrapperProvider.createSurfaceWrapper());
}
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
}
}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.java
similarity index 73%
rename from car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.java
rename to car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.java
index 25b21d4..210b789 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageCompat.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/SurfacePackageWrapper.java
@@ -35,7 +35,7 @@
*/
//TODO(179714355): Investigate using Bundleable instead of Parcelable
@SuppressLint({"BanParcelableUsage"})
-public final class SurfacePackageCompat implements Parcelable {
+public final class SurfacePackageWrapper implements Parcelable {
enum SurfacePackageType {
LEGACY,
SURFACE_CONTROL;
@@ -45,31 +45,31 @@
private final Parcelable mSurfacePackage;
/**
- * Creates a {@link SurfacePackageCompat} that stores a
+ * Creates a {@link SurfacePackageWrapper} that stores a
* {@link android.view.SurfaceControlViewHost.SurfacePackage}.
*
* @param legacySurfacePackage the {@link android.view.SurfaceControlViewHost.SurfacePackage}
* to be stored in this wrapper.
*/
- public SurfacePackageCompat(@NonNull LegacySurfacePackage legacySurfacePackage) {
+ public SurfacePackageWrapper(@NonNull LegacySurfacePackage legacySurfacePackage) {
mSurfacePackage = legacySurfacePackage;
}
/**
- * Creates a {@link SurfacePackageCompat} that stores a {@link LegacySurfacePackage}.
+ * Creates a {@link SurfacePackageWrapper} that stores a {@link LegacySurfacePackage}.
*
* @param surfacePackage the {@link LegacySurfacePackage} to be stored in this wrapper.
*/
@RequiresApi(Build.VERSION_CODES.R)
- public SurfacePackageCompat(@NonNull SurfacePackage surfacePackage) {
+ public SurfacePackageWrapper(@NonNull SurfacePackage surfacePackage) {
mSurfacePackage = surfacePackage;
}
- SurfacePackageCompat(Parcel parcel) {
+ SurfacePackageWrapper(Parcel parcel) {
SurfacePackageType type = SurfacePackageType.values()[parcel.readInt()];
if (type == SurfacePackageType.LEGACY) {
mSurfacePackage = parcel.readParcelable(LegacySurfacePackage.class.getClassLoader());
- } else if (type == SurfacePackageCompat.SurfacePackageType.SURFACE_CONTROL
+ } else if (type == SurfacePackageWrapper.SurfacePackageType.SURFACE_CONTROL
&& VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mSurfacePackage = parcel.readParcelable(SurfacePackage.class.getClassLoader());
} else {
@@ -79,10 +79,10 @@
@Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
- SurfacePackageCompat.SurfacePackageType type =
+ SurfacePackageWrapper.SurfacePackageType type =
mSurfacePackage instanceof LegacySurfacePackage
- ? SurfacePackageCompat.SurfacePackageType.LEGACY
- : SurfacePackageCompat.SurfacePackageType.SURFACE_CONTROL;
+ ? SurfacePackageWrapper.SurfacePackageType.LEGACY
+ : SurfacePackageWrapper.SurfacePackageType.SURFACE_CONTROL;
parcel.writeInt(type.ordinal());
parcel.writeParcelable(mSurfacePackage, flags);
}
@@ -98,18 +98,18 @@
}
@NonNull
- public static final Creator<SurfacePackageCompat> CREATOR =
- new Creator<SurfacePackageCompat>() {
+ public static final Creator<SurfacePackageWrapper> CREATOR =
+ new Creator<SurfacePackageWrapper>() {
@NonNull
@Override
- public SurfacePackageCompat createFromParcel(@NonNull Parcel parcel) {
- return new SurfacePackageCompat(parcel);
+ public SurfacePackageWrapper createFromParcel(@NonNull Parcel parcel) {
+ return new SurfacePackageWrapper(parcel);
}
@NonNull
@Override
- public SurfacePackageCompat[] newArray(int size) {
- return new SurfacePackageCompat[size];
+ public SurfacePackageWrapper[] newArray(int size) {
+ return new SurfacePackageWrapper[size];
}
};
}
diff --git a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/TemplateSurfaceView.java b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/TemplateSurfaceView.java
index 95d4028..847f181 100644
--- a/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/TemplateSurfaceView.java
+++ b/car/app/app-aaos/src/main/java/androidx/car/app/aaos/renderer/surface/TemplateSurfaceView.java
@@ -24,6 +24,9 @@
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.aaos.LogTags.TAG_AAOS_HOST;
+
import static java.util.Objects.requireNonNull;
import android.annotation.SuppressLint;
@@ -55,11 +58,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
-import androidx.car.app.aaos.LogTags;
-import androidx.car.app.aaos.renderer.IBackButtonListener;
-import androidx.car.app.aaos.renderer.IInputConnectionListener;
import androidx.car.app.aaos.renderer.IProxyInputConnection;
-import androidx.car.app.aaos.renderer.IRotaryEventListener;
/**
* A surface view suitable for template rendering.
@@ -68,7 +67,7 @@
*
* @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(LIBRARY)
public final class TemplateSurfaceView extends SurfaceView {
private static final boolean SUPPORTS_SURFACE_CONTROL =
VERSION.SDK_INT >= Build.VERSION_CODES.R;
@@ -88,11 +87,11 @@
+ ".DIRECT_MANIPULATION";
@Nullable
- private IInputConnectionListener mInputConnectionListener;
+ private RotaryEventCallback mRotaryEventCallback;
@Nullable
- private IRotaryEventListener mRotaryEventListener;
+ private OnCreateInputConnectionListener mOnCreateInputConnectionListener;
@Nullable
- private IBackButtonListener mBackButtonListener;
+ private OnBackPressedListener mOnBackPressedListener;
ISurfaceControl mSurfaceControl;
private boolean mIsInInputMode;
@@ -110,7 +109,7 @@
mSurfaceControl.onWindowFocusChanged(hasFocus(), isInTouchMode);
}
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
}
};
@@ -119,18 +118,27 @@
super(context, attrs, 0);
}
- public void setInputConnectionListener(
- @Nullable IInputConnectionListener inputConnectionListener) {
- mInputConnectionListener = inputConnectionListener;
+ /**
+ * Registers a {@link RotaryEventCallback} that is notified of rotary events.
+ */
+ public void registerRotaryEventCallback(@Nullable RotaryEventCallback callback) {
+ mRotaryEventCallback = callback;
}
- public void setRotaryEventListener(
- @Nullable IRotaryEventListener rotaryEventListener) {
- mRotaryEventListener = rotaryEventListener;
+ /**
+ * Registers a {@link OnCreateInputConnectionListener} that is notified of invocations on
+ * {@link #onCreateInputConnection(EditorInfo)}.
+ */
+ public void setOnCreateInputConnectionListener(
+ @Nullable OnCreateInputConnectionListener listener) {
+ mOnCreateInputConnectionListener = listener;
}
- public void setBackButtonListener(@Nullable IBackButtonListener backButtonListener) {
- mBackButtonListener = backButtonListener;
+ /**
+ * Registers a {@link OnBackPressedListener} that is notified of back button presses.
+ */
+ public void setOnBackPressedListener(@Nullable OnBackPressedListener listener) {
+ mOnBackPressedListener = listener;
}
/**
@@ -165,7 +173,7 @@
mSurfaceControl.onWindowFocusChanged(gainFocus, isInTouchMode());
}
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
enableDirectManipulationMode(this, gainFocus);
}
@@ -175,18 +183,18 @@
public InputConnection onCreateInputConnection(@NonNull EditorInfo editorInfo) {
requireNonNull(editorInfo);
- if (!mIsInInputMode || mInputConnectionListener == null) {
+ if (!mIsInInputMode || mOnCreateInputConnectionListener == null) {
return null;
}
try {
IProxyInputConnection proxyInputConnection =
- mInputConnectionListener.onCreateInputConnection(editorInfo);
+ mOnCreateInputConnectionListener.onCreateInputConnection(editorInfo);
// Clear the input and return null if inputConnectionListener is null or there is no
// open input connection on the host.
if (proxyInputConnection == null) {
- Log.e(LogTags.TAG_AAOS_HOST,
+ Log.e(TAG_AAOS_HOST,
"InputConnectionListener has not been received yet. Canceling the input");
onStopInput();
return null;
@@ -195,7 +203,7 @@
return new RemoteProxyInputConnection(proxyInputConnection);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
return null;
@@ -282,16 +290,16 @@
/**
* Updates the surface package. The surface package can be either a
* {@link android.view.SurfaceControlViewHost.SurfacePackage} or a
- * {@link LegacySurfacePackage} wrapped in a {@link SurfacePackageCompat}.
+ * {@link LegacySurfacePackage} wrapped in a {@link SurfacePackageWrapper}.
*/
- public void setSurfacePackage(@NonNull SurfacePackageCompat surfacePackageCompat) {
- Parcelable surfacePackage = surfacePackageCompat.getSurfacePackage();
+ public void setSurfacePackage(@NonNull SurfacePackageWrapper surfacePackageWrapper) {
+ Parcelable surfacePackage = surfacePackageWrapper.getSurfacePackage();
if (SUPPORTS_SURFACE_CONTROL && surfacePackage instanceof SurfacePackage) {
Api30Impl.setSurfacePackage(this, (SurfacePackage) surfacePackage);
} else if (surfacePackage instanceof LegacySurfacePackage) {
setLegacySurfacePackage((LegacySurfacePackage) surfacePackage);
} else {
- Log.e(LogTags.TAG_AAOS_HOST, "Unrecognized surface package");
+ Log.e(TAG_AAOS_HOST, "Unrecognized surface package");
}
}
@@ -307,7 +315,7 @@
try {
surfaceControl.setSurfaceWrapper(surfaceWrapper);
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
return;
}
mSurfaceControl = surfaceControl;
@@ -338,7 +346,7 @@
return true;
}
} catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ Log.e(TAG_AAOS_HOST, "Remote connection lost", e);
}
return false;
@@ -346,17 +354,13 @@
/** Passes the generic motion events to the host. */
boolean handleGenericMotionEvent(@NonNull MotionEvent event) {
- try {
- if (requireNonNull(event).getActionMasked() == MotionEvent.ACTION_SCROLL) {
- int steps = (int) event.getAxisValue(MotionEvent.AXIS_SCROLL);
- boolean isClockwise = steps > 0;
- if (mRotaryEventListener != null) {
- mRotaryEventListener.onRotate(steps, isClockwise);
- }
- return true;
+ if (requireNonNull(event).getActionMasked() == MotionEvent.ACTION_SCROLL) {
+ int steps = (int) event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ boolean isClockwise = steps > 0;
+ if (mRotaryEventCallback != null) {
+ mRotaryEventCallback.onRotate(steps, isClockwise);
}
- } catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ return true;
}
return false;
}
@@ -367,39 +371,35 @@
return false;
}
- try {
- switch (event.getKeyCode()) {
- case KEYCODE_BACK:
- if (mBackButtonListener != null) {
- mBackButtonListener.onBackPressed();
- return true;
+ switch (event.getKeyCode()) {
+ case KEYCODE_BACK:
+ if (mOnBackPressedListener != null) {
+ mOnBackPressedListener.onBackPressed();
+ return true;
+ }
+ break;
+ case KEYCODE_DPAD_CENTER:
+ if (mRotaryEventCallback != null) {
+ mRotaryEventCallback.onSelect();
+ return true;
+ }
+ break;
+ case KEYCODE_DPAD_RIGHT:
+ case KEYCODE_DPAD_LEFT:
+ case KEYCODE_DPAD_UP:
+ case KEYCODE_DPAD_DOWN:
+ if (mRotaryEventCallback != null) {
+ boolean success = mRotaryEventCallback.onNudge(event.getKeyCode());
+ if (!success) {
+ // Quit direct manipulation mode if the nudge event cannot be handled.
+ enableDirectManipulationMode(this, false);
+ return false;
}
- break;
- case KEYCODE_DPAD_CENTER:
- if (mRotaryEventListener != null) {
- mRotaryEventListener.onSelect();
- return true;
- }
- break;
- case KEYCODE_DPAD_RIGHT:
- case KEYCODE_DPAD_LEFT:
- case KEYCODE_DPAD_UP:
- case KEYCODE_DPAD_DOWN:
- if (mRotaryEventListener != null) {
- boolean success = mRotaryEventListener.onNudge(event.getKeyCode());
- if (!success) {
- // Quit direct manipulation mode if the nudge event cannot be handled.
- enableDirectManipulationMode(this, false);
- return false;
- }
- return true;
- }
- break;
- default:
- return false;
- }
- } catch (RemoteException e) {
- Log.e(LogTags.TAG_AAOS_HOST, "Remote connection lost", e);
+ return true;
+ }
+ break;
+ default:
+ return false;
}
return false;
diff --git a/car/app/app-aaos/src/test/java/androidx/car/app/aaos/CarAppActivityTest.java b/car/app/app-aaos/src/test/java/androidx/car/app/aaos/CarAppActivityTest.java
index 77e8f78..1adb428 100644
--- a/car/app/app-aaos/src/test/java/androidx/car/app/aaos/CarAppActivityTest.java
+++ b/car/app/app-aaos/src/test/java/androidx/car/app/aaos/CarAppActivityTest.java
@@ -54,16 +54,13 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
-import androidx.car.app.aaos.renderer.IBackButtonListener;
import androidx.car.app.aaos.renderer.ICarAppActivity;
-import androidx.car.app.aaos.renderer.IInputConnectionListener;
-import androidx.car.app.aaos.renderer.ILifecycleListener;
import androidx.car.app.aaos.renderer.IProxyInputConnection;
+import androidx.car.app.aaos.renderer.IRendererCallback;
import androidx.car.app.aaos.renderer.IRendererService;
-import androidx.car.app.aaos.renderer.IRotaryEventListener;
import androidx.car.app.aaos.renderer.surface.LegacySurfacePackage;
import androidx.car.app.aaos.renderer.surface.SurfaceControlCallback;
-import androidx.car.app.aaos.renderer.surface.SurfacePackageCompat;
+import androidx.car.app.aaos.renderer.surface.SurfacePackageWrapper;
import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
@@ -164,22 +161,22 @@
try (ActivityScenario<CarAppActivity> scenario = ActivityScenario.launch(
CarAppActivity.class)) {
scenario.onActivity(activity -> {
- ILifecycleListener listener = mock(ILifecycleListener.class);
+ IRendererCallback callback = mock(IRendererCallback.class);
try {
- mRenderServiceDelegate.getCarAppActivity().setLifecycleListener(listener);
- // Last observed event is reported as soon as listener is set.
- verify(listener, times(1)).onResume();
- // Verify lifecycle events are reported to registered listener.
+ mRenderServiceDelegate.getCarAppActivity().registerRendererCallback(callback);
+ // Last observed event is reported as soon as callback is set.
+ verify(callback, times(1)).onResume();
+ // Verify lifecycle events are reported to registered callback.
scenario.moveToState(Lifecycle.State.STARTED);
- verify(listener, times(1)).onPause();
+ verify(callback, times(1)).onPause();
scenario.moveToState(Lifecycle.State.RESUMED);
- verify(listener, times(2)).onResume();
+ verify(callback, times(2)).onResume();
scenario.moveToState(Lifecycle.State.CREATED);
- verify(listener, times(1)).onStop();
+ verify(callback, times(1)).onStop();
scenario.moveToState(Lifecycle.State.CREATED);
- verify(listener, times(1)).onStop();
+ verify(callback, times(1)).onStop();
scenario.moveToState(Lifecycle.State.DESTROYED);
- verify(listener, times(1)).onDestroyed();
+ verify(callback, times(1)).onDestroyed();
} catch (RemoteException e) {
fail(Log.getStackTraceString(e));
}
@@ -194,14 +191,15 @@
CarAppActivity.class)) {
scenario.onActivity(activity -> {
try {
- ILifecycleListener listener = mock(ILifecycleListener.class);
- mRenderServiceDelegate.getCarAppActivity().setLifecycleListener(listener);
- // Last observed event is reported as soon as listener is set.
- verify(listener, times(1)).onResume();
+ IRendererCallback callback = mock(IRendererCallback.class);
+ mRenderServiceDelegate.getCarAppActivity().registerRendererCallback(callback);
+ // Last observed event is reported as soon as callback is set.
+ verify(callback, times(1)).onResume();
- // Add a test-specific lifecycle listener to activity.
- ActivityLifecycleCallbacks callback = mock(ActivityLifecycleCallbacks.class);
- activity.registerActivityLifecycleCallbacks(callback);
+ // Add a test-specific lifecycle callback to activity.
+ ActivityLifecycleCallbacks activityCallback = mock(
+ ActivityLifecycleCallbacks.class);
+ activity.registerActivityLifecycleCallbacks(activityCallback);
// Report service connection error.
activity.onServiceConnectionError("fake error");
@@ -210,8 +208,8 @@
// After service connection error has been reported, test that lifecycle
// events are no longer reported to host lifecycle listener.
scenario.moveToState(Lifecycle.State.STARTED);
- verify(callback, times(1)).onActivityPaused(activity);
- verify(listener, times(0)).onPause();
+ verify(activityCallback, times(1)).onActivityPaused(activity);
+ verify(callback, times(0)).onPause();
} catch (RemoteException e) {
fail(Log.getStackTraceString(e));
}
@@ -220,16 +218,16 @@
}
@Test
- public void testBackButtonListener() {
+ public void testOnBackPressed() {
setupCarAppActivityForTesting();
try (ActivityScenario<CarAppActivity> scenario = ActivityScenario.launch(
CarAppActivity.class)) {
scenario.onActivity(activity -> {
try {
- IBackButtonListener listener = mock(IBackButtonListener.class);
- mRenderServiceDelegate.getCarAppActivity().setBackButtonListener(listener);
+ IRendererCallback callback = mock(IRendererCallback.class);
+ mRenderServiceDelegate.getCarAppActivity().registerRendererCallback(callback);
activity.onBackPressed();
- verify(listener, times(1)).onBackPressed();
+ verify(callback, times(1)).onBackPressed();
} catch (RemoteException e) {
fail(Log.getStackTraceString(e));
}
@@ -272,19 +270,17 @@
scenario.onActivity(activity -> {
try {
SurfaceControlCallback callback = mock(SurfaceControlCallback.class);
- IBackButtonListener backButtonListener = mock(IBackButtonListener.class);
- IRotaryEventListener rotaryEventListener = mock(IRotaryEventListener.class);
+ IRendererCallback rendererCallback = mock(IRendererCallback.class);
- SurfacePackageCompat wrapper =
- new SurfacePackageCompat(new LegacySurfacePackage(callback));
+ SurfacePackageWrapper wrapper =
+ new SurfacePackageWrapper(new LegacySurfacePackage(callback));
ICarAppActivity carAppActivity = mRenderServiceDelegate.getCarAppActivity();
carAppActivity.setSurfacePackage(wrapper);
- carAppActivity.setBackButtonListener(backButtonListener);
- carAppActivity.setRotaryEventListener(rotaryEventListener);
+ carAppActivity.registerRendererCallback(rendererCallback);
// Verify back events on surfaceView are sent to host.
activity.mSurfaceView.dispatchKeyEvent(new KeyEvent(ACTION_UP, KEYCODE_BACK));
- verify(backButtonListener, times(1)).onBackPressed();
+ verify(rendererCallback, times(1)).onBackPressed();
// Verify focus request sent to host.
activity.mSurfaceView.requestFocus();
@@ -295,13 +291,13 @@
// Verify rotary events on surfaceView are sent to host.
activity.mSurfaceView.dispatchKeyEvent(
new KeyEvent(ACTION_UP, KEYCODE_DPAD_RIGHT));
- verify(rotaryEventListener, times(1)).onNudge(KEYCODE_DPAD_RIGHT);
+ verify(rendererCallback, times(1)).onNudge(KEYCODE_DPAD_RIGHT);
activity.mSurfaceView.dispatchKeyEvent(
new KeyEvent(ACTION_UP, KEYCODE_DPAD_DOWN));
- verify(rotaryEventListener, times(1)).onNudge(KEYCODE_DPAD_DOWN);
+ verify(rendererCallback, times(1)).onNudge(KEYCODE_DPAD_DOWN);
activity.mSurfaceView.dispatchKeyEvent(
new KeyEvent(ACTION_UP, KEYCODE_DPAD_CENTER));
- verify(rotaryEventListener, times(1)).onSelect();
+ verify(rendererCallback, times(1)).onSelect();
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis();
@@ -323,7 +319,7 @@
event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_SCROLL, x,
y, metaState);
activity.mSurfaceView.dispatchGenericMotionEvent(event);
- verify(rotaryEventListener, times(1)).onRotate(anyInt(), eq(false));
+ verify(rendererCallback, times(1)).onRotate(anyInt(), eq(false));
} catch (RemoteException e) {
fail(Log.getStackTraceString(e));
}
@@ -339,12 +335,12 @@
scenario.onActivity(activity -> {
try {
EditorInfo editorInfo = new EditorInfo();
- IInputConnectionListener listener = mock(IInputConnectionListener.class);
+ IRendererCallback callback = mock(IRendererCallback.class);
IProxyInputConnection inputConnection = mock(IProxyInputConnection.class);
- when(listener.onCreateInputConnection(any())).thenReturn(inputConnection);
+ when(callback.onCreateInputConnection(any())).thenReturn(inputConnection);
when(inputConnection.getEditorInfo()).thenReturn(editorInfo);
- mRenderServiceDelegate.getCarAppActivity().setInputConnectionListener(listener);
+ mRenderServiceDelegate.getCarAppActivity().registerRendererCallback(callback);
// Create input connection without first calling ICarAppActivity#startInput().
InputConnection remoteProxyInputConnection =
activity.mSurfaceView.onCreateInputConnection(editorInfo);
@@ -365,12 +361,12 @@
scenario.onActivity(activity -> {
try {
EditorInfo editorInfo = new EditorInfo();
- IInputConnectionListener listener = mock(IInputConnectionListener.class);
+ IRendererCallback callback = mock(IRendererCallback.class);
IProxyInputConnection inputConnection = mock(IProxyInputConnection.class);
- when(listener.onCreateInputConnection(any())).thenReturn(inputConnection);
+ when(callback.onCreateInputConnection(any())).thenReturn(inputConnection);
when(inputConnection.getEditorInfo()).thenReturn(editorInfo);
- mRenderServiceDelegate.getCarAppActivity().setInputConnectionListener(listener);
+ mRenderServiceDelegate.getCarAppActivity().registerRendererCallback(callback);
mRenderServiceDelegate.getCarAppActivity().onStartInput();
InputConnection remoteProxyInputConnection =
activity.mSurfaceView.onCreateInputConnection(editorInfo);
diff --git a/car/app/app/src/main/java/androidx/car/app/AppInfo.java b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
index 86a9a92..185f585 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
@@ -16,7 +16,7 @@
package androidx.car.app;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import static java.util.Objects.requireNonNull;
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
index 70ce7e9..c3cad0d 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
@@ -16,7 +16,7 @@
package androidx.car.app;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import android.content.Context;
import android.content.pm.PackageInfo;
@@ -26,7 +26,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
-import androidx.car.app.utils.CommonUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -129,8 +128,7 @@
}
}
} catch (NameNotFoundException e) {
- Log.e(CommonUtils.TAG,
- "Package name not found on the system: " + context.getPackageName(), e);
+ Log.e(TAG, "Package name not found on the system: " + context.getPackageName(), e);
}
throw new SecurityException(
"The car app does not have a required permission: " + permission);
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index cc77a18..64cf178 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -20,7 +20,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import static java.util.Objects.requireNonNull;
@@ -157,9 +157,7 @@
* @param name The name of the car service requested. This should be one of
* {@link #APP_SERVICE},
* {@link #NAVIGATION_SERVICE} or {@link #SCREEN_SERVICE}
- *
* @return The car service instance
- *
* @throws IllegalArgumentException if {@code name} does not refer to a valid car service
* @throws NullPointerException if {@code name} is {@code null}
*/
@@ -187,7 +185,6 @@
* ScreenManager}.
*
* @param serviceClass the class of the requested service
- *
* @throws IllegalArgumentException if {@code serviceClass} is not the class of a supported car
* service
* @throws NullPointerException if {@code serviceClass} is {@code null}
@@ -201,13 +198,10 @@
* Gets the name of the car service that is represented by the specified class.
*
* @param serviceClass the class of the requested service
- *
* @return the car service name to use with {@link #getCarService(String)}
- *
* @throws IllegalArgumentException if {@code serviceClass} is not the class of a supported car
* service
* @throws NullPointerException if {@code serviceClass} is {@code null}
- *
* @see #getCarService
*/
@NonNull
@@ -252,7 +246,6 @@
* </dl>
*
* @param intent the {@link Intent} to send to the target application
- *
* @throws SecurityException if the app attempts to start a different app explicitly or
* does not have permissions for the requested action
* @throws InvalidParameterException if {@code intent} does not meet the criteria defined
@@ -280,7 +273,6 @@
* @param appIntent the {@link Intent} to use for starting the car app. See {@link
* #startCarApp(Intent)} for the documentation on valid
* {@link Intent}s
- *
* @throws InvalidParameterException if {@code notificationIntent} is not an {@link Intent}
* received from a broadcast, due to an action taken by the
* user in the car
@@ -475,7 +467,6 @@
* @return a value between {@link AppInfo#getMinCarAppApiLevel()} and
* {@link AppInfo#getLatestCarAppApiLevel()}. In case of incompatibility, the host will
* disconnect from the service before completing the handshake
- *
* @throws IllegalStateException if invoked before the connection handshake with the host has
* been completed (for example, before
* {@link Session#onCreateScreen(Intent)})
diff --git a/car/app/app/src/main/java/androidx/car/app/CarToast.java b/car/app/app/src/main/java/androidx/car/app/CarToast.java
index 422cc33..7db14a4 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarToast.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarToast.java
@@ -77,7 +77,6 @@
* text will be set to empty
* @param duration how long to display the message. Either {@link #LENGTH_SHORT} or {@link
* #LENGTH_LONG}
- *
* @throws NullPointerException if {@code carContext} is {@code null}
*/
@NonNull
@@ -95,7 +94,6 @@
* @param text the text to show
* @param duration how long to display the message. Either {@link #LENGTH_SHORT} or {@link
* #LENGTH_LONG}
- *
* @throws NullPointerException if either the {@code carContext} or the {@code text} are {@code
* null}
*/
diff --git a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
index 5aee696..ff206e3 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
@@ -55,7 +55,6 @@
* @param hostType the service to dispatch to
* @param call the request to dispatch
* @param callName the name of the call for logging purposes
- *
* @throws SecurityException if the host has thrown it
* @throws HostException if the host throws any exception other than
* {@link SecurityException}
diff --git a/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java b/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
index d7b4eb8f..41422f3 100644
--- a/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
+++ b/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
@@ -31,7 +31,6 @@
*
* @param result the result provided by the {@link Screen} that was pushed using {@link
* ScreenManager#pushForResult} or {@code null} if no result was set
- *
* @see Screen#setResult
*/
void onScreenResult(@Nullable Object result);
diff --git a/car/app/app/src/main/java/androidx/car/app/Screen.java b/car/app/app/src/main/java/androidx/car/app/Screen.java
index 254cddf..2df5d5e 100644
--- a/car/app/app/src/main/java/androidx/car/app/Screen.java
+++ b/car/app/app/src/main/java/androidx/car/app/Screen.java
@@ -17,7 +17,7 @@
package androidx.car.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import static java.util.Objects.requireNonNull;
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index d017ad5..8dc452d 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -17,7 +17,7 @@
package androidx.car.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import static androidx.car.app.utils.ThreadUtils.checkMainThread;
import static java.util.Objects.requireNonNull;
@@ -91,7 +91,6 @@
* @param onScreenResultListener the listener that will be executed with the result pushed by
* the {@code screen} through {@link Screen#setResult}. This
* callback will be executed on the main thread
- *
* @throws NullPointerException if either the {@code screen} or the {@code
* onScreenResultCallback} are {@code null}
* @throws IllegalStateException if the current thread is not the main thread
@@ -126,7 +125,6 @@
*
* @throws NullPointerException if {@code marker} is {@code null}
* @throws IllegalStateException if the current thread is not the main thread
- *
* @see Screen#setMarker
*/
public void popTo(@NonNull String marker) {
diff --git a/car/app/app/src/main/java/androidx/car/app/Session.java b/car/app/app/src/main/java/androidx/car/app/Session.java
index da71ee3..abf1512 100644
--- a/car/app/app/src/main/java/androidx/car/app/Session.java
+++ b/car/app/app/src/main/java/androidx/car/app/Session.java
@@ -64,7 +64,6 @@
* @param intent the intent that was used to start this app. If the app was started with a
* call to {@link CarContext#startCarApp}, this intent will be equal to the
* intent passed to that method
- *
* @see CarContext#startCarApp
*/
public void onNewIntent(@NonNull Intent intent) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
index 7e7150d..d749c07 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Action.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -286,7 +286,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
index 211c872..fb81efe 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
@@ -123,10 +123,8 @@
* @param alignment the alignment of the {@link CarIcon} relative to the text. This should be
* one of {@link #ALIGN_BASELINE}, {@link #ALIGN_BOTTOM} or
* {@link #ALIGN_CENTER}
- *
* @throws NullPointerException if {@code icon} is {@code null}
* @throws IllegalArgumentException if {@code alignment} is not a valid value
- *
* @see #ALIGN_BASELINE
* @see #ALIGN_BOTTOM
* @see #ALIGN_CENTER
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarText.java b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
index 135123b..f30e582 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/CarText.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
@@ -63,7 +63,6 @@
* <p>The {@link CarText#toString} method can be used to get a string representation of the string,
* whereas the {@link CarText#toCharSequence()} method returns the reconstructed
* {@link CharSequence}, with the non{@link CarSpan} spans removed.
- *
*/
public final class CarText {
@Keep
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Distance.java b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
index 7787185..416873fe 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Distance.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
@@ -117,7 +117,6 @@
* @param displayUnit the unit of distance to use when displaying the value in {@code
* displayUnit}. This should be one of the {@code UNIT_*} static
* constants defined in this class. See {@link #getDisplayUnit()}
- *
* @throws IllegalArgumentException if {@code displayDistance} is negative
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index cebf436..640fd1c 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -270,7 +270,6 @@
* Sets an image to show in the grid item with the default size {@link #IMAGE_TYPE_LARGE}.
*
* @throws NullPointerException if {@code image} is {@code null}
- *
* @see #setImage(CarIcon, int)
*/
@NonNull
@@ -297,7 +296,6 @@
*
* @param image the {@link CarIcon} to display
* @param imageType one of {@link #IMAGE_TYPE_ICON} or {@link #IMAGE_TYPE_LARGE}
- *
* @throws NullPointerException if {@code image} is {@code null}
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
index 5ea1d970..e344f9b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
@@ -260,7 +260,6 @@
* radio button group, while others may highlight the selected item's background.
*
* @throws NullPointerException if {@code onSelectedListener} is {@code null}
- *
* @see #setSelectedIndex(int)
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
index fb5150b..ff13005 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -196,7 +196,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -338,7 +337,6 @@
* Returns a {@link Builder} instance.
*
* @param message the text message to display in the template
- *
* @throws NullPointerException if the {@code message} is {@code null}
*/
public Builder(@NonNull CharSequence message) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
index 8a9e32e..7fb8b85 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
@@ -160,7 +160,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Place.java b/car/app/app/src/main/java/androidx/car/app/model/Place.java
index 2442464..8706e46 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Place.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Place.java
@@ -101,7 +101,6 @@
* Returns a builder instance for a {@link CarLocation}.
*
* @param location the geographical location associated with the place
- *
* @throws NullPointerException if {@code location} is {@code null}
*/
public Builder(@NonNull CarLocation location) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
index ea9299d..5eb6f9e 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
@@ -263,7 +263,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
index 9497000..b7ca8fa 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
@@ -196,7 +196,6 @@
*
* @param icon the {@link CarIcon} to display inside the marker
* @param iconType one of {@link #TYPE_ICON} or {@link #TYPE_IMAGE}
- *
* @throws NullPointerException if the {@code icon} is {@code null}
*/
@NonNull
@@ -219,9 +218,7 @@
* @param label the text to display inside of the marker. The string must have a maximum
* size of 3 characters. Set to {@code null} to let the host choose a
* labelling scheme (for example, using a sequence of numbers)
- *
* @throws NullPointerException if the {@code label} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 6b4e055..47aae4e 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -371,7 +371,6 @@
* </pre>
*
* @throws NullPointerException if {@code text} is {@code null}
- *
* @see ForegroundCarColorSpan
*/
@NonNull
@@ -384,7 +383,6 @@
* Sets an image to show in the row with the default size {@link #IMAGE_TYPE_SMALL}.
*
* @throws NullPointerException if {@code image} is {@code null}
- *
* @see #setImage(CarIcon, int)
*/
@NonNull
@@ -412,7 +410,6 @@
* @param image the {@link CarIcon} to display or {@code null} to not display one
* @param imageType one of {@link #IMAGE_TYPE_ICON}, {@link #IMAGE_TYPE_SMALL} or {@link
* #IMAGE_TYPE_LARGE}
- *
* @throws NullPointerException if {@code image} is {@code null}
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
index c5c7624..763335c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -94,7 +94,6 @@
* displays the associated icon may be shown.
*
* @param trip destination, steps, and trip estimates to be sent to the host
- *
* @throws HostException if the call is invoked by an app that is not declared as
* a navigation app in the manifest
* @throws IllegalStateException if the call occurs when navigation is not started (see
@@ -134,7 +133,6 @@
* {@link #setNavigationManagerCallback(Executor, NavigationManagerCallback)}.
*
* @param callback the {@link NavigationManagerCallback} to use
- *
* @throws IllegalStateException if the current thread is not the main thread
*/
@SuppressLint("ExecutorRegistration")
@@ -150,7 +148,6 @@
*
* @param executor the executor which will be used for invoking the callback
* @param callback the {@link NavigationManagerCallback} to use
- *
* @throws IllegalStateException if the current thread is not the main thread
*/
@MainThread
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
index eda9390..4683cb0 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
@@ -130,7 +130,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code name} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -145,7 +144,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code address} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -182,7 +180,6 @@
* <p>At least one of the name or the address must be set and not empty.
*
* @throws IllegalStateException if both the name and the address are {@code null} or empty.
- *
* @see #setName(CharSequence)
* @see #setAddress(CharSequence)
*/
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
index f822633..f4a06f2 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
@@ -587,7 +587,6 @@
* units will not display any information in that case.
*
* @param type one of the {@code TYPE_*} static constants defined in this class
- *
* @throws IllegalArgumentException if {@code type} is not a valid maneuver type
*/
public Builder(@Type int type) {
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
index 3cb430b..186bb9d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
@@ -126,7 +126,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code message} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -143,7 +142,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code text} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
index 4948cca..8c8833d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -197,7 +197,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is null
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
index d5f4bb3..66a0da6 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -224,7 +224,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code title} is null
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
index df4af78..3c52e43 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -185,7 +185,6 @@
* unsupported characters will not be displayed properly.
*
* @throws NullPointerException if {@code cue} is {@code null}
- *
* @see Builder#setCue(CharSequence)
*/
public Builder(@NonNull CharSequence cue) {
@@ -284,7 +283,6 @@
* that work with different car screen pixel densities.
*
* @throws NullPointerException if {@code cue} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -304,7 +302,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code destinations} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
index 024ad74..608feac 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -171,7 +171,6 @@
* arriving at the destination
* @param arrivalTimeAtDestination The arrival time with the time zone information
* provided for the destination
- *
* @throws NullPointerException if {@code remainingDistance} or
* {@code arrivalTimeAtDestination} are {@code null}
*/
@@ -189,7 +188,6 @@
* arriving at the destination
* @param arrivalTimeAtDestination The arrival time with the time zone information
* provided for the destination
- *
* @throws NullPointerException if {@code remainingDistance} or
* {@code arrivalTimeAtDestination} are {@code null}
*/
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
index 18b45a6..1e2ed08 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
@@ -234,7 +234,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code currentRoad} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
index dcb933a..02fb8fc 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
@@ -399,7 +399,6 @@
* <p>Spans are not supported in the input string.
*
* @throws NullPointerException if {@code contentTitle} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -418,9 +417,7 @@
*
* @param contentText override for the notification's content text. If set to an empty
* string, it will be treated as if there is no context text
- *
* @throws NullPointerException if {@code contentText} is {@code null}
- *
* @see CarText for details on text handling and span support.
*/
@NonNull
@@ -475,7 +472,6 @@
* {@link NotificationCompat.Builder#setContentIntent(PendingIntent)} for the car screen.
*
* @param contentIntent override for the notification's content intent.
- *
* @throws NullPointerException if {@code contentIntent} is {@code null}
*/
@NonNull
@@ -495,7 +491,6 @@
* {@link NotificationCompat.Builder#setDeleteIntent(PendingIntent)} for the car screen.
*
* @param deleteIntent override for the notification's delete intent
- *
* @throws NullPointerException if {@code deleteIntent} is {@code null}
*/
@NonNull
@@ -529,7 +524,6 @@
* navigation notifications in the rail widget, this intent will be sent
* when the user taps on the action icon in the rail
* widget
- *
* @throws NullPointerException if {@code title} or {@code intent} are {@code null}
*/
@SuppressWarnings("deprecation")
diff --git a/car/app/app/src/main/java/androidx/car/app/serialization/BundlerException.java b/car/app/app/src/main/java/androidx/car/app/serialization/BundlerException.java
index cae4434..42691f5 100644
--- a/car/app/app/src/main/java/androidx/car/app/serialization/BundlerException.java
+++ b/car/app/app/src/main/java/androidx/car/app/serialization/BundlerException.java
@@ -26,6 +26,7 @@
public BundlerException(@Nullable String msg, @NonNull Throwable e) {
super(msg, e);
}
+
public BundlerException(@Nullable String msg) {
super(msg);
}
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/CommonUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
similarity index 84%
rename from car/app/app/src/main/java/androidx/car/app/utils/CommonUtils.java
rename to car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
index 3ddd55e..1ffe2b0 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/CommonUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
@@ -21,18 +21,18 @@
import androidx.annotation.RestrictTo;
/**
- * Assorted common utilities.
+ * Declares the log tags to use in the library.
*
* @hide
*/
@RestrictTo(LIBRARY)
-public final class CommonUtils {
+public final class LogTags {
/** Tag to use for logging in the library. */
public static final String TAG = "CarApp";
/** Tag to use for host validation */
- public static final String TAG_HOST_VALIDATION = "CarApp.Val";
+ public static final String TAG_HOST_VALIDATION = TAG + ".Val";
- private CommonUtils() {
+ private LogTags() {
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index 61b4585..6c2c042 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -17,7 +17,7 @@
package androidx.car.app.utils;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-import static androidx.car.app.utils.CommonUtils.TAG;
+import static androidx.car.app.utils.LogTags.TAG;
import android.annotation.SuppressLint;
import android.graphics.Rect;
diff --git a/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java b/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
index cd11784..346da8a 100644
--- a/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
+++ b/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
@@ -16,7 +16,7 @@
package androidx.car.app.validation;
-import static androidx.car.app.utils.CommonUtils.TAG_HOST_VALIDATION;
+import static androidx.car.app.utils.LogTags.TAG_HOST_VALIDATION;
import static java.util.Objects.requireNonNull;
@@ -375,7 +375,6 @@
* package-name formatting.
*
* @param allowListedHostsRes string-array resource identifier
- *
* @throws IllegalArgumentException if the provided resource doesn't exist or if the entries
* in the given resource are not formatted as expected
*/
diff --git a/compose/animation/animation-core/api/1.0.0-beta02.txt b/compose/animation/animation-core/api/1.0.0-beta02.txt
index cc3acca..2016497 100644
--- a/compose/animation/animation-core/api/1.0.0-beta02.txt
+++ b/compose/animation/animation-core/api/1.0.0-beta02.txt
@@ -603,6 +603,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index cc3acca..2016497 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -603,6 +603,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/api/public_plus_experimental_1.0.0-beta02.txt b/compose/animation/animation-core/api/public_plus_experimental_1.0.0-beta02.txt
index cc3acca..2016497 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_1.0.0-beta02.txt
@@ -603,6 +603,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index cc3acca..2016497 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -603,6 +603,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/api/restricted_1.0.0-beta02.txt b/compose/animation/animation-core/api/restricted_1.0.0-beta02.txt
index a4a2771..7df77c6 100644
--- a/compose/animation/animation-core/api/restricted_1.0.0-beta02.txt
+++ b/compose/animation/animation-core/api/restricted_1.0.0-beta02.txt
@@ -606,6 +606,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index a4a2771..7df77c6 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -606,6 +606,9 @@
public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public long getDurationNanos(V initialValue, V targetValue, V initialVelocity);
+ method public V getValueFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
+ method public V getVelocityFromNanos(long playTimeNanos, V initialValue, V targetValue, V initialVelocity);
method public boolean isInfinite();
property public boolean isInfinite;
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
index 6f69027..5f51eda 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
@@ -170,7 +170,7 @@
if (!isRunning) {
val clampedValue = clampToBounds(value)
if (clampedValue != value) {
- this.internalState.value = value
+ this.internalState.value = clampedValue
}
}
}
@@ -318,7 +318,7 @@
private fun clampToBounds(value: T): T {
if (
lowerBoundVector == negativeInfinityBounds &&
- upperBoundVector == negativeInfinityBounds
+ upperBoundVector == positiveInfinityBounds
) {
// Expect this to be the most common use case
return value
@@ -351,7 +351,11 @@
/**
* Sets the current value to the target value, without any animation. This will also cancel any
* on-going animation with a [CancellationException]. This function will return *after*
- * canceling any on-going animation and updating the [value] to the provided [targetValue].
+ * canceling any on-going animation and updating the [Animatable.value] and
+ * [Animatable.targetValue] to the provided [targetValue].
+ *
+ * __Note__: If the [lowerBound] or [upperBound] is specified, the provided [targetValue]
+ * will be clamped to the bounds to ensure [Animatable.value] is always within bounds.
*
* See [animateTo] and [animateDecay] for more details about animation being canceled.
*
@@ -364,8 +368,9 @@
suspend fun snapTo(targetValue: T) {
mutatorMutex.mutate {
endAnimation()
- internalState.value = targetValue
- this.targetValue = targetValue
+ val clampedValue = clampToBounds(targetValue)
+ internalState.value = clampedValue
+ this.targetValue = clampedValue
}
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index f9d455c..0912403 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -360,9 +360,68 @@
class VectorizedInfiniteRepeatableSpec<V : AnimationVector>(
private val animation: VectorizedDurationBasedAnimationSpec<V>,
private val repeatMode: RepeatMode = RepeatMode.Restart
-) : VectorizedAnimationSpec<V> by
- VectorizedRepeatableSpec<V>(InfiniteIterations, animation, repeatMode) {
+) : VectorizedAnimationSpec<V> {
override val isInfinite: Boolean get() = true
+
+ /**
+ * Single iteration duration
+ */
+ internal val durationNanos: Long =
+ (animation.delayMillis + animation.durationMillis) * MillisToNanos
+
+ private fun repetitionPlayTimeNanos(playTimeNanos: Long): Long {
+ val repeatsCount = playTimeNanos / durationNanos
+ if (repeatMode == RepeatMode.Restart || repeatsCount % 2 == 0L) {
+ return playTimeNanos - repeatsCount * durationNanos
+ } else {
+ return (repeatsCount + 1) * durationNanos - playTimeNanos
+ }
+ }
+
+ private fun repetitionStartVelocity(
+ playTimeNanos: Long,
+ start: V,
+ startVelocity: V,
+ end: V
+ ): V = if (playTimeNanos > durationNanos) {
+ // Start velocity of the 2nd and subsequent iteration will be the velocity at the end
+ // of the first iteration, instead of the initial velocity.
+ getVelocityFromNanos(durationNanos, start, startVelocity, end)
+ } else {
+ startVelocity
+ }
+
+ override fun getValueFromNanos(
+ playTimeNanos: Long,
+ initialValue: V,
+ targetValue: V,
+ initialVelocity: V
+ ): V {
+ return animation.getValueFromNanos(
+ repetitionPlayTimeNanos(playTimeNanos),
+ initialValue,
+ targetValue,
+ repetitionStartVelocity(playTimeNanos, initialValue, initialVelocity, targetValue)
+ )
+ }
+
+ override fun getVelocityFromNanos(
+ playTimeNanos: Long,
+ initialValue: V,
+ targetValue: V,
+ initialVelocity: V
+ ): V {
+ return animation.getVelocityFromNanos(
+ repetitionPlayTimeNanos(playTimeNanos),
+ initialValue,
+ targetValue,
+ repetitionStartVelocity(playTimeNanos, initialValue, initialVelocity, targetValue)
+ )
+ }
+
+ @Suppress("MethodNameUnits")
+ override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long =
+ Long.MAX_VALUE
}
/**
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
index e97bd85..78a236a 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
@@ -264,4 +264,34 @@
}
}
}
+
+ @Test
+ fun testUpdateBounds() {
+ val animatable = Animatable(5f)
+ // Update bounds when *not* running
+ animatable.updateBounds(0f, 4f)
+ assertEquals(4f, animatable.value)
+ runBlocking {
+ val clock = SuspendAnimationTest.TestFrameClock()
+ // Put two frames in clock
+ clock.frame(0L)
+ clock.frame(200 * 1_000_000L)
+
+ withContext(clock) {
+ animatable.animateTo(4f, tween(100)) {
+ if (animatable.upperBound == 4f) {
+ // Update bounds while running
+ animatable.updateBounds(-4f, 0f)
+ }
+ }
+ }
+ }
+ assertEquals(0f, animatable.value)
+
+ // Snap to value out of bounds
+ runBlocking {
+ animatable.snapTo(animatable.lowerBound!! - 100f)
+ }
+ assertEquals(animatable.lowerBound!!, animatable.value)
+ }
}
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
index 30ccaf0..2761e91 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
@@ -18,6 +18,7 @@
import com.google.common.truth.Truth.assertThat
import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -25,8 +26,6 @@
@RunWith(JUnit4::class)
class RepeatableAnimationTest {
- private val Animation = TweenSpec<AnimationVector1D>(durationMillis = Duration)
-
private val DelayedAnimation = VectorizedTweenSpec<AnimationVector1D>(
delayMillis = DelayDuration,
durationMillis = Duration
@@ -63,13 +62,13 @@
animation = DelayedAnimation
)
- val duration = repeat.getDurationMillis(
+ val duration = repeat.getDurationNanos(
AnimationVector1D(0f),
AnimationVector1D(0f),
AnimationVector1D(0f)
)
- assertEquals((DelayDuration + Duration) * iters.toLong(), duration)
+ assertEquals((DelayDuration + Duration) * iters * MillisToNanos, duration)
}
@Test
@@ -106,20 +105,71 @@
@Test
fun testInfiniteRepeat() {
- val repeat = infiniteRepeatable(
+ val repeatShortAnimation = infiniteRepeatable(
animation = TweenSpec<Float>(
durationMillis = 100, easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
)
+ val extraLongDurationNanos = 1000000000
+ val repeatLongAnimation = infiniteRepeatable(
+ animation = TweenSpec<Float>(
+ durationMillis = extraLongDurationNanos, easing = LinearEasing
+ ),
+ repeatMode = RepeatMode.Restart
+ )
+ val vectorizedInfiniteRepeatingShort = repeatShortAnimation.vectorize(Float.VectorConverter)
+ val vectorizedInfiniteRepeatingLong = repeatLongAnimation.vectorize(Float.VectorConverter)
+
assertEquals(
- Int.MAX_VALUE.toLong() * 100,
- repeat.vectorize(Float.VectorConverter).getDurationMillis(
- AnimationVector(0f),
- AnimationVector(100f),
- AnimationVector(0f)
- )
+ Long.MAX_VALUE,
+ vectorizedInfiniteRepeatingShort
+ .getDurationNanos(
+ AnimationVector(0f),
+ AnimationVector(100f),
+ AnimationVector(0f)
+ )
+ )
+
+ assertEquals(
+ Long.MAX_VALUE,
+ vectorizedInfiniteRepeatingLong
+ .getDurationNanos(
+ AnimationVector(0f),
+ AnimationVector(100f),
+ AnimationVector(0f)
+ )
+ )
+
+ val repeatShort = TargetBasedAnimation(
+ repeatShortAnimation,
+ Float.VectorConverter,
+ 0f,
+ 100f
+ )
+ val repeatLong = TargetBasedAnimation(
+ repeatLongAnimation,
+ Float.VectorConverter,
+ 0f,
+ extraLongDurationNanos.toFloat()
+ )
+
+ assertEquals(repeatShort.durationNanos, Long.MAX_VALUE)
+ assertEquals(repeatLong.durationNanos, Long.MAX_VALUE)
+ assertFalse(repeatShort.isFinishedFromNanos(100000000000000000L))
+ assertFalse(repeatShort.isFinishedFromNanos(100000000000000000L))
+
+ // Also check on repeating value. Repeat mode: reverse
+ assertEquals(31f, repeatShort.getValueFromNanos(31 * MillisToNanos))
+ assertEquals(67f, repeatShort.getValueFromNanos(133 * MillisToNanos))
+
+ // Also check on repeating value. Repeat mode: restart
+ assertEquals(31f, repeatLong.getValueFromNanos(31 * MillisToNanos), 0.1f)
+ assertEquals(
+ 31f,
+ repeatLong.getValueFromNanos((extraLongDurationNanos + 31) * MillisToNanos),
+ 0.1f
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt
index 1778378..e17f108 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt
@@ -23,14 +23,34 @@
internal class TextLayoutResultProxy(val value: TextLayoutResult) {
// TextLayoutResult methods
- fun getOffsetForPosition(position: Offset): Int {
- val shiftedOffset = shiftedOffset(position)
- return value.getOffsetForPosition(shiftedOffset)
+ /**
+ * Translates the position of the touch on the screen to the position in text. Because touch
+ * is relative to the decoration box, we need to translate it to the inner text field's
+ * coordinates first before calculating position of the symbol in text.
+ *
+ * @param position original position of the gesture relative to the decoration box
+ * @param coerceInVisibleBounds if true and original [position] is outside visible bounds
+ * of the inner text field, the [position] will be shifted to the closest edge of the inner
+ * text field's visible bounds. This is useful when you have a decoration box
+ * bigger than the inner text field, so when user touches to the decoration box area, the cursor
+ * goes to the beginning or the end of the visible inner text field; otherwise if we put the
+ * cursor under the touch in the invisible part of the inner text field, it would scroll to
+ * make the cursor visible. This behavior is not needed, and therefore
+ * [coerceInVisibleBounds] should be set to false, when the user drags outside visible bounds
+ * to make a selection.
+ */
+ fun getOffsetForPosition(position: Offset, coerceInVisibleBounds: Boolean = true): Int {
+ val relativePosition = position
+ .let { if (coerceInVisibleBounds) it.coercedInVisibleBoundsOfInputText() else it }
+ .relativeToInputText()
+ return value.getOffsetForPosition(relativePosition)
}
fun getLineForVerticalPosition(vertical: Float): Int {
- val shiftedVertical = shiftedOffset(Offset(0f, vertical)).y
- return value.getLineForVerticalPosition(shiftedVertical)
+ val relativeVertical = Offset(0f, vertical)
+ .coercedInVisibleBoundsOfInputText()
+ .relativeToInputText().y
+ return value.getLineForVerticalPosition(relativeVertical)
}
fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
@@ -40,10 +60,10 @@
* in the view. Returns false when the position is in the empty space of left/right of text.
*/
fun isPositionOnText(offset: Offset): Boolean {
- val shiftedOffset = shiftedOffset(offset)
- val line = value.getLineForVerticalPosition(shiftedOffset.y)
- return shiftedOffset.x >= value.getLineLeft(line) &&
- shiftedOffset.x <= value.getLineRight(line)
+ val relativeOffset = offset.coercedInVisibleBoundsOfInputText().relativeToInputText()
+ val line = value.getLineForVerticalPosition(relativeOffset.y)
+ return relativeOffset.x >= value.getLineLeft(line) &&
+ relativeOffset.x <= value.getLineRight(line)
}
// Shift offset
@@ -54,19 +74,39 @@
var innerTextFieldCoordinates: LayoutCoordinates? = null
var decorationBoxCoordinates: LayoutCoordinates? = null
- private fun shiftedOffset(offset: Offset): Offset {
- // If offset is outside visible bounds of the inner text field, use visible bounds edges
- val visibleInnerTextFieldRect = innerTextFieldCoordinates?.let { inner ->
- decorationBoxCoordinates?.localBoundingBoxOf(inner)
- } ?: Rect.Zero
- val coercedOffset = offset.coerceIn(visibleInnerTextFieldRect)
-
+ /**
+ * Translates the click happened on the decoration box to the position in the inner text
+ * field coordinates. This relative position is then used to determine symbol position in
+ * text using TextLayoutResult object.
+ */
+ private fun Offset.relativeToInputText(): Offset {
// Translates touch to the inner text field coordinates
return innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
decorationBoxCoordinates?.let { decorationBoxCoordinates ->
- innerTextFieldCoordinates.localPositionOf(decorationBoxCoordinates, coercedOffset)
+ if (innerTextFieldCoordinates.isAttached && decorationBoxCoordinates.isAttached) {
+ innerTextFieldCoordinates.localPositionOf(decorationBoxCoordinates, this)
+ } else {
+ this
+ }
}
- } ?: coercedOffset
+ } ?: this
+ }
+
+ /**
+ * If click on the decoration box happens outside visible inner text field, coerce the click
+ * position to the visible edges of the inner text field.
+ */
+ private fun Offset.coercedInVisibleBoundsOfInputText(): Offset {
+ // If offset is outside visible bounds of the inner text field, use visible bounds edges
+ val visibleInnerTextFieldRect =
+ innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
+ if (innerTextFieldCoordinates.isAttached) {
+ decorationBoxCoordinates?.localBoundingBoxOf(innerTextFieldCoordinates)
+ } else {
+ Rect.Zero
+ }
+ } ?: Rect.Zero
+ return this.coerceIn(visibleInnerTextFieldRect)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 7b824e1..e8c775c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -114,6 +114,15 @@
private var dragBeginPosition = Offset.Zero
/**
+ * The beginning offset of the drag gesture translated into position in text. Every time a
+ * new drag gesture starts, it wil be recalculated.
+ * Unlike [dragBeginPosition] that is relative to the decoration box,
+ * [dragBeginOffsetInText] represents index in text. Essentially, it is equal to
+ * `layoutResult.getOffsetForPosition(dragBeginPosition)`.
+ */
+ private var dragBeginOffsetInText: Int? = null
+
+ /**
* The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
* it will be zeroed out.
*/
@@ -166,6 +175,7 @@
isStartHandle = false,
wordBasedSelection = true
)
+ dragBeginOffsetInText = offset
}
dragBeginPosition = pxPosition
dragTotalDistance = Offset.Zero
@@ -177,9 +187,13 @@
dragTotalDistance += dragDistance
state?.layoutResult?.let { layoutResult ->
- val startOffset = layoutResult.getOffsetForPosition(dragBeginPosition)
+ val startOffset = dragBeginOffsetInText ?: layoutResult.getOffsetForPosition(
+ position = dragBeginPosition,
+ coerceInVisibleBounds = false
+ )
val endOffset = layoutResult.getOffsetForPosition(
- dragBeginPosition + dragTotalDistance
+ position = dragBeginPosition + dragTotalDistance,
+ coerceInVisibleBounds = false
)
updateSelection(
value = value,
@@ -197,6 +211,7 @@
super.onStop(velocity)
state?.showFloatingToolbar = true
if (textToolbar?.status == TextToolbarStatus.Hidden) showSelectionToolbar()
+ dragBeginOffsetInText = null
}
}
@@ -212,6 +227,7 @@
offsetMapping,
onValueChange
)
+ dragBeginOffsetInText = layoutResult.getOffsetForPosition(downPosition)
}
dragBeginPosition = downPosition
@@ -224,9 +240,12 @@
dragTotalDistance += dragDistance
state?.layoutResult?.let { layoutResult ->
- val startOffset = layoutResult.getOffsetForPosition(dragBeginPosition)
- val endOffset =
- layoutResult.getOffsetForPosition(dragBeginPosition + dragTotalDistance)
+ val startOffset = dragBeginOffsetInText ?: layoutResult
+ .getOffsetForPosition(dragBeginPosition, false)
+ val endOffset = layoutResult.getOffsetForPosition(
+ position = dragBeginPosition + dragTotalDistance,
+ coerceInVisibleBounds = false
+ )
updateSelection(
value = value,
transformedStartOffset = startOffset,
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index 029cfd7..4d87a55 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -74,7 +74,7 @@
private val dragTextRange = TextRange("Hello".length + 1, text.length)
private val layoutResult: TextLayoutResult = mock()
private val layoutResultProxy: TextLayoutResultProxy = mock()
- private val manager = TextFieldSelectionManager()
+ private lateinit var manager: TextFieldSelectionManager
private val clipboardManager = mock<ClipboardManager>()
private val textToolbar = mock<TextToolbar>()
@@ -84,6 +84,7 @@
@OptIn(InternalFoundationTextApi::class)
@Before
fun setup() {
+ manager = TextFieldSelectionManager()
manager.offsetMapping = offsetMapping
manager.onValueChange = lambda
manager.value = value
@@ -116,10 +117,15 @@
whenever(layoutResult.getBoundingBox(any())).thenReturn(Rect.Zero)
// left or right handle drag
whenever(layoutResult.getOffsetForPosition(dragBeginPosition)).thenReturn(beginOffset)
- whenever(layoutResult.getOffsetForPosition(dragDistance)).thenReturn(dragOffset)
+ whenever(layoutResult.getOffsetForPosition(dragBeginPosition + dragDistance))
+ .thenReturn(dragOffset)
// touch drag
- whenever(layoutResultProxy.getOffsetForPosition(dragBeginPosition)).thenReturn(beginOffset)
- whenever(layoutResultProxy.getOffsetForPosition(dragDistance)).thenReturn(dragOffset)
+ whenever(
+ layoutResultProxy.getOffsetForPosition(dragBeginPosition, false)
+ ).thenReturn(beginOffset)
+ whenever(
+ layoutResultProxy.getOffsetForPosition(dragBeginPosition + dragDistance, false)
+ ).thenReturn(dragOffset)
whenever(layoutResultProxy.value).thenReturn(layoutResult)
diff --git a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
index c49f0e8..bcf4af3 100644
--- a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
+++ b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
@@ -42,7 +42,6 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -95,7 +94,6 @@
}
@Test
- @Ignore("b/179339732")
fun navigateThroughAllDemos_2() {
navigateThroughAllDemos(SplitDemoCategories[1])
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
index 6667f7e..8a7be37 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
@@ -246,6 +246,38 @@
}
@Test
+ fun slider_tap_rangeChange() {
+ val state = mutableStateOf(0f)
+ val rangeEnd = mutableStateOf(0.25f)
+
+ rule.setMaterialContent {
+ Slider(
+ modifier = Modifier.testTag(tag),
+ value = state.value,
+ onValueChange = { state.value = it },
+ valueRange = 0f..rangeEnd.value
+ )
+ }
+ // change to 1 since [calculateFraction] coerces between 0..1
+ rule.runOnUiThread {
+ rangeEnd.value = 1f
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performGesture {
+ down(Offset(centerX + 50, centerY))
+ up()
+ expected = calculateFraction(left, right, centerX + 50)
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(abs(state.value - expected)).isLessThan(0.001f)
+ }
+ }
+
+ @Test
fun slider_drag_rtl() {
val state = mutableStateOf(0f)
var slop = 0f
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index 5c1cfa9..9920b05 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -66,6 +66,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
@@ -152,7 +153,7 @@
}
val press = if (enabled) {
- Modifier.pointerInput(Unit) {
+ Modifier.pointerInput(position, interactionSource, maxPx, isRtl) {
detectTapGestures(
onPress = { pos ->
position.snapTo(if (isRtl) maxPx - pos.x else pos.x)
@@ -161,12 +162,16 @@
launch {
interactionSource.emit(interaction)
}
- }
- val success = tryAwaitRelease()
- if (success) gestureEndAction(0f)
- coroutineScope {
- launch {
- interactionSource.emit(PressInteraction.Release(interaction))
+ try {
+ val success = tryAwaitRelease()
+ if (success) gestureEndAction(0f)
+ launch {
+ interactionSource.emit(PressInteraction.Release(interaction))
+ }
+ } catch (c: CancellationException) {
+ launch {
+ interactionSource.emit(PressInteraction.Cancel(interaction))
+ }
}
}
}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ModelViewTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ModelViewTests.kt
index f019e79..6095c44 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ModelViewTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ModelViewTests.kt
@@ -24,12 +24,12 @@
import androidx.compose.runtime.mock.validate
import kotlin.test.Test
-const val PRESIDENT_NAME_1 = "George Washington"
-const val PRESIDENT_AGE_1 = 57
-const val PRESIDENT_NAME_16 = "Abraham Lincoln"
-const val PRESIDENT_AGE_16 = 52
+internal const val PRESIDENT_NAME_1 = "George Washington"
+internal const val PRESIDENT_AGE_1 = 57
+internal const val PRESIDENT_NAME_16 = "Abraham Lincoln"
+internal const val PRESIDENT_AGE_16 = 52
-class Person(name: String, age: Int) {
+internal class Person(name: String, age: Int) {
var name by mutableStateOf(name)
var age by mutableStateOf(age)
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
index 01bdda7..e82524a 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.inspection.compose
+import android.content.res.Resources
import android.view.View
+import androidx.compose.ui.inspection.framework.ancestors
+import androidx.compose.ui.inspection.framework.isRoot
import androidx.compose.ui.inspection.inspector.LayoutInspectorTree
import androidx.compose.ui.inspection.proto.StringTable
import androidx.compose.ui.inspection.proto.toComposableNodes
@@ -35,6 +38,31 @@
}
/**
+ * Returns true if the view is contained in a layout generated by the platform / system.
+ *
+ * <ul>
+ * <li>DecorView, ViewStub, AndroidComposeView, View will have a null layout
+ * <li>Layouts from the "android" namespace are from the platform
+ * <li>AppCompat will typically use an "abc_" prefix for their layout names
+ * </ul>
+ */
+private fun View.isSystemView(): Boolean {
+ return try {
+ val layoutId = sourceLayoutResId
+ if (layoutId == 0) {
+ // Programmatically added Views are treated as system views:
+ return true
+ }
+ val namespace = resources.getResourcePackageName(layoutId)
+ val name = resources.getResourceEntryName(layoutId)
+
+ namespace == "android" || name.startsWith("abc_")
+ } catch (ignored: Resources.NotFoundException) {
+ false
+ }
+}
+
+/**
* The `AndroidComposeView` class inside the compose library is internal, so we make our own fake
* class there that wraps a normal [View], verifies it's the expected type, and exposes compose
* related data that we care about.
@@ -42,7 +70,7 @@
* As this class extracts information about the view it's targeting, it must be instantiated on the
* UI thread.
*/
-class AndroidComposeViewWrapper(private val composeView: View, skipSystemComposables: Boolean) {
+class AndroidComposeViewWrapper(composeView: View, skipSystemComposables: Boolean) {
companion object {
fun tryCreateFor(view: View, skipSystemComposables: Boolean): AndroidComposeViewWrapper? {
return if (view.isAndroidComposeView()) {
@@ -58,6 +86,11 @@
check(composeView.isAndroidComposeView())
}
+ private val viewParent =
+ if (!skipSystemComposables) composeView
+ else composeView.ancestors().first { !it.isSystemView() || it.isRoot() }
+
+ // TODO: Reuse LayoutInspectorTree to avoid redoing the constant searching in ParameterFactory
val inspectorNodes = LayoutInspectorTree().apply {
this.hideSystemNodes = skipSystemComposables
}.convert(composeView)
@@ -66,7 +99,7 @@
ThreadUtils.assertOnMainThread()
return ComposableRoot.newBuilder().apply {
- viewId = composeView.uniqueDrawingId
+ viewId = viewParent.uniqueDrawingId
addAllNodes(inspectorNodes.toComposableNodes(stringTable))
}.build()
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/framework/ViewExtensions.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/framework/ViewExtensions.kt
index 84ddac5..da400e4 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/framework/ViewExtensions.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/framework/ViewExtensions.kt
@@ -25,6 +25,12 @@
return (0 until childCount).map { i -> getChildAt(i) }
}
+fun View.ancestors(): Sequence<View> =
+ generateSequence(this) { it.parent as? View }
+
+fun View.isRoot(): Boolean =
+ parent as? View == null
+
/**
* Return a list of this view and all its children in depth-first order
*/
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
index bc405cb..1df07e6 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MultipleActivitiesClickTest.kt
@@ -27,7 +27,6 @@
import androidx.compose.ui.test.click
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performGesture
-import androidx.test.filters.FlakyTest
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import com.google.common.truth.Truth.assertThat
@@ -40,9 +39,8 @@
@get:Rule
val rule = createAndroidComposeRule<Activity1>()
- @Ignore("b/178044284")
@Test
- @FlakyTest(bugId = 178003554)
+ @Ignore("b/155774664")
fun test() {
lateinit var activity1: Activity1
rule.activityRule.scenario.onActivity { activity1 = it }
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/PopupDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/PopupDemo.kt
index 28d1e5e..6e7516c 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/PopupDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/PopupDemo.kt
@@ -140,7 +140,6 @@
@Composable
private fun ColumnScope.PopupElevation() {
- var isFocusable by remember { mutableStateOf(false) }
var shape by remember { mutableStateOf(RectangleShape) }
var background by remember { mutableStateOf(Color.Transparent) }
var contentSize by remember { mutableStateOf(100.dp) }
@@ -152,7 +151,6 @@
Box(Modifier.size(110.dp).background(background)) {
Popup(
alignment = Alignment.Center,
- properties = PopupProperties(focusable = true),
onDismissRequest = { dismissCounter++ }
) {
Card(
@@ -166,7 +164,7 @@
}
Spacer(Modifier.requiredHeight(20.dp))
- Text("Dismiss clicked: $dismissCounter (focusable: $isFocusable)")
+ Text("Dismiss clicked: $dismissCounter")
Spacer(Modifier.requiredHeight(20.dp))
Row {
Button(onClick = { elevation -= 1.dp }) {
@@ -182,9 +180,6 @@
Text("Toggle shape")
}
Spacer(Modifier.requiredHeight(10.dp))
- Button(onClick = { isFocusable = !isFocusable }) {
- Text("Toggle focusable")
- }
Spacer(Modifier.requiredHeight(10.dp))
Button(
onClick = {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index bc85617..39b8c81 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -316,6 +316,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun reportedText_inTextFieldWithLabel_whenEditableTextEmpty() {
textFieldValue.value = TextFieldValue()
val textFieldNode = rule.onNodeWithTag(TextFieldTag)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index cc53d27..1966425 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -37,6 +37,7 @@
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -65,7 +66,7 @@
}
@Test
- @FlakyTest(bugId = 159364185)
+ @Ignore("100% failing b/179359518")
fun dialogTest_isNotDismissed_whenClicked() {
val textBeforeClick = "textBeforeClick"
val textAfterClick = "textAfterClick"
diff --git a/core/core-google-shortcuts/api/current.txt b/core/core-google-shortcuts/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/core/core-google-shortcuts/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/core/core-google-shortcuts/api/public_plus_experimental_current.txt b/core/core-google-shortcuts/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/core/core-google-shortcuts/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/core/core-google-shortcuts/api/res-current.txt b/core/core-google-shortcuts/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/core-google-shortcuts/api/res-current.txt
diff --git a/core/core-google-shortcuts/api/restricted_current.txt b/core/core-google-shortcuts/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/core/core-google-shortcuts/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
new file mode 100644
index 0000000..8451eed
--- /dev/null
+++ b/core/core-google-shortcuts/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ // Add dependencies here
+}
+
+androidx {
+ name = "Google Shortcuts Integration Library"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.CORE_GOOGLE_SHORTCUTS
+ mavenGroup = LibraryGroups.CORE
+ inceptionYear = "2021"
+ description = "Library for powering Google features with Android app shortcuts"
+}
diff --git a/core/core-google-shortcuts/src/androidTest/AndroidManifest.xml b/core/core-google-shortcuts/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..db37e46
--- /dev/null
+++ b/core/core-google-shortcuts/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.core.google.shortcuts.test">
+
+</manifest>
diff --git a/core/core-google-shortcuts/src/main/AndroidManifest.xml b/core/core-google-shortcuts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c97471d
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.core.google.shortcuts">
+
+</manifest>
\ No newline at end of file
diff --git a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl b/core/core-google-shortcuts/src/main/androidx/core/package-info.java
similarity index 65%
copy from car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl
copy to core/core-google-shortcuts/src/main/androidx/core/package-info.java
index d48ca1b..2b06a14 100644
--- a/car/app/app-aaos/src/main/aidl/androidx/car/app/aaos/renderer/IBackButtonListener.aidl
+++ b/core/core-google-shortcuts/src/main/androidx/core/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 2021 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.
@@ -14,14 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.aaos.renderer;
-
/**
- * Classes that wish to listen for back button events should implement this.
- *
- * @hide
+ * Insert package level documentation here
*/
-interface IBackButtonListener {
- /** Notifies that the button was pressed. */
- void onBackPressed() = 1;
-}
+package androidx.core.google.shortcuts;
diff --git a/core/core/api/restricted_1.5.0-beta02.txt b/core/core/api/restricted_1.5.0-beta02.txt
index 1bb8297..4fb5f40 100644
--- a/core/core/api/restricted_1.5.0-beta02.txt
+++ b/core/core/api/restricted_1.5.0-beta02.txt
@@ -1940,7 +1940,7 @@
ctor public FontRequest(String, String, String, @ArrayRes int);
method public java.util.List<java.util.List<byte[]!>!>? getCertificates();
method @ArrayRes public int getCertificatesArrayResId();
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public String! getIdentifier();
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public String! getIdentifier();
method public String getProviderAuthority();
method public String getProviderPackage();
method public String getQuery();
@@ -1949,12 +1949,12 @@
public class FontsContractCompat {
method public static android.graphics.Typeface? buildTypeface(android.content.Context, android.os.CancellationSignal?, androidx.core.provider.FontsContractCompat.FontInfo![]);
method public static androidx.core.provider.FontsContractCompat.FontFamilyResult fetchFonts(android.content.Context, android.os.CancellationSignal?, androidx.core.provider.FontRequest) throws android.content.pm.PackageManager.NameNotFoundException;
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.graphics.Typeface! getFontSync(android.content.Context!, androidx.core.provider.FontRequest!, androidx.core.content.res.ResourcesCompat.FontCallback?, android.os.Handler?, boolean, int, int);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @VisibleForTesting public static android.content.pm.ProviderInfo? getProvider(android.content.pm.PackageManager, androidx.core.provider.FontRequest, android.content.res.Resources?) throws android.content.pm.PackageManager.NameNotFoundException;
- method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Map<android.net.Uri!,java.nio.ByteBuffer!>! prepareFontData(android.content.Context!, androidx.core.provider.FontsContractCompat.FontInfo![]!, android.os.CancellationSignal!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.graphics.Typeface! getFontSync(android.content.Context!, androidx.core.provider.FontRequest!, androidx.core.content.res.ResourcesCompat.FontCallback?, android.os.Handler?, boolean, int, int);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @VisibleForTesting public static android.content.pm.ProviderInfo? getProvider(android.content.pm.PackageManager, androidx.core.provider.FontRequest, android.content.res.Resources?) throws android.content.pm.PackageManager.NameNotFoundException;
+ method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Map<android.net.Uri!,java.nio.ByteBuffer!>! prepareFontData(android.content.Context!, androidx.core.provider.FontsContractCompat.FontInfo![]!, android.os.CancellationSignal!);
method public static void requestFont(android.content.Context, androidx.core.provider.FontRequest, androidx.core.provider.FontsContractCompat.FontRequestCallback, android.os.Handler);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void resetCache();
- field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final String PARCEL_FONT_RESULTS = "font_results";
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void resetCache();
+ field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final String PARCEL_FONT_RESULTS = "font_results";
}
public static final class FontsContractCompat.Columns implements android.provider.BaseColumns {
@@ -1972,7 +1972,7 @@
}
public static class FontsContractCompat.FontFamilyResult {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontFamilyResult(int, androidx.core.provider.FontsContractCompat.FontInfo![]?);
+ ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontFamilyResult(int, androidx.core.provider.FontsContractCompat.FontInfo![]?);
method public androidx.core.provider.FontsContractCompat.FontInfo![]! getFonts();
method public int getStatusCode();
field public static final int STATUS_OK = 0; // 0x0
@@ -1981,7 +1981,7 @@
}
public static class FontsContractCompat.FontInfo {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontInfo(android.net.Uri, @IntRange(from=0) int, @IntRange(from=1, to=1000) int, boolean, int);
+ ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontInfo(android.net.Uri, @IntRange(from=0) int, @IntRange(from=1, to=1000) int, boolean, int);
method public int getResultCode();
method @IntRange(from=0) public int getTtcIndex();
method public android.net.Uri getUri();
@@ -2000,22 +2000,22 @@
field public static final int FAIL_REASON_PROVIDER_NOT_FOUND = -1; // 0xffffffff
field public static final int FAIL_REASON_SECURITY_VIOLATION = -4; // 0xfffffffc
field public static final int FAIL_REASON_WRONG_CERTIFICATES = -2; // 0xfffffffe
- field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int RESULT_OK = 0; // 0x0
+ field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int RESULT_OK = 0; // 0x0
}
@IntDef({androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_UNAVAILABLE, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_MALFORMED_QUERY, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION, androidx.core.provider.FontsContractCompat.FontRequestCallback.RESULT_OK}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface FontsContractCompat.FontRequestCallback.FontRequestFailReason {
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SelfDestructiveThread {
- ctor public SelfDestructiveThread(String!, int, int);
- method @VisibleForTesting public int getGeneration();
- method @VisibleForTesting public boolean isRunning();
- method public <T> void postAndReply(java.util.concurrent.Callable<T!>!, androidx.core.provider.SelfDestructiveThread.ReplyCallback<T!>!);
- method public <T> T! postAndWait(java.util.concurrent.Callable<T!>!, int) throws java.lang.InterruptedException;
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SelfDestructiveThread {
+ ctor @Deprecated public SelfDestructiveThread(String!, int, int);
+ method @Deprecated @VisibleForTesting public int getGeneration();
+ method @Deprecated @VisibleForTesting public boolean isRunning();
+ method @Deprecated public <T> void postAndReply(java.util.concurrent.Callable<T!>!, androidx.core.provider.SelfDestructiveThread.ReplyCallback<T!>!);
+ method @Deprecated public <T> T! postAndWait(java.util.concurrent.Callable<T!>!, int) throws java.lang.InterruptedException;
}
- public static interface SelfDestructiveThread.ReplyCallback<T> {
- method public void onReply(T!);
+ @Deprecated public static interface SelfDestructiveThread.ReplyCallback<T> {
+ method @Deprecated public void onReply(T!);
}
}
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 1bb8297..4fb5f40 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1940,7 +1940,7 @@
ctor public FontRequest(String, String, String, @ArrayRes int);
method public java.util.List<java.util.List<byte[]!>!>? getCertificates();
method @ArrayRes public int getCertificatesArrayResId();
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public String! getIdentifier();
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public String! getIdentifier();
method public String getProviderAuthority();
method public String getProviderPackage();
method public String getQuery();
@@ -1949,12 +1949,12 @@
public class FontsContractCompat {
method public static android.graphics.Typeface? buildTypeface(android.content.Context, android.os.CancellationSignal?, androidx.core.provider.FontsContractCompat.FontInfo![]);
method public static androidx.core.provider.FontsContractCompat.FontFamilyResult fetchFonts(android.content.Context, android.os.CancellationSignal?, androidx.core.provider.FontRequest) throws android.content.pm.PackageManager.NameNotFoundException;
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.graphics.Typeface! getFontSync(android.content.Context!, androidx.core.provider.FontRequest!, androidx.core.content.res.ResourcesCompat.FontCallback?, android.os.Handler?, boolean, int, int);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @VisibleForTesting public static android.content.pm.ProviderInfo? getProvider(android.content.pm.PackageManager, androidx.core.provider.FontRequest, android.content.res.Resources?) throws android.content.pm.PackageManager.NameNotFoundException;
- method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Map<android.net.Uri!,java.nio.ByteBuffer!>! prepareFontData(android.content.Context!, androidx.core.provider.FontsContractCompat.FontInfo![]!, android.os.CancellationSignal!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.graphics.Typeface! getFontSync(android.content.Context!, androidx.core.provider.FontRequest!, androidx.core.content.res.ResourcesCompat.FontCallback?, android.os.Handler?, boolean, int, int);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @VisibleForTesting public static android.content.pm.ProviderInfo? getProvider(android.content.pm.PackageManager, androidx.core.provider.FontRequest, android.content.res.Resources?) throws android.content.pm.PackageManager.NameNotFoundException;
+ method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Map<android.net.Uri!,java.nio.ByteBuffer!>! prepareFontData(android.content.Context!, androidx.core.provider.FontsContractCompat.FontInfo![]!, android.os.CancellationSignal!);
method public static void requestFont(android.content.Context, androidx.core.provider.FontRequest, androidx.core.provider.FontsContractCompat.FontRequestCallback, android.os.Handler);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void resetCache();
- field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final String PARCEL_FONT_RESULTS = "font_results";
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void resetCache();
+ field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final String PARCEL_FONT_RESULTS = "font_results";
}
public static final class FontsContractCompat.Columns implements android.provider.BaseColumns {
@@ -1972,7 +1972,7 @@
}
public static class FontsContractCompat.FontFamilyResult {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontFamilyResult(int, androidx.core.provider.FontsContractCompat.FontInfo![]?);
+ ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontFamilyResult(int, androidx.core.provider.FontsContractCompat.FontInfo![]?);
method public androidx.core.provider.FontsContractCompat.FontInfo![]! getFonts();
method public int getStatusCode();
field public static final int STATUS_OK = 0; // 0x0
@@ -1981,7 +1981,7 @@
}
public static class FontsContractCompat.FontInfo {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontInfo(android.net.Uri, @IntRange(from=0) int, @IntRange(from=1, to=1000) int, boolean, int);
+ ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public FontsContractCompat.FontInfo(android.net.Uri, @IntRange(from=0) int, @IntRange(from=1, to=1000) int, boolean, int);
method public int getResultCode();
method @IntRange(from=0) public int getTtcIndex();
method public android.net.Uri getUri();
@@ -2000,22 +2000,22 @@
field public static final int FAIL_REASON_PROVIDER_NOT_FOUND = -1; // 0xffffffff
field public static final int FAIL_REASON_SECURITY_VIOLATION = -4; // 0xfffffffc
field public static final int FAIL_REASON_WRONG_CERTIFICATES = -2; // 0xfffffffe
- field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int RESULT_OK = 0; // 0x0
+ field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int RESULT_OK = 0; // 0x0
}
@IntDef({androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_UNAVAILABLE, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_MALFORMED_QUERY, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES, androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION, androidx.core.provider.FontsContractCompat.FontRequestCallback.RESULT_OK}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface FontsContractCompat.FontRequestCallback.FontRequestFailReason {
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SelfDestructiveThread {
- ctor public SelfDestructiveThread(String!, int, int);
- method @VisibleForTesting public int getGeneration();
- method @VisibleForTesting public boolean isRunning();
- method public <T> void postAndReply(java.util.concurrent.Callable<T!>!, androidx.core.provider.SelfDestructiveThread.ReplyCallback<T!>!);
- method public <T> T! postAndWait(java.util.concurrent.Callable<T!>!, int) throws java.lang.InterruptedException;
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SelfDestructiveThread {
+ ctor @Deprecated public SelfDestructiveThread(String!, int, int);
+ method @Deprecated @VisibleForTesting public int getGeneration();
+ method @Deprecated @VisibleForTesting public boolean isRunning();
+ method @Deprecated public <T> void postAndReply(java.util.concurrent.Callable<T!>!, androidx.core.provider.SelfDestructiveThread.ReplyCallback<T!>!);
+ method @Deprecated public <T> T! postAndWait(java.util.concurrent.Callable<T!>!, int) throws java.lang.InterruptedException;
}
- public static interface SelfDestructiveThread.ReplyCallback<T> {
- method public void onReply(T!);
+ @Deprecated public static interface SelfDestructiveThread.ReplyCallback<T> {
+ method @Deprecated public void onReply(T!);
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
index 976407c..fdc55dc 100644
--- a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
@@ -402,7 +402,7 @@
inst.runOnMainSync(new Runnable() {
@Override
public void run() {
- FontsContractCompat.getFontSync(mContext, request, callback, null,
+ FontsContractCompat.getFont(mContext, request, callback, null,
false /* isBlockingFetch */, 300 /* timeout */, Typeface.NORMAL);
}
});
diff --git a/core/core/src/androidTest/java/androidx/core/provider/SelfDestructiveThreadTest.java b/core/core/src/androidTest/java/androidx/core/provider/SelfDestructiveThreadTest.java
index 7b0387e..c2b4165 100644
--- a/core/core/src/androidTest/java/androidx/core/provider/SelfDestructiveThreadTest.java
+++ b/core/core/src/androidTest/java/androidx/core/provider/SelfDestructiveThreadTest.java
@@ -16,8 +16,6 @@
package androidx.core.provider;
-import static androidx.core.provider.SelfDestructiveThread.ReplyCallback;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -43,12 +41,13 @@
/**
* Tests for {@link SelfDestructiveThread}
*/
+// TODO Remove
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SelfDestructiveThreadTest {
private static final int DEFAULT_TIMEOUT = 1000;
- private void waitUntilDestruction(SelfDestructiveThread thread, long timeoutMs) {
+ private void waitUntilDestruction(FontRequestThreadPool thread, long timeoutMs) {
if (!thread.isRunning()) {
return;
}
@@ -86,11 +85,11 @@
@Test
public void testDestruction() throws InterruptedException {
final int destructAfterLastActivityInMs = 300;
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
thread.postAndWait(new Callable<Object>() {
@Override
- public Object call() throws Exception {
+ public Object call() {
return null;
}
}, DEFAULT_TIMEOUT);
@@ -98,25 +97,22 @@
assertFalse(thread.isRunning());
}
+ private static class IdCallable implements Callable<Integer> {
+ @Override
+ public Integer call() {
+ return System.identityHashCode(Thread.currentThread());
+ }
+ }
+
@Test
public void testReconstruction() throws InterruptedException {
final int destructAfterLastActivityInMs = 300;
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
- Integer generation = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer generation = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(generation);
waitUntilDestruction(thread, DEFAULT_TIMEOUT);
- Integer nextGeneration = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer nextGeneration = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(nextGeneration);
assertNotEquals(generation.intValue(), nextGeneration.intValue());
}
@@ -124,21 +120,11 @@
@Test
public void testReuseSameThread() throws InterruptedException {
final int destructAfterLastActivityInMs = 300;
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
- Integer generation = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer generation = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(generation);
- Integer nextGeneration = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer nextGeneration = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(nextGeneration);
waitUntilDestruction(thread, DEFAULT_TIMEOUT);
assertEquals(generation.intValue(), nextGeneration.intValue());
@@ -148,25 +134,16 @@
@Test
public void testReuseSameThread_Multiple() throws InterruptedException {
final int destructAfterLastActivityInMs = 300;
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
- Integer generation = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer generation = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(generation);
int firstGeneration = generation.intValue();
+
for (int i = 0; i < 10; ++i) {
// Less than renewal duration, so that the same thread must be used.
waitMillis(destructAfterLastActivityInMs / 2);
- Integer nextGeneration = thread.postAndWait(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- return thread.getGeneration();
- }
- }, DEFAULT_TIMEOUT);
+ Integer nextGeneration = thread.postAndWait(new IdCallable(), DEFAULT_TIMEOUT);
assertNotNull(nextGeneration);
assertEquals(firstGeneration, nextGeneration.intValue());
}
@@ -176,14 +153,14 @@
@Test
public void testTimeout() {
final int destructAfterLastActivityInMs = 300;
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
final int timeoutMs = 300;
try {
thread.postAndWait(new Callable<Object>() {
@Override
- public Object call() throws Exception {
+ public Object call() {
waitMillis(timeoutMs * 3); // Wait longer than timeout.
return new Object();
}
@@ -194,7 +171,7 @@
}
}
- private class WaitableReplyCallback implements ReplyCallback<Integer> {
+ private class WaitableReplyCallback implements FontRequestThreadPool.ReplyCallback<Integer> {
private final ReentrantLock mLock = new ReentrantLock();
private final Condition mCond = mLock.newCondition();
@@ -258,12 +235,12 @@
final Callable<Integer> callable = new Callable<Integer>() {
@Override
- public Integer call() throws Exception {
+ public Integer call() {
return expectedResult;
}
};
final WaitableReplyCallback reply = new WaitableReplyCallback();
- final SelfDestructiveThread thread = new SelfDestructiveThread(
+ final FontRequestThreadPool thread = new FontRequestThreadPool(
"test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
index f91741c7..83bd973 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
@@ -139,7 +139,7 @@
: fontCallback == null;
final int timeout = isRequestFromLayoutInflator ? providerEntry.getTimeout()
: FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE;
- typeface = FontsContractCompat.getFontSync(context, providerEntry.getRequest(),
+ typeface = FontsContractCompat.getFont(context, providerEntry.getRequest(),
fontCallback, handler, isBlocking, timeout, style);
} else {
typeface = sTypefaceCompatImpl.createFromFontFamilyFilesResourceEntry(
diff --git a/core/core/src/main/java/androidx/core/provider/FontProvider.java b/core/core/src/main/java/androidx/core/provider/FontProvider.java
index c9729e1..5a1e691 100644
--- a/core/core/src/main/java/androidx/core/provider/FontProvider.java
+++ b/core/core/src/main/java/androidx/core/provider/FontProvider.java
@@ -34,6 +34,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.FontResourcesParserCompat;
+import androidx.core.provider.FontsContractCompat.FontFamilyResult;
+import androidx.core.provider.FontsContractCompat.FontInfo;
import java.util.ArrayList;
import java.util.Arrays;
@@ -45,20 +47,18 @@
private FontProvider() {}
@NonNull
- static FontsContractCompat.FontFamilyResult getFontFamilyResult(@NonNull Context context,
+ static FontFamilyResult getFontFamilyResult(@NonNull Context context,
@NonNull FontRequest request, @Nullable CancellationSignal cancellationSignal)
throws PackageManager.NameNotFoundException {
ProviderInfo providerInfo = getProvider(
context.getPackageManager(), request, context.getResources());
if (providerInfo == null) {
- return new FontsContractCompat.FontFamilyResult(
- FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
+ return FontFamilyResult.create(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
}
- FontsContractCompat.FontInfo[] fonts = query(
+ FontInfo[] fonts = query(
context, request, providerInfo.authority, cancellationSignal);
- return new FontsContractCompat.FontFamilyResult(
- FontsContractCompat.FontFamilyResult.STATUS_OK, fonts);
+ return FontFamilyResult.create(FontFamilyResult.STATUS_OK, fonts);
}
/**
@@ -112,13 +112,13 @@
@SuppressWarnings("UnsafeNewApiCall")
@VisibleForTesting
@NonNull
- static FontsContractCompat.FontInfo[] query(
+ static FontInfo[] query(
Context context,
FontRequest request,
String authority,
CancellationSignal cancellationSignal
) {
- ArrayList<FontsContractCompat.FontInfo> result = new ArrayList<>();
+ ArrayList<FontInfo> result = new ArrayList<>();
final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.build();
@@ -175,8 +175,7 @@
int weight = weightColumnIndex != -1 ? cursor.getInt(weightColumnIndex) : 400;
boolean italic = italicColumnIndex != -1 && cursor.getInt(italicColumnIndex)
== 1;
- result.add(new FontsContractCompat.FontInfo(fileUri, ttcIndex, weight, italic,
- resultCode));
+ result.add(FontInfo.create(fileUri, ttcIndex, weight, italic, resultCode));
}
}
} finally {
@@ -184,7 +183,7 @@
cursor.close();
}
}
- return result.toArray(new FontsContractCompat.FontInfo[0]);
+ return result.toArray(new FontInfo[0]);
}
private static List<List<byte[]>> getCertificates(FontRequest request, Resources resources) {
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequest.java b/core/core/src/main/java/androidx/core/provider/FontRequest.java
index 6fb880e..4b692c3 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequest.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequest.java
@@ -16,6 +16,7 @@
package androidx.core.provider;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.util.Base64;
@@ -137,12 +138,25 @@
return mCertificatesArray;
}
- /** @hide */
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ *
+ * @hide
+ */
+ @Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public String getIdentifier() {
return mIdentifier;
}
+ /** @hide */
+ @RestrictTo(LIBRARY)
+ @NonNull
+ String getId() {
+ return mIdentifier;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java b/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java
new file mode 100644
index 0000000..74e0970
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 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.core.provider;
+
+import android.os.Handler;
+import android.os.Process;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+class FontRequestThreadPool {
+ private final ThreadPoolExecutor mExecutor;
+ private final ThreadFactory mThreadFactory;
+
+ FontRequestThreadPool(
+ @NonNull String threadName,
+ int threadPriority,
+ @IntRange(from = 0) int keepAliveTimeInMillis
+ ) {
+ mThreadFactory = new ThreadFactory(threadName, threadPriority);
+
+ // allow core thread timeout to timeout the core threads so that
+ // when no tasks arrive, the core threads can be killed
+ mExecutor = new ThreadPoolExecutor(
+ 0 /* corePoolSize */,
+ 1 /* maximumPoolSize */,
+ keepAliveTimeInMillis /* keepAliveTime */,
+ TimeUnit.MILLISECONDS /* keepAliveTime TimeUnit */,
+ new LinkedBlockingDeque<Runnable>() /* unbounded queue*/,
+ mThreadFactory
+ );
+ mExecutor.allowCoreThreadTimeOut(true);
+ }
+
+ <T> T postAndWait(
+ @NonNull final Callable<T> callable,
+ @IntRange(from = 0) int timeoutMillis
+ ) throws InterruptedException {
+ Future<T> future = mExecutor.submit(callable);
+ try {
+ return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ throw e;
+ } catch (TimeoutException e) {
+ throw new InterruptedException("timeout");
+ }
+ }
+
+ <T> void postAndReply(
+ @NonNull final Callable<T> callable,
+ @NonNull final ReplyCallback<T> callback
+ ) {
+ final Handler calleeHandler = CalleeHandler.create();
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ T t;
+ try {
+ t = callable.call();
+ } catch (Exception e) {
+ t = null;
+ }
+ final T result = t;
+
+ calleeHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onReply(result);
+ }
+ });
+ }
+ });
+ }
+
+ // TODO Remove
+ @VisibleForTesting
+ boolean isRunning() {
+ return mExecutor.getPoolSize() != 0;
+ }
+
+ /**
+ * Reply callback for postAndReply
+ *
+ * @param <T> A type which will be received as the argument.
+ */
+ interface ReplyCallback<T> {
+ /**
+ * Called when the task was finished.
+ */
+ void onReply(T value);
+ }
+
+ private static class ThreadFactory implements java.util.concurrent.ThreadFactory {
+ private String mThreadName;
+ private int mPriority;
+
+ ThreadFactory(@NonNull String threadName, int priority) {
+ mThreadName = threadName;
+ mPriority = priority;
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ return new ProcessPriorityThread(runnable, mThreadName, mPriority);
+ }
+
+ private static class ProcessPriorityThread extends Thread {
+ private final int mPriority;
+
+ ProcessPriorityThread(Runnable target, String name, int priority) {
+ super(target, name);
+ mPriority = priority;
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(mPriority);
+ super.run();
+ }
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
index b599ab3..e57f6c55 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
@@ -23,6 +23,7 @@
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
@@ -37,7 +38,7 @@
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.TypefaceCompat;
-import androidx.core.provider.SelfDestructiveThread.ReplyCallback;
+import androidx.core.provider.FontRequestThreadPool.ReplyCallback;
import java.util.ArrayList;
import java.util.concurrent.Callable;
@@ -48,10 +49,11 @@
static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
- private static final int BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS = 10000;
- private static final SelfDestructiveThread BACKGROUND_THREAD =
- new SelfDestructiveThread("fonts-androidx", Process.THREAD_PRIORITY_BACKGROUND,
- BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS);
+ private static final FontRequestThreadPool BACKGROUND_THREAD = new FontRequestThreadPool(
+ "fonts-androidx",
+ Process.THREAD_PRIORITY_BACKGROUND,
+ 10000 /* keepAliveTime */
+ );
/** Package protected to prevent synthetic accessor */
static final Object LOCK = new Object();
@@ -174,7 +176,7 @@
@Nullable final ResourcesCompat.FontCallback fontCallback,
@Nullable final Handler handler, boolean isBlockingFetch, int timeout,
final int style) {
- final String id = request.getIdentifier() + "-" + style;
+ final String id = request.getId() + "-" + style;
Typeface cached = sTypefaceCache.get(id);
if (cached != null) {
if (fontCallback != null) {
@@ -198,7 +200,7 @@
final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
@Override
- public TypefaceResult call() throws Exception {
+ public TypefaceResult call() {
TypefaceResult typeface = getFontInternal(context, request, style);
if (typeface.mTypeface != null) {
sTypefaceCache.put(id, typeface.mTypeface);
@@ -214,21 +216,21 @@
return null;
}
} else {
- final ReplyCallback<TypefaceResult> reply = fontCallback == null ? null
- : new ReplyCallback<TypefaceResult>() {
- @Override
- public void onReply(final TypefaceResult typeface) {
- if (typeface == null) {
- fontCallback.callbackFailAsync(
- FAIL_REASON_FONT_NOT_FOUND, handler);
- } else if (typeface.mResult
- == FontsContractCompat.FontFamilyResult.STATUS_OK) {
- fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
- } else {
- fontCallback.callbackFailAsync(typeface.mResult, handler);
- }
- }
- };
+ final ReplyCallback<TypefaceResult> reply = fontCallback == null ? null :
+ new ReplyCallback<TypefaceResult>() {
+ @Override
+ public void onReply(final TypefaceResult typeface) {
+ if (typeface == null) {
+ fontCallback.callbackFailAsync(
+ FAIL_REASON_FONT_NOT_FOUND, handler);
+ } else if (typeface.mResult
+ == FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
+ } else {
+ fontCallback.callbackFailAsync(typeface.mResult, handler);
+ }
+ }
+ };
synchronized (LOCK) {
ArrayList<ReplyCallback<TypefaceResult>> pendingReplies = PENDING_REPLIES.get(id);
@@ -267,6 +269,7 @@
}
/** Package protected to prevent synthetic accessor */
+ @SuppressLint("WrongConstant")
@NonNull
static TypefaceResult getFontInternal(
@NonNull final Context context,
@@ -282,7 +285,7 @@
final Typeface typeface = TypefaceCompat.createFromFontInfo(
context, null /* CancellationSignal */, result.getFonts(), style);
return new TypefaceResult(typeface, typeface != null
- ? FontsContractCompat.FontRequestCallback.RESULT_OK
+ ? FontsContractCompat.FontRequestCallback.RESULT_SUCCESS
: FAIL_REASON_FONT_LOAD_ERROR);
}
int resultCode = result.getStatusCode() == STATUS_WRONG_CERTIFICATES
diff --git a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
index 4f9327a..a878fd3 100644
--- a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
+++ b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
@@ -16,6 +16,7 @@
package androidx.core.provider;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.content.Context;
@@ -52,28 +53,6 @@
public class FontsContractCompat {
private FontsContractCompat() { }
- // TODO remove unused
- /**
- * Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
- * returned to the ResultReceiver in getFont.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- public static final String PARCEL_FONT_RESULTS = "font_results";
-
- // TODO remove unused
- // Error codes internal to the system, which can not come from a provider. To keep the number
- // space open for new provider codes, these should all be negative numbers.
- /** @hide */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1;
-
- // TODO remove unused
- /** @hide */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- static final int RESULT_CODE_WRONG_CERTIFICATES = -2;
- // Note -3 is used by FontRequestCallback to indicate the font failed to load.
-
// TODO deprecated from here, move to TypefaceCompat
/**
* Build a Typeface from an array of {@link FontInfo}
@@ -88,8 +67,8 @@
* @return A Typeface object. Returns null if typeface creation fails.
*/
@Nullable
- public static Typeface buildTypeface(@
- NonNull Context context,
+ public static Typeface buildTypeface(
+ @NonNull Context context,
@Nullable CancellationSignal cancellationSignal,
@NonNull FontInfo[] fonts
) {
@@ -143,66 +122,30 @@
return FontProvider.getFontFamilyResult(context, request, cancellationSignal);
}
- // TODO remove, replace with LIBRARY private, used for tests
/**
- * Used for tests, should not be used otherwise.
+ * Used by TypefaceCompat and tests.
* @hide
- **/
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- public static void resetCache() {
- FontRequestWorker.resetTypefaceCache();
- }
-
- // TODO @RestrictTo(LIBRARY)
- /** @hide */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- public static Typeface getFontSync(
- final Context context,
- final FontRequest request,
- final @Nullable ResourcesCompat.FontCallback fontCallback,
- final @Nullable Handler handler,
+ */
+ @RestrictTo(LIBRARY)
+ @Nullable
+ public static Typeface getFont(
+ @NonNull final Context context,
+ @NonNull final FontRequest request,
+ @Nullable final ResourcesCompat.FontCallback fontCallback,
+ @Nullable final Handler handler,
boolean isBlockingFetch,
- int timeout,
+ @IntRange(from = 0) int timeout,
final int style
) {
return FontRequestWorker.getTypeface(context, request, fontCallback, handler,
isBlockingFetch, timeout, style);
}
- // TODO remove
- /**
- * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
- *
- * Skip if the file contents is not ready to be read.
- *
- * @param context A {@link Context} to be used for resolving content URI in
- * {@link FontInfo}.
- * @param fonts An array of {@link FontInfo}.
- * @return A map from {@link Uri} to {@link ByteBuffer}.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- @RequiresApi(19)
- public static Map<Uri, ByteBuffer> prepareFontData(
- Context context,
- FontInfo[] fonts,
- CancellationSignal cancellationSignal
- ) {
- return TypefaceCompatUtil.readFontInfoIntoByteBuffer(context, fonts, cancellationSignal);
- }
-
-
- // TODO: Remove, unused
/** @hide */
@VisibleForTesting
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static ProviderInfo getProvider(
- @NonNull PackageManager packageManager,
- @NonNull FontRequest request,
- @Nullable Resources resources
- ) throws PackageManager.NameNotFoundException {
- return FontProvider.getProvider(packageManager, request, resources);
+ @RestrictTo(LIBRARY)
+ public static void resetTypefaceCache() {
+ FontRequestWorker.resetTypefaceCache();
}
/**
@@ -302,14 +245,20 @@
* @param italic A boolean that indicates the font is italic style or not.
* @param resultCode A boolean that indicates the font contents is ready.
*
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ *
* @hide
*/
+ // TODO after removing from public API make package private.
+ @Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public FontInfo(
@NonNull Uri uri,
@IntRange(from = 0) int ttcIndex,
@IntRange(from = 1, to = 1000) int weight,
- boolean italic, int resultCode
+ boolean italic,
+ int resultCode
) {
mUri = Preconditions.checkNotNull(uri);
mTtcIndex = ttcIndex;
@@ -318,6 +267,17 @@
mResultCode = resultCode;
}
+ @SuppressWarnings("deprecation")
+ static FontInfo create(
+ @NonNull Uri uri,
+ @IntRange(from = 0) int ttcIndex,
+ @IntRange(from = 1, to = 1000) int weight,
+ boolean italic,
+ int resultCode
+ ) {
+ return new FontInfo(uri, ttcIndex, weight, italic, resultCode);
+ }
+
/**
* Returns a URI associated to this record.
*/
@@ -381,7 +341,7 @@
public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
/** @hide */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @RestrictTo(LIBRARY)
@IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
@Retention(RetentionPolicy.SOURCE)
@interface FontResultStatus {}
@@ -389,7 +349,13 @@
private final @FontResultStatus int mStatusCode;
private final FontInfo[] mFonts;
- /** @hide */
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ // TODO after removing from public API make package private.
+ @Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
mStatusCode = statusCode;
@@ -403,16 +369,30 @@
public FontInfo[] getFonts() {
return mFonts;
}
+
+ @SuppressWarnings("deprecation")
+ static FontFamilyResult create(
+ @FontResultStatus int statusCode,
+ @Nullable FontInfo[] fonts) {
+ return new FontFamilyResult(statusCode, fonts);
+ }
}
/**
* Interface used to receive asynchronously fetched typefaces.
*/
public static class FontRequestCallback {
- /** @hide */
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ */
+ @Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final int RESULT_OK = Columns.RESULT_CODE_OK;
+ static final int RESULT_SUCCESS = Columns.RESULT_CODE_OK;
+
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
* provider was not found on the device.
@@ -455,7 +435,9 @@
*/
public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
+ // TODO Move to @RestrictTo(LIBRARY)
/** @hide */
+ @SuppressWarnings("deprecation")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
@@ -488,5 +470,111 @@
public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {}
}
+ /**
+ * Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
+ * returned to the ResultReceiver in getFont.
+ *
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ *
+ * @hide
+ */
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public static final String PARCEL_FONT_RESULTS = "font_results";
+ // Error codes internal to the system, which can not come from a provider. To keep the number
+ // space open for new provider codes, these should all be negative numbers.
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1;
+
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static final int RESULT_CODE_WRONG_CERTIFICATES = -2;
+ // Note -3 is used by FontRequestCallback to indicate the font failed to load.
+
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public static Typeface getFontSync(
+ final Context context,
+ final FontRequest request,
+ final @Nullable ResourcesCompat.FontCallback fontCallback,
+ final @Nullable Handler handler,
+ boolean isBlockingFetch,
+ int timeout,
+ final int style
+ ) {
+ return FontRequestWorker.getTypeface(context, request, fontCallback, handler,
+ isBlockingFetch, timeout, style);
+ }
+
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public static void resetCache() {
+ FontRequestWorker.resetTypefaceCache();
+ }
+
+ /**
+ * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
+ *
+ * Skip if the file contents is not ready to be read.
+ *
+ * @param context A {@link Context} to be used for resolving content URI in
+ * {@link FontInfo}.
+ * @param fonts An array of {@link FontInfo}.
+ * @return A map from {@link Uri} to {@link ByteBuffer}.
+ *
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ *
+ * @hide
+ */
+ @Deprecated // unused
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @RequiresApi(19)
+ public static Map<Uri, ByteBuffer> prepareFontData(
+ Context context,
+ FontInfo[] fonts,
+ CancellationSignal cancellationSignal
+ ) {
+ return TypefaceCompatUtil.readFontInfoIntoByteBuffer(context, fonts, cancellationSignal);
+ }
+
+ /**
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
+ * @hide
+ **/
+ @Deprecated // unused
+ @VisibleForTesting
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Nullable
+ public static ProviderInfo getProvider(
+ @NonNull PackageManager packageManager,
+ @NonNull FontRequest request,
+ @Nullable Resources resources
+ ) throws PackageManager.NameNotFoundException {
+ return FontProvider.getProvider(packageManager, request, resources);
+ }
}
diff --git a/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java b/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
index 7a8e69c..9ccb661 100644
--- a/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
+++ b/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
@@ -37,8 +37,12 @@
/**
* Background thread which is destructed after certain period after all pending activities are
* finished.
+ *
+ * @deprecated Not being used by any cross library, and should not be used, internal
+ * implementation detail.
* @hide
*/
+@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class SelfDestructiveThread {
private final Object mLock = new Object();
diff --git a/datastore/datastore-preferences/api/current.txt b/datastore/datastore-preferences/api/current.txt
index 2dbaa4c..c31d7d3 100644
--- a/datastore/datastore-preferences/api/current.txt
+++ b/datastore/datastore-preferences/api/current.txt
@@ -9,7 +9,8 @@
}
public final class SharedPreferencesMigrationKt {
- method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate, optional boolean deleteEmptyPreferences);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, optional java.util.Set<java.lang.String>? keysToMigrate);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
}
diff --git a/datastore/datastore-preferences/api/public_plus_experimental_current.txt b/datastore/datastore-preferences/api/public_plus_experimental_current.txt
index 2dbaa4c..c31d7d3 100644
--- a/datastore/datastore-preferences/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-preferences/api/public_plus_experimental_current.txt
@@ -9,7 +9,8 @@
}
public final class SharedPreferencesMigrationKt {
- method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate, optional boolean deleteEmptyPreferences);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, optional java.util.Set<java.lang.String>? keysToMigrate);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
}
diff --git a/datastore/datastore-preferences/api/restricted_current.txt b/datastore/datastore-preferences/api/restricted_current.txt
index 2dbaa4c..c31d7d3 100644
--- a/datastore/datastore-preferences/api/restricted_current.txt
+++ b/datastore/datastore-preferences/api/restricted_current.txt
@@ -9,7 +9,8 @@
}
public final class SharedPreferencesMigrationKt {
- method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate, optional boolean deleteEmptyPreferences);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, optional java.util.Set<java.lang.String>? keysToMigrate);
+ method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, optional java.util.Set<java.lang.String>? keysToMigrate);
method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.core.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
}
diff --git a/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt b/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
index f24d608..786da57 100644
--- a/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
+++ b/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
@@ -250,7 +250,6 @@
context = context,
sharedPreferencesName = sharedPrefsName,
keysToMigrate = setOf(integerKey.name),
- deleteEmptyPreferences = true
)
val preferenceStore = getDataStoreWithMigrations(listOf(migration))
@@ -260,27 +259,6 @@
}
@Test
- fun sharedPreferencesFileNotDeletedIfDisabled() = runBlockingTest {
- val integerKey = intPreferencesKey("integer_key")
-
- assertTrue { sharedPrefs.edit().putInt(integerKey.name, 123).commit() }
-
- val migration = SharedPreferencesMigration(
- context = context,
- sharedPreferencesName = sharedPrefsName,
- keysToMigrate = setOf(integerKey.name),
- deleteEmptyPreferences = false
- )
-
- val preferenceStore = getDataStoreWithMigrations(listOf(migration))
- preferenceStore.data.first()
-
- assertTrue {
- getSharedPrefsFile(context, sharedPrefsName).exists()
- }
- }
-
- @Test
fun sharedPreferencesFileNotDeletedIfPrefsNotEmpty() = runBlockingTest {
val integerKey1 = intPreferencesKey("integer_key1")
val integerKey2 = intPreferencesKey("integer_key2")
@@ -293,7 +271,6 @@
context = context,
sharedPreferencesName = sharedPrefsName,
keysToMigrate = setOf(integerKey1.name),
- deleteEmptyPreferences = true
)
val preferenceStore = getDataStoreWithMigrations(listOf(migration))
@@ -317,7 +294,6 @@
context = context,
sharedPreferencesName = sharedPrefsName,
keysToMigrate = setOf(integerKey.name),
- deleteEmptyPreferences = true
)
val preferenceStore = getDataStoreWithMigrations(listOf(migration))
@@ -399,6 +375,21 @@
assertEquals(1, prefs.asMap().size)
}
+ @Test
+ fun producedSharedPreferencesIsUsed() = runBlockingTest {
+ val integerKey = intPreferencesKey("integer_key")
+ val integerValue = 123
+
+ assertTrue { sharedPrefs.edit().putInt(integerKey.name, integerValue).commit() }
+
+ val migration = SharedPreferencesMigration(produceSharedPreferences = { sharedPrefs })
+
+ val preferencesStore = getDataStoreWithMigrations(listOf(migration))
+ val prefs = preferencesStore.data.first()
+ assertEquals(integerValue, prefs[integerKey])
+ assertEquals(1, prefs.asMap().size)
+ }
+
private fun getDataStoreWithMigrations(
migrations: List<DataMigration<Preferences>>
): DataStore<Preferences> {
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt
index b91abe3..157a9bf 100644
--- a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt
+++ b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt
@@ -17,6 +17,7 @@
package androidx.datastore.preferences
import android.content.Context
+import android.content.SharedPreferences
import androidx.datastore.migrations.SharedPreferencesView
import androidx.datastore.migrations.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
@@ -30,78 +31,103 @@
/**
* Creates a SharedPreferencesMigration for DataStore<Preferences>.
*
+ * Note: This migration only supports the basic SharedPreferences types: boolean, float, int,
+ * long, string and string set. If the result of getAll contains other types, they will be ignored.
+ *
+ * @param produceSharedPreferences Should return the instance of SharedPreferences to migrate from.
+ * @param keysToMigrate The list of keys to migrate. The keys will be mapped to
+ * datastore.Preferences with their same values. If the key is already present in the new
+ * Preferences, the key will not be migrated again. If the key is not present in the
+ * SharedPreferences it will not be migrated. If keysToMigrate is not set, all keys will be
+ * migrated from the existing SharedPreferences.
+ */
+@JvmOverloads // Generate methods for default params for java users.
+public fun SharedPreferencesMigration(
+ produceSharedPreferences: () -> SharedPreferences,
+ keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS
+): SharedPreferencesMigration<Preferences> {
+ return SharedPreferencesMigration(
+ produceSharedPreferences = produceSharedPreferences,
+ keysToMigrate = keysToMigrate,
+ shouldRunMigration = getShouldRunMigration(keysToMigrate),
+ migrate = getMigrationFunction(),
+ )
+}
+
+/**
+ * Creates a SharedPreferencesMigration for DataStore<Preferences>.
+ *
+ * If the SharedPreferences is empty once the migration completes, this migration will attempt to
+ * delete it.
+ *
* @param context Context used for getting SharedPreferences.
* @param sharedPreferencesName The name of the SharedPreferences.
- * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore.Preferences with
- * their same values. If the key is already present in the new Preferences, the key
- * will not be migrated again. If the key is not present in the SharedPreferences it
- * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the existing
- * SharedPreferences.
- * @param deleteEmptyPreferences If enabled and the SharedPreferences are empty (i.e. no remaining
- * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
- * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
- * SharedPreferences to begin with then the (potentially) empty SharedPreferences
- * won't be cleaned up by this option. This functionality is best effort - if there
- * is an issue deleting the SharedPreferences file it will be silently ignored.
- *
- * TODO(rohitsat): determine whether to remove the deleteEmptyPreferences option.
+ * @param keysToMigrate The list of keys to migrate. The keys will be mapped to
+ * datastore.Preferences with their same values. If the key is already present in the new
+ * Preferences, the key will not be migrated again. If the key is not present in the
+ * SharedPreferences it will not be migrated. If keysToMigrate is not set, all keys will be
+ * migrated from the existing SharedPreferences.
*/
@JvmOverloads // Generate methods for default params for java users.
public fun SharedPreferencesMigration(
context: Context,
sharedPreferencesName: String,
keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
- deleteEmptyPreferences: Boolean = true
): SharedPreferencesMigration<Preferences> {
return SharedPreferencesMigration(
context = context,
sharedPreferencesName = sharedPreferencesName,
keysToMigrate = keysToMigrate,
- deleteEmptyPreferences = deleteEmptyPreferences,
- shouldRunMigration = { prefs ->
- // If any key hasn't been migrated to currentData, we can't skip the migration. If
- // the key set is not specified, we can't skip the migration.
- val allKeys = prefs.asMap().keys.map { it.name }
- keysToMigrate?.any { it !in allKeys } ?: true
- },
- migrate = { sharedPrefs: SharedPreferencesView, currentData: Preferences ->
- // prefs.getAll is already filtered to our key set, but we don't want to overwrite
- // already existing keys.
- val currentKeys = currentData.asMap().keys.map { it.name }
-
- val filteredSharedPreferences =
- sharedPrefs.getAll().filter { (key, _) -> key !in currentKeys }
-
- val mutablePreferences = currentData.toMutablePreferences()
- for ((key, value) in filteredSharedPreferences) {
- when (value) {
- is Boolean -> mutablePreferences[
- booleanPreferencesKey(key)
- ] = value
- is Float -> mutablePreferences[
- floatPreferencesKey(key)
- ] = value
- is Int -> mutablePreferences[
- intPreferencesKey(key)
- ] = value
- is Long -> mutablePreferences[
- longPreferencesKey(key)
- ] = value
- is String -> mutablePreferences[
- stringPreferencesKey(key)
- ] = value
- is Set<*> -> {
- @Suppress("UNCHECKED_CAST")
- mutablePreferences[
- stringSetPreferencesKey(key)
- ] = value as Set<String>
- }
- }
- }
-
- mutablePreferences.toPreferences()
- }
+ shouldRunMigration = getShouldRunMigration(keysToMigrate),
+ migrate = getMigrationFunction()
)
}
+private fun getMigrationFunction(): suspend (SharedPreferencesView, Preferences) -> Preferences =
+ { sharedPrefs: SharedPreferencesView, currentData: Preferences ->
+ // prefs.getAll is already filtered to our key set, but we don't want to overwrite
+ // already existing keys.
+ val currentKeys = currentData.asMap().keys.map { it.name }
+
+ val filteredSharedPreferences =
+ sharedPrefs.getAll().filter { (key, _) -> key !in currentKeys }
+
+ val mutablePreferences = currentData.toMutablePreferences()
+ for ((key, value) in filteredSharedPreferences) {
+ when (value) {
+ is Boolean -> mutablePreferences[
+ booleanPreferencesKey(key)
+ ] = value
+ is Float -> mutablePreferences[
+ floatPreferencesKey(key)
+ ] = value
+ is Int -> mutablePreferences[
+ intPreferencesKey(key)
+ ] = value
+ is Long -> mutablePreferences[
+ longPreferencesKey(key)
+ ] = value
+ is String -> mutablePreferences[
+ stringPreferencesKey(key)
+ ] = value
+ is Set<*> -> {
+ @Suppress("UNCHECKED_CAST")
+ mutablePreferences[
+ stringSetPreferencesKey(key)
+ ] = value as Set<String>
+ }
+ }
+ }
+
+ mutablePreferences.toPreferences()
+ }
+
+private fun getShouldRunMigration(keysToMigrate: Set<String>?): suspend (Preferences) -> Boolean =
+ { prefs ->
+ // If any key hasn't been migrated to currentData, we can't skip the migration. If
+ // the key set is not specified, we can't skip the migration.
+ val allKeys = prefs.asMap().keys.map { it.name }
+ keysToMigrate?.any { it !in allKeys } ?: true
+ }
+
internal val MIGRATE_ALL_KEYS = null
\ No newline at end of file
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
index 4878ed3..b55ea08 100644
--- a/datastore/datastore-rxjava2/api/current.txt
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
index 4878ed3..b55ea08 100644
--- a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
index 4878ed3..b55ea08 100644
--- a/datastore/datastore-rxjava2/api/restricted_current.txt
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
index d97261b..1461ee7 100644
--- a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
@@ -156,26 +156,6 @@
assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
}
- @Test
- public void testDeletesEmptySharedPreferences() {
- String key = "key";
- String value = "value";
- assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
-
- DataMigration<Byte> dataMigration =
- getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
- true).build();
- RxDataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
- assertThat(byteStore.data().blockingFirst()).isEqualTo(0);
-
- // Check that the shared preferences files are deleted
- File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
- File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
- File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
- assertThat(prefsFile.exists()).isFalse();
- assertThat(backupPrefsFile.exists()).isFalse();
- }
-
private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
return new RxSharedPreferencesMigrationBuilder<Byte>(mContext, mSharedPrefsName,
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
index e84e377..0b294be 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -71,8 +71,6 @@
private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
) {
- /** Optional */
- private var deleteEmptyPreference: Boolean = true
private var keysToMigrate: Set<String>? = null
/**
@@ -94,24 +92,10 @@
}
/**
- * If enabled and the SharedPreferences are empty (i.e. no remaining
- * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
- * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
- * SharedPreferences to begin with then the (potentially) empty SharedPreferences
- * won't be cleaned up by this option. This functionality is best effort - if there
- * is an issue deleting the SharedPreferences file it will be silently ignored.
+ * Build and return the DataMigration instance.
*
- * This method is optional and defaults to true.
- *
- * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
- * @return this
+ * @return the DataMigration.
*/
- @Suppress("MissingGetterMatchingBuilder")
- public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
- RxSharedPreferencesMigrationBuilder<T> = apply {
- this.deleteEmptyPreference = deleteEmptyPreferences
- }
-
public fun build(): DataMigration<T> {
return SharedPreferencesMigration(
context = context,
@@ -120,7 +104,6 @@
rxSharedPreferencesMigration.migrate(spView, curData).await()
},
keysToMigrate = keysToMigrate,
- deleteEmptyPreferences = deleteEmptyPreference,
shouldRunMigration = { curData ->
rxSharedPreferencesMigration.shouldMigrate(curData).await()
}
diff --git a/datastore/datastore-rxjava3/api/current.txt b/datastore/datastore-rxjava3/api/current.txt
index 35b64273..2a6f395 100644
--- a/datastore/datastore-rxjava3/api/current.txt
+++ b/datastore/datastore-rxjava3/api/current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
index 35b64273..2a6f395 100644
--- a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava3/api/restricted_current.txt b/datastore/datastore-rxjava3/api/restricted_current.txt
index 35b64273..2a6f395 100644
--- a/datastore/datastore-rxjava3/api/restricted_current.txt
+++ b/datastore/datastore-rxjava3/api/restricted_current.txt
@@ -33,7 +33,6 @@
public final class RxSharedPreferencesMigrationBuilder<T> {
ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
method public androidx.datastore.core.DataMigration<T> build();
- method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
diff --git a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
index 9d3d36b..10a141c 100644
--- a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
+++ b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
@@ -156,25 +156,7 @@
assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
}
- @Test
- public void testDeletesEmptySharedPreferences() {
- String key = "key";
- String value = "value";
- assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
- DataMigration<Byte> dataMigration =
- getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
- true).build();
- RxDataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
- assertThat(byteStore.data().blockingFirst()).isEqualTo(0);
-
- // Check that the shared preferences files are deleted
- File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
- File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
- File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
- assertThat(prefsFile.exists()).isFalse();
- assertThat(backupPrefsFile.exists()).isFalse();
- }
private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
index e5f71c6..7e76603 100644
--- a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -71,8 +71,6 @@
private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
) {
- /** Optional */
- private var deleteEmptyPreference: Boolean = true
private var keysToMigrate: Set<String>? = null
/**
@@ -88,30 +86,16 @@
* @return this
*/
@Suppress("MissingGetterMatchingBuilder")
- public fun setKeysToMigrate(vararg keys: String):
- RxSharedPreferencesMigrationBuilder<T> = apply {
+ public fun setKeysToMigrate(vararg keys: String): RxSharedPreferencesMigrationBuilder<T> =
+ apply {
keysToMigrate = setOf(*keys)
}
/**
- * If enabled and the SharedPreferences are empty (i.e. no remaining
- * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
- * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
- * SharedPreferences to begin with then the (potentially) empty SharedPreferences
- * won't be cleaned up by this option. This functionality is best effort - if there
- * is an issue deleting the SharedPreferences file it will be silently ignored.
+ * Build and return the DataMigration instance.
*
- * This method is optional and defaults to true.
- *
- * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
- * @return this
+ * @return the DataMigration.
*/
- @Suppress("MissingGetterMatchingBuilder")
- public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
- RxSharedPreferencesMigrationBuilder<T> = apply {
- this.deleteEmptyPreference = deleteEmptyPreferences
- }
-
public fun build(): DataMigration<T> {
return SharedPreferencesMigration(
context = context,
@@ -120,10 +104,9 @@
rxSharedPreferencesMigration.migrate(spView, curData).await()
},
keysToMigrate = keysToMigrate,
- deleteEmptyPreferences = deleteEmptyPreference,
shouldRunMigration = { curData ->
rxSharedPreferencesMigration.shouldMigrate(curData).await()
}
)
}
-}
+}
\ No newline at end of file
diff --git a/datastore/datastore/api/current.txt b/datastore/datastore/api/current.txt
index dab188c..7497360 100644
--- a/datastore/datastore/api/current.txt
+++ b/datastore/datastore/api/current.txt
@@ -13,8 +13,10 @@
package androidx.datastore.migrations {
public final class SharedPreferencesMigration<T> implements androidx.datastore.core.DataMigration<T> {
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
diff --git a/datastore/datastore/api/public_plus_experimental_current.txt b/datastore/datastore/api/public_plus_experimental_current.txt
index dab188c..7497360 100644
--- a/datastore/datastore/api/public_plus_experimental_current.txt
+++ b/datastore/datastore/api/public_plus_experimental_current.txt
@@ -13,8 +13,10 @@
package androidx.datastore.migrations {
public final class SharedPreferencesMigration<T> implements androidx.datastore.core.DataMigration<T> {
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
diff --git a/datastore/datastore/api/restricted_current.txt b/datastore/datastore/api/restricted_current.txt
index dab188c..7497360 100644
--- a/datastore/datastore/api/restricted_current.txt
+++ b/datastore/datastore/api/restricted_current.txt
@@ -13,8 +13,10 @@
package androidx.datastore.migrations {
public final class SharedPreferencesMigration<T> implements androidx.datastore.core.DataMigration<T> {
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
- ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(kotlin.jvm.functions.Function0<? extends android.content.SharedPreferences> produceSharedPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+ ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
index 6ef13b2..37bd740 100644
--- a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
+++ b/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
@@ -136,6 +136,22 @@
assertThat(sharedPrefs.contains(key2)).isFalse()
}
+ @Test
+ fun producedSharedPreferencesIsUsed() = runBlockingTest {
+ assertThat(sharedPrefs.edit().putInt("integer_key", 123).commit()).isTrue()
+
+ val migration = SharedPreferencesMigration(
+ produceSharedPreferences = { sharedPrefs }
+ ) { prefs: SharedPreferencesView, _: Byte ->
+ assertThat(prefs.getAll().size).isEqualTo(1)
+ assertThat(prefs.getInt("integer_key", 0)).isEqualTo(123)
+ 123
+ }
+
+ val dataStore = getDataStoreWithMigrations(listOf(migration))
+ assertThat(dataStore.data.first()).isEqualTo(123)
+ }
+
private fun getDataStoreWithMigrations(
migrations: List<DataMigration<Byte>>
): DataStore<Byte> {
diff --git a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt b/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
index 5d9240d..2252461 100644
--- a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
+++ b/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
@@ -25,50 +25,109 @@
import kotlin.jvm.Throws
/**
- * DataMigration from SharedPreferences to DataStore.
- *
- * Example usage:
- *
- * val sharedPrefsMigration = SharedPreferencesMigration(
- * context,
- * mySharedPreferencesName
- * ) { prefs: SharedPreferencesView, myData: MyData ->
- * myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
- * }
- *
- * @param context Context used for getting SharedPreferences.
- * @param sharedPreferencesName The name of the SharedPreferences.
- * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore
- * .Preferences with their same values. If the key is already present in the new Preferences, the key
- * will not be migrated again. If the key is not present in the SharedPreferences it will not be
- * migrated. If keysToMigrate is null, all keys will be migrated from the existing
- * SharedPreferences.
- * @param deleteEmptyPreferences If enabled and the SharedPreferences are empty (i.e. no remaining
- * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
- * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
- * SharedPreferences to begin with then the (potentially) empty SharedPreferences won't be
- * cleaned up by this option. This functionality is best effort - if there is an issue deleting
- * the SharedPreferences file it will be silently ignored.
- * @param migrate maps SharedPreferences into T. Implementations should be idempotent
- * since this may be called multiple times. See [DataMigration.migrate] for more
- * information. The lambda accepts a SharedPreferencesView which is the view of the
- * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
- * the current data. The function must return the migrated data.
+ * DataMigration instance for migrating from SharedPreferences to DataStore.
*/
public class SharedPreferencesMigration<T>
-@JvmOverloads // Generate constructors for default params for java users.
-constructor(
- private val context: Context,
- private val sharedPreferencesName: String,
- keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
- private val deleteEmptyPreferences: Boolean = true,
+private constructor(
+ produceSharedPreferences: () -> SharedPreferences,
+ keysToMigrate: Set<String>?,
private val shouldRunMigration: suspend (T) -> Boolean = { true },
- private val migrate: suspend (SharedPreferencesView, T) -> T
+ private val migrate: suspend (SharedPreferencesView, T) -> T,
+ private val context: Context?,
+ private val name: String?
) : DataMigration<T> {
- private val sharedPrefs: SharedPreferences by lazy {
- context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE)
- }
+ /**
+ * DataMigration from SharedPreferences to DataStore.
+ *
+ * Note: This migration only supports the basic SharedPreferences types: boolean, float, int,
+ * long, string and string set. If the result of getAll contains other types, they will be
+ * ignored.
+ *
+ *
+ * Example usage:
+ * ```
+ * val sharedPrefsMigration = SharedPreferencesMigration(
+ * produceSharedPreferences = { EncryptedSharedPreferences.create(...) }
+ * ) { prefs: SharedPreferencesView, myData: MyData ->
+ * myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
+ * }
+ * ```
+ * @param produceSharedPreferences Should return the instance of SharedPreferences to migrate
+ * from.
+ * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore
+ * .Preferences with their same values. If the key is already present in the new Preferences,
+ * the key will not be migrated again. If the key is not present in the SharedPreferences it
+ * will not be migrated. If keysToMigrate is null, all keys will be migrated from the existing
+ * SharedPreferences.
+ * @param migrate maps SharedPreferences into T. Implementations should be idempotent
+ * since this may be called multiple times. See [DataMigration.migrate] for more
+ * information. The lambda accepts a SharedPreferencesView which is the view of the
+ * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+ * the current data. The function must return the migrated data.
+ */
+ @JvmOverloads // Generate constructors for default params for java users.
+ public constructor(
+ produceSharedPreferences: () -> SharedPreferences,
+ keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
+ shouldRunMigration: suspend (T) -> Boolean = { true },
+ migrate: suspend (SharedPreferencesView, T) -> T
+ ) : this(
+ produceSharedPreferences,
+ keysToMigrate,
+ shouldRunMigration,
+ migrate,
+ context = null,
+ name = null
+ )
+
+ /**
+ * DataMigration from SharedPreferences to DataStore.
+ *
+ * If the SharedPreferences is empty once the migration completes, this migration will
+ * attempt to delete it.
+ *
+ * Example usage:
+ *
+ * ```
+ * val sharedPrefsMigration = SharedPreferencesMigration(
+ * context,
+ * mySharedPreferencesName
+ * ) { prefs: SharedPreferencesView, myData: MyData ->
+ * myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
+ * }
+ * ```
+ *
+ * @param context Context used for getting SharedPreferences.
+ * @param sharedPreferencesName The name of the SharedPreferences.
+ * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore
+ * .Preferences with their same values. If the key is already present in the new Preferences,
+ * the key will not be migrated again. If the key is not present in the SharedPreferences it
+ * will not be migrated. If keysToMigrate is null, all keys will be migrated from the existing
+ * SharedPreferences.
+ * @param migrate maps SharedPreferences into T. Implementations should be idempotent
+ * since this may be called multiple times. See [DataMigration.migrate] for more
+ * information. The lambda accepts a SharedPreferencesView which is the view of the
+ * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+ * the current data. The function must return the migrated data.
+ */
+ @JvmOverloads // Generate constructors for default params for java users.
+ public constructor(
+ context: Context,
+ sharedPreferencesName: String,
+ keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
+ shouldRunMigration: suspend (T) -> Boolean = { true },
+ migrate: suspend (SharedPreferencesView, T) -> T
+ ) : this(
+ { context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE) },
+ keysToMigrate,
+ shouldRunMigration,
+ migrate,
+ context,
+ sharedPreferencesName
+ )
+
+ private val sharedPrefs: SharedPreferences by lazy(produceSharedPreferences)
private val keySet: MutableSet<String> by lazy {
(keysToMigrate ?: sharedPrefs.all.keys).toMutableSet()
@@ -100,13 +159,11 @@
}
if (!sharedPrefsEditor.commit()) {
- throw IOException(
- "Unable to delete migrated keys from SharedPreferences: $sharedPreferencesName"
- )
+ throw IOException("Unable to delete migrated keys from SharedPreferences.")
}
- if (deleteEmptyPreferences && sharedPrefs.all.isEmpty()) {
- deleteSharedPreferences(context, sharedPreferencesName)
+ if (sharedPrefs.all.isEmpty() && context != null && name != null) {
+ deleteSharedPreferences(context, name)
}
keySet.clear()
@@ -226,7 +283,7 @@
}
}
- private fun checkKey(key: String): String? {
+ private fun checkKey(key: String): String {
check(key in keySet) { "Can't access key outside migration: $key" }
return key
}
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index 12395f5..c70a1b0 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -28,3 +28,8 @@
# Some messages that encode the number of a certain type of other error
[0-9]+ errors, [0-9]+ warnings \([0-9]+ warnings filtered by baseline lint\-baseline\.xml\)
[0-9]+ errors, [0-9]+ warnings \([0-9]+ warning filtered by baseline lint\-baseline\.xml\)
+# > Task :webkit:integration-tests:testapp:compileReleaseJavaWithJavac
+\[ant\:jacocoReport\] Note\: Some input files use or override a deprecated API\.
+\[ant\:jacocoReport\] Note\: Recompile with \-Xlint\:deprecation for details\.
+\[ant\:jacocoReport\] Note\: Some input files use unchecked or unsafe operations\.
+\[ant\:jacocoReport\] Note\: Recompile with \-Xlint\:unchecked for details\.
\ No newline at end of file
diff --git a/development/split_change_into_owners.sh b/development/split_change_into_owners.sh
new file mode 100755
index 0000000..9405140
--- /dev/null
+++ b/development/split_change_into_owners.sh
@@ -0,0 +1,31 @@
+set -e
+
+if [ ! -e .git ]; then
+ echo "This script must be run from the root of the git repository"
+ exit 1
+fi
+
+function usage() {
+ echo "Usage: split_change_into_owners.sh <commit message>"
+ echo
+ echo "Splits changes in the current repository based on OWNERS files"
+ exit 1
+}
+
+commitMessage="$1"
+if [ "$commitMessage" == "" ]; then
+ usage
+fi
+
+ownersFiles="$(find -name OWNERS)"
+ownedDirs="$(echo "$ownersFiles" | sed 's|/OWNERS||' | sort -r)"
+
+for d in $ownedDirs; do
+ git add "$d"
+ if git status | grep -i "changes to be committed" >/dev/null; then
+ echo making commit for "$d"
+ git commit -m "$commitMessage
+
+This change includes files under $d"
+ fi
+done
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 7075406..265b8c8 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -91,6 +91,7 @@
docs(project(":core:core-animation"))
docs(project(":core:core-animation-testing"))
docs(project(":core:core-appdigest"))
+ docs(project(":core:core-google-shortcuts"))
docs(project(":core:core-ktx"))
docs(project(":cursoradapter:cursoradapter"))
docs(project(":customview:customview"))
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
index 9b18a2d..c39e98f9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
@@ -210,6 +210,35 @@
assertThat(vm.cleared).isTrue()
}
}
+
+ @Test
+ fun testDefaultFactoryAfterReuse() {
+ with(ActivityScenario.launch(ViewModelActivity::class.java)) {
+ val fragment = withActivity {
+ Fragment().also {
+ supportFragmentManager.beginTransaction().add(it, "temp").commitNow()
+ }
+ }
+
+ val defaultFactory = fragment.defaultViewModelProviderFactory
+
+ onActivity { activity ->
+ activity.supportFragmentManager.beginTransaction().remove(fragment).commitNow()
+ }
+
+ // Now re-add the removed fragment
+ onActivity { activity ->
+ activity.supportFragmentManager.beginTransaction()
+ .add(fragment, "temp")
+ .commitNow()
+ }
+
+ val newDefaultFactory = fragment.defaultViewModelProviderFactory
+
+ // New Fragment should have a new default factory
+ assertThat(newDefaultFactory).isNotSameInstanceAs(defaultFactory)
+ }
+ }
}
private fun FragmentActivity.getFragment(tag: String) =
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index cb57854..3245b4c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -567,6 +567,9 @@
private void initLifecycle() {
mLifecycleRegistry = new LifecycleRegistry(this);
mSavedStateRegistryController = SavedStateRegistryController.create(this);
+ // The default factory depends on the SavedStateRegistry so it
+ // needs to be reset when the SavedStateRegistry is reset
+ mDefaultFactory = null;
}
/**
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
index 0afbeaf..318929d 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
@@ -102,8 +102,7 @@
fragment.mHost = mFragmentManager.getHost();
fragment.onInflate(mFragmentManager.getHost().getContext(), attrs,
fragment.mSavedFragmentState);
- fragmentStateManager = mFragmentManager.createOrGetFragmentStateManager(fragment);
- mFragmentManager.addFragment(fragment);
+ fragmentStateManager = mFragmentManager.addFragment(fragment);
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG, "Fragment " + fragment + " has been inflated via "
+ "the <fragment> tag: id=0x" + Integer.toHexString(id));
@@ -155,23 +154,20 @@
// Fragments added via the <fragment> tag cannot move above VIEW_CREATED
// during inflation. Instead, we'll wait for the view to be attached to
// window and its parent view and trigger moveToExpectedState() at that point.
- if (mFragmentManager.mCurState > fragment.mState) {
- fragment.mView.addOnAttachStateChangeListener(
- new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- fragmentStateManager.moveToExpectedState();
- SpecialEffectsController controller = SpecialEffectsController
- .getOrCreateController((ViewGroup) parent, mFragmentManager);
- controller.forceCompleteAllOperations();
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
+ fragment.mView.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ fragmentStateManager.moveToExpectedState();
+ SpecialEffectsController controller = SpecialEffectsController
+ .getOrCreateController((ViewGroup) parent, mFragmentManager);
+ controller.forceCompleteAllOperations();
}
- );
- }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) { }
+ }
+ );
return fragment.mView;
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 8dfc448..ee8da34 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1710,7 +1710,7 @@
return fragmentStateManager;
}
- void addFragment(@NonNull Fragment fragment) {
+ FragmentStateManager addFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "add: " + fragment);
FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
fragment.mFragmentManager = this;
@@ -1725,6 +1725,7 @@
mNeedMenuInvalidate = true;
}
}
+ return fragmentStateManager;
}
void removeFragment(@NonNull Fragment fragment) {
diff --git a/lifecycle/lifecycle-runtime-ktx/api/current.txt b/lifecycle/lifecycle-runtime-ktx/api/current.txt
index 9df7a7b..cd4e431 100644
--- a/lifecycle/lifecycle-runtime-ktx/api/current.txt
+++ b/lifecycle/lifecycle-runtime-ktx/api/current.txt
@@ -29,6 +29,11 @@
method public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
}
+ public final class RepeatOnLifecycleKt {
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
public final class ViewKt {
method public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
}
diff --git a/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt
index 9df7a7b..cd4e431 100644
--- a/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt
@@ -29,6 +29,11 @@
method public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
}
+ public final class RepeatOnLifecycleKt {
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
public final class ViewKt {
method public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
}
diff --git a/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt b/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt
index 6205755..07c02b7 100644
--- a/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt
@@ -29,6 +29,11 @@
method public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
}
+ public final class RepeatOnLifecycleKt {
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static kotlinx.coroutines.Job repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
public final class ViewKt {
method public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
}
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
new file mode 100644
index 0000000..2a8ae8e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2020 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.lifecycle
+
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
+import org.junit.Test
+
+@SmallTest
+class RepeatOnLifecycleTest {
+
+ private val expectations = Expectations()
+ private val owner = FakeLifecycleOwner()
+
+ @Test
+ fun testBlockRunsWhenCreatedStateIsReached() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.CREATED) {
+ expectations.expect(2)
+ }
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlockRunsWhenStartedStateIsReached() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ expectations.expect(2)
+ }
+ owner.setState(Lifecycle.State.STARTED)
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlockRunsWhenResumedStateIsReached() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ expectations.expect(3)
+ }
+ owner.setState(Lifecycle.State.STARTED)
+ expectations.expect(2)
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(4)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlocksRepeatsExecution() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ var restarted = false
+ expectations.expect(1)
+
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ if (!restarted) {
+ expectations.expect(2)
+ } else {
+ expectations.expect(5)
+ }
+ }
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.STARTED)
+ expectations.expect(4)
+
+ restarted = true
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(6)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlockIsCancelledWhenLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ try {
+ expectations.expect(2)
+ awaitCancellation()
+ } catch (e: CancellationException) {
+ expectations.expect(4)
+ }
+ }
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ yield() // let the cancellation code run before asserting it happened
+ expectations.expect(5)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlockRunsOnSubsequentLifecycleState() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ expectations.expect(2)
+ }
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testBlockDoesNotStartIfLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.DESTROYED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ expectations.expectUnreached()
+ }
+ expectations.expect(2)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testCancellingTheReturnedJobCancelsTheBlock() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(1)
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ try {
+ expectations.expect(2)
+ awaitCancellation()
+ } catch (e: CancellationException) {
+ expectations.expect(4)
+ }
+ }
+ expectations.expect(3)
+ repeatingWorkJob.cancel()
+ yield() // let the cancellation code run before asserting it happened
+ expectations.expect(5)
+ assertThat(repeatingWorkJob.isCancelled).isTrue()
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testCancellingACustomJobCancelsTheBlock() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(1)
+ val customJob = Job()
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.STARTED, customJob) {
+ try {
+ expectations.expect(2)
+ awaitCancellation()
+ } catch (e: CancellationException) {
+ expectations.expect(4)
+ }
+ }
+ expectations.expect(3)
+ customJob.cancel()
+ yield() // let the cancellation code run before asserting it happened
+ expectations.expect(5)
+ assertThat(customJob.isCancelled).isTrue()
+ assertThat(customJob.isCompleted).isTrue()
+ assertThat(repeatingWorkJob.isCancelled).isTrue()
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ }
+
+ @Test
+ fun testCancellingTheJobDoesNotRestartTheBlockOnNewStates() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ expectations.expect(1)
+
+ var restarted = false
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ if (!restarted) {
+ expectations.expect(2)
+ } else {
+ expectations.expectUnreached()
+ }
+ }
+ expectations.expect(3)
+ repeatingWorkJob.cancel()
+ assertThat(repeatingWorkJob.isCancelled).isTrue()
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+
+ owner.setState(Lifecycle.State.STARTED)
+ restarted = true
+ expectations.expect(4)
+ owner.setState(Lifecycle.State.RESUMED)
+ yield() // Block shouldn't restart
+ expectations.expect(5)
+ owner.setState(Lifecycle.State.DESTROYED)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class)
+ @Test
+ fun testBlockRunsWhenUsingCustomDispatchers() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ expectations.expect(1)
+
+ val testDispatcher = TestCoroutineDispatcher()
+ val repeatingWorkJob = owner.repeatOnLifecycle(Lifecycle.State.CREATED, testDispatcher) {
+ // testDispatcher is ignored. This still runs on Dispatchers.Main
+ assertThat(coroutineContext[CoroutineDispatcher]).isEqualTo(Dispatchers.Main)
+ expectations.expect(2)
+ }
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ assertThat(repeatingWorkJob.isCompleted).isTrue()
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testBlockRunsWhenLogicUsesWithContext() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
+ expectations.expect(1)
+
+ val testDispatcher = TestCoroutineDispatcher().apply {
+ runBlockingTest {
+ owner.repeatOnLifecycle(Lifecycle.State.CREATED) {
+ withContext(this@apply) {
+ expectations.expect(2)
+ }
+ }
+ }
+ }
+
+ expectations.expect(3)
+ owner.setState(Lifecycle.State.DESTROYED)
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun testAddRepeatingWorkFailsWithInitializedState() = runBlocking(Dispatchers.Main) {
+ try {
+ owner.repeatOnLifecycle(Lifecycle.State.INITIALIZED) {
+ // IllegalArgumentException expected
+ }
+ } catch (e: Throwable) {
+ assertThat(e is IllegalArgumentException).isTrue()
+ }
+ Unit // runBlocking tries to return the result of the try expression, using Unit instead
+ }
+
+ @Test
+ fun testAddRepeatingWorkFailsWithDestroyedState() = runBlocking(Dispatchers.Main) {
+ try {
+ owner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
+ // IllegalArgumentException expected
+ }
+ } catch (e: Throwable) {
+ assertThat(e is IllegalArgumentException).isTrue()
+ }
+ Unit // runBlocking tries to return the result of the try expression, using Unit instead
+ }
+}
diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt
new file mode 100644
index 0000000..92e1234
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2021 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.lifecycle
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * Launches and runs the given [block] in a coroutine that executes the [block] on the
+ * main thread when this [LifecycleOwner]'s [Lifecycle] is at least at [state].
+ * The launched coroutine will be cancelled when the lifecycle state falls below [state].
+ *
+ * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state.
+ * To permanently remove the work from the lifecycle, [Job.cancel] the returned [Job].
+ *
+ * [Lifecycle] is bound to the Android **main thread** and the lifecycle state is only
+ * guaranteed to be valid and consistent on that main thread. [block] always runs on
+ * [Dispatchers.Main] and launches when the lifecycle [state] is first reached.
+ * **This overrides any [CoroutineDispatcher] specified in [coroutineContext].**
+ *
+ * An active coroutine is a child [Job] of any job present in [coroutineContext]. This returned
+ * [Job] is the parent of any currently running [block] and will **complete** when the [Lifecycle]
+ * is [destroyed][Lifecycle.Event.ON_DESTROY]. To perform an action when a [RepeatingWorkObserver]
+ * is completed or cancelled, see [Job.join] or [Job.invokeOnCompletion].
+ *
+ * ```
+ * // Runs the block of code in a coroutine when the lifecycleOwner is at least STARTED.
+ * // The coroutine will be cancelled when the ON_STOP event happens and will restart executing
+ * // if the lifecycleOwner's lifecycle receives the ON_START event again.
+ * lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ * uiStateFlow.collect { uiState ->
+ * updateUi(uiState)
+ * }
+ * }
+ * ```
+ *
+ * The best practice is to call this function when the lifecycleOwner is initialized. For
+ * example, `onCreate` in an Activity, or `onViewCreated` in a Fragment. Otherwise, multiple
+ * repeating jobs doing the same could be registered and be executed at the same time.
+ *
+ * Warning: [Lifecycle.State.DESTROYED] and [Lifecycle.State.INITIALIZED] are not allowed in this
+ * API. Passing those as a parameter will throw an [IllegalArgumentException].
+ *
+ * @param state [Lifecycle.State] in which the coroutine starts. That coroutine will cancel
+ * if the lifecycle falls below that state, and will restart if it's in that state again.
+ * @param coroutineContext [CoroutineContext] used to execute [block]. Note that its
+ * [CoroutineDispatcher] will be replaced by [Dispatchers.Main].
+ * @param block The block to run when the lifecycle is at least in [state] state.
+ * @return [Job] to manage the repeating work.
+ */
+public fun LifecycleOwner.repeatOnLifecycle(
+ state: Lifecycle.State,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit
+): Job = lifecycle.repeatOnLifecycle(state, coroutineContext, block)
+
+/**
+ * Launches and runs the given [block] in a coroutine that executes the [block] on the
+ * main thread when this [Lifecycle] is at least at [state].
+ * The launched coroutine will be cancelled when the lifecycle state falls below [state].
+ *
+ * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state.
+ * To permanently remove the work from the lifecycle, [Job.cancel] the returned [Job].
+ *
+ * [Lifecycle] is bound to the Android **main thread** and the lifecycle state is only
+ * guaranteed to be valid and consistent on that main thread. [block] always runs on
+ * [Dispatchers.Main] and launches when the lifecycle [state] is first reached.
+ * **This overrides any [CoroutineDispatcher] specified in [coroutineContext].**
+ *
+ * An active coroutine is a child [Job] of any job present in [coroutineContext]. This returned
+ * [Job] is the parent of any currently running [block] and will **complete** when the [Lifecycle]
+ * is [destroyed][Lifecycle.Event.ON_DESTROY]. To perform an action when a [RepeatingWorkObserver]
+ * is completed or cancelled, see [Job.join] or [Job.invokeOnCompletion].
+ *
+ * ```
+ * class MyActivity : AppCompatActivity() {
+ * override fun onCreate(savedInstanceState: Bundle?) {
+ * /* ... */
+ * // Runs the block of code in a coroutine when the lifecycle is at least STARTED.
+ * // The coroutine will be cancelled when the ON_STOP event happens and will restart
+ * // executing if the lifecycle receives the ON_START event again.
+ * lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ * uiStateFlow.collect { uiState ->
+ * updateUi(uiState)
+ * }
+ * }
+ * }
+ * }
+ * ```
+ *
+ * The best practice is to call this function in the lifecycle callback when the View gets
+ * created. For example, `onCreate` in an Activity, or `onViewCreated` in a Fragment. Otherwise,
+ * multiple repeating jobs doing the same could be registered and be executed at the same time.
+ *
+ * Warning: [Lifecycle.State.DESTROYED] and [Lifecycle.State.INITIALIZED] are not allowed in this
+ * API. Passing those as a parameter will throw an [IllegalArgumentException].
+ *
+ * @param state [Lifecycle.State] in which the coroutine starts. That coroutine will cancel
+ * if the lifecycle falls below that state, and will restart if it's in that state again.
+ * @param coroutineContext [CoroutineContext] used to execute [block]. Note that its
+ * [CoroutineDispatcher] will be replaced by [Dispatchers.Main].
+ * @param block The block to run when the lifecycle is at least in [state].
+ * @return [Job] to manage the repeating work.
+ */
+public fun Lifecycle.repeatOnLifecycle(
+ state: Lifecycle.State,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit
+): Job {
+ if (currentState === Lifecycle.State.DESTROYED) {
+ // Fast-path! As the work would immediately complete, return a completed Job
+ // to avoid extra allocations and adding/removing observers
+ return Job().apply { complete() }
+ }
+
+ return RepeatingWorkObserver(this, state, coroutineContext + Dispatchers.Main, block)
+ .also { observer ->
+ // Immediately add the LifecycleEventObserver to ensure that RepeatingWorkObserver Job's
+ // `invokeOnCompletion` that removes the observer doesn't happen before this in the case
+ // of a parent job cancelled early or an ON_DESTROY completing the observer
+ observer.launch(Dispatchers.Main.immediate) {
+ addObserver(observer)
+ }
+ }.job
+}
+
+/**
+ * [LifecycleEventObserver] that executes work periodically when the Lifecycle reaches
+ * certain State.
+ */
+private class RepeatingWorkObserver(
+ private val lifecycle: Lifecycle,
+ state: Lifecycle.State,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ private val block: suspend CoroutineScope.() -> Unit,
+) : LifecycleEventObserver, CoroutineScope {
+
+ init {
+ if (state === Lifecycle.State.INITIALIZED || state === Lifecycle.State.DESTROYED) {
+ throw IllegalArgumentException(
+ "RepeatingWorkObserver cannot start work with lifecycle states that are " +
+ "at least INITIALIZED or DESTROYED."
+ )
+ }
+ }
+
+ private val startWorkEvent = Lifecycle.Event.upTo(state)
+ private val cancelWorkEvent = Lifecycle.Event.downFrom(state)
+
+ // Exposing this job to enable cancelling RepeatingWorkObserver from the outside
+ val job = Job(coroutineContext[Job]).apply { invokeOnCompletion { removeSelf() } }
+ override val coroutineContext = coroutineContext + job
+
+ private var launchedJob: Job? = null
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == startWorkEvent) {
+ launchedJob = launch(start = CoroutineStart.UNDISPATCHED, block = block)
+ return
+ }
+ if (event == cancelWorkEvent) {
+ launchedJob?.cancel()
+ launchedJob = null
+ }
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ job.complete()
+ }
+ }
+
+ private fun removeSelf() {
+ if (Dispatchers.Main.immediate.isDispatchNeeded(EmptyCoroutineContext)) {
+ GlobalScope.launch(Dispatchers.Main.immediate) {
+ lifecycle.removeObserver(this@RepeatingWorkObserver)
+ }
+ } else lifecycle.removeObserver(this@RepeatingWorkObserver)
+ }
+}
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
index dcc9d01..4e1e00a 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
@@ -72,7 +72,9 @@
File(projectRoot(), "$NAV_RESOURCES/$name")
internal fun gradleBuilder(vararg args: String) = GradleRunner.create()
- .withProjectDir(projectRoot()).withPluginClasspath().withArguments(*args)
+ .withProjectDir(projectRoot()).withPluginClasspath()
+ // b/175897186 set explicit metaspace size in hopes of fewer crashes
+ .withArguments("-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m", *args)
internal fun runGradle(vararg args: String) = gradleBuilder(*args).build()
internal fun runAndFailGradle(vararg args: String) = gradleBuilder(*args).buildAndFail()
@@ -147,4 +149,4 @@
internal fun BuildResult.assertFailingTask(name: String): BuildResult {
assertThat(task(":$name")!!.outcome, CoreMatchers.`is`(TaskOutcome.FAILED))
return this
-}
\ No newline at end of file
+}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PositionalDataSource.kt b/paging/common/src/main/kotlin/androidx/paging/PositionalDataSource.kt
index 2804dcd..f8ee981 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PositionalDataSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PositionalDataSource.kt
@@ -87,7 +87,19 @@
*/
@JvmField
val placeholdersEnabled: Boolean
- )
+ ) {
+ init {
+ check(requestedStartPosition >= 0) {
+ "invalid start position: $requestedStartPosition"
+ }
+ check(requestedLoadSize >= 0) {
+ "invalid load size: $requestedLoadSize"
+ }
+ check(pageSize >= 0) {
+ "invalid page size: $pageSize"
+ }
+ }
+ }
/**
* Holder object for inputs to [loadRange].
@@ -315,7 +327,7 @@
initialPosition = maxOf(0, idealStart / params.pageSize * params.pageSize)
} else {
// not tiled, so don't try to snap or force multiple of a page size
- initialPosition -= initialLoadSize / 2
+ initialPosition = maxOf(0, initialPosition - initialLoadSize / 2)
}
}
val initParams = LoadInitialParams(
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
index e6c02d2..7fd6143 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
@@ -16,9 +16,14 @@
package androidx.paging
+import android.view.View
+import android.view.ViewGroup
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -28,6 +33,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
+@Suppress("DEPRECATION")
class LivePagedListTest {
@JvmField
@Rule
@@ -35,7 +41,6 @@
@Test
fun toLiveData_dataSourceConfig() {
- @Suppress("DEPRECATION")
val livePagedList = dataSourceFactory.toLiveData(config)
livePagedList.observeForever {}
assertNotNull(livePagedList.value)
@@ -44,7 +49,6 @@
@Test
fun toLiveData_dataSourcePageSize() {
- @Suppress("DEPRECATION")
val livePagedList = dataSourceFactory.toLiveData(24)
livePagedList.observeForever {}
assertNotNull(livePagedList.value)
@@ -53,7 +57,6 @@
@Test
fun toLiveData_pagingSourceConfig() {
- @Suppress("DEPRECATION")
val livePagedList = pagingSourceFactory.toLiveData(config)
livePagedList.observeForever {}
assertNotNull(livePagedList.value)
@@ -62,13 +65,71 @@
@Test
fun toLiveData_pagingSourcePageSize() {
- @Suppress("DEPRECATION")
val livePagedList = pagingSourceFactory.toLiveData(24)
livePagedList.observeForever {}
assertNotNull(livePagedList.value)
assertEquals(24, livePagedList.value!!.config.pageSize)
}
+ /**
+ * Some paging2 tests might be using InstantTaskExecutor and expect first page to be loaded
+ * immediately. This test replicates that by checking observe forever receives the value in
+ * its own call stack.
+ */
+ @Test
+ fun instantExecutionWorksWithLegacy() {
+ val totalSize = 300
+ val data = (0 until totalSize).map { "$it/$it" }
+ val factory = object : DataSource.Factory<Int, String>() {
+ override fun create(): DataSource<Int, String> {
+ return TestPositionalDataSource(data)
+ }
+ }
+
+ class TestAdapter : PagedListAdapter<String, RecyclerView.ViewHolder>(
+ DIFF_STRING
+ ) {
+ // open it up by overriding
+ public override fun getItem(position: Int): String? {
+ return super.getItem(position)
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): RecyclerView.ViewHolder {
+ return object : RecyclerView.ViewHolder(View(parent.context)) {}
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ }
+ }
+
+ val livePagedList = LivePagedListBuilder(
+ factory,
+ PagedList.Config.Builder()
+ .setEnablePlaceholders(false)
+ .setPageSize(30)
+ .build()
+ ).build()
+
+ val adapter = TestAdapter()
+ livePagedList.observeForever { pagedList ->
+ // make sure observeForever worked sync where it did load the data immediately
+ assertThat(
+ Throwable().stackTraceToString()
+ ).contains("observeForever")
+ assertThat(pagedList.loadedCount).isEqualTo(90)
+ }
+ adapter.submitList(checkNotNull(livePagedList.value))
+ assertThat(adapter.itemCount).isEqualTo(90)
+
+ (0 until totalSize).forEach {
+ // getting that item will trigger load around which should load the item immediately
+ assertThat(adapter.getItem(it)).isEqualTo("$it/$it")
+ }
+ }
+
companion object {
@Suppress("DEPRECATION")
private val dataSource = object : PositionalDataSource<String>() {
@@ -90,5 +151,14 @@
)
private val config = Config(10)
+ private val DIFF_STRING = object : DiffUtil.ItemCallback<String>() {
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+ }
}
}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
index 228fa79..7938e7c 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
@@ -2710,7 +2710,19 @@
TestLayoutManager layoutManager = new SimpleTestLayoutManager();
RecyclerView recyclerView = new RecyclerView(getActivity());
- recyclerView.setAdapter(new TestAdapter(1000));
+ // Setting height of RV to a fixed value so that it doesn't change based on the device
+ // that the test is running on.
+ recyclerView.setLayoutParams(
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 500)
+ );
+ recyclerView.setAdapter(
+ new TestAdapter(
+ 10,
+ new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 100)
+ )
+ );
recyclerView.setLayoutManager(layoutManager);
layoutManager.expectLayouts(1);
setRecyclerView(recyclerView);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 6244b7f..0e664f5 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -4630,7 +4630,7 @@
final int width = getWidth();
final int padding = mClipToPadding ? getPaddingTop() : 0;
c.rotate(90);
- c.translate(-padding, -width);
+ c.translate(padding, -width);
needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
c.restoreToCount(restore);
}
@@ -5618,7 +5618,7 @@
// Handle cases where parameter values aren't defined.
if (duration == UNDEFINED_DURATION) {
- duration = computeScrollDuration(dx, dy, 0, 0);
+ duration = computeScrollDuration(dx, dy);
}
if (interpolator == null) {
interpolator = sQuinticInterpolator;
@@ -5648,31 +5648,21 @@
postOnAnimation();
}
- private float distanceInfluenceForSnapDuration(float f) {
- f -= 0.5f; // center the values about 0.
- f *= 0.3f * (float) Math.PI / 2.0f;
- return (float) Math.sin(f);
- }
-
- private int computeScrollDuration(int dx, int dy, int vx, int vy) {
+ /**
+ * Computes of an animated scroll in milliseconds.
+ * @param dx x distance in pixels.
+ * @param dy y distance in pixels.
+ * @return The duration of the animated scroll in milliseconds.
+ */
+ private int computeScrollDuration(int dx, int dy) {
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final boolean horizontal = absDx > absDy;
- final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
- final int delta = (int) Math.sqrt(dx * dx + dy * dy);
final int containerSize = horizontal ? getWidth() : getHeight();
- final int halfContainerSize = containerSize / 2;
- final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
- final float distance = halfContainerSize + halfContainerSize
- * distanceInfluenceForSnapDuration(distanceRatio);
- final int duration;
- if (velocity > 0) {
- duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
- } else {
- float absDelta = (float) (horizontal ? absDx : absDy);
- duration = (int) (((absDelta / containerSize) + 1) * 300);
- }
+ float absDelta = (float) (horizontal ? absDx : absDy);
+ final int duration = (int) (((absDelta / containerSize) + 1) * 300);
+
return Math.min(duration, MAX_SCROLL_DURATION);
}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
index df23b64..82f5446 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
@@ -19,6 +19,7 @@
import com.tschuchort.compiletesting.KotlinCompilation
import java.io.File
import java.io.OutputStream
+import java.net.URLClassLoader
/**
* Helper class for Kotlin Compile Testing library to have common setup for room.
@@ -43,10 +44,100 @@
compilation.kotlincArguments += "-Xjava-source-roots=${javaSrcRoot.absolutePath}"
compilation.jvmDefault = "enable"
compilation.jvmTarget = "1.8"
- compilation.inheritClassPath = true
+ compilation.inheritClassPath = false
compilation.verbose = false
- compilation.classpaths += classpaths
+ compilation.classpaths = Classpaths.inheritedClasspath + classpaths
compilation.messageOutputStream = outputStream
+ compilation.kotlinStdLibJar = Classpaths.kotlinStdLibJar
+ compilation.kotlinStdLibCommonJar = Classpaths.kotlinStdLibCommonJar
+ compilation.kotlinStdLibJdkJar = Classpaths.kotlinStdLibJdkJar
+ compilation.kotlinReflectJar = Classpaths.kotlinReflectJar
+ compilation.kotlinScriptRuntimeJar = Classpaths.kotlinScriptRuntimeJar
return compilation
}
-}
\ No newline at end of file
+
+ /**
+ * Helper object to persist common classpaths resolved by KCT to make sure it does not
+ * re-resolve host classpath repeatedly and also runs compilation with a smaller classpath.
+ * see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/113
+ */
+ private object Classpaths {
+
+ val inheritedClasspath: List<File>
+
+ /**
+ * These jars are files that Kotlin Compile Testing discovers from classpath. It uses a
+ * rather expensive way of discovering these so we cache them here for now.
+ *
+ * We can remove this cache once we update to a version that includes the fix in KCT:
+ * https://github.com/tschuchortdev/kotlin-compile-testing/pull/114
+ */
+ val kotlinStdLibJar: File?
+ val kotlinStdLibCommonJar: File?
+ val kotlinStdLibJdkJar: File?
+ val kotlinReflectJar: File?
+ val kotlinScriptRuntimeJar: File?
+
+ init {
+ // create a KotlinCompilation to resolve common jars
+ val compilation = KotlinCompilation()
+ kotlinStdLibJar = compilation.kotlinStdLibJar
+ kotlinStdLibCommonJar = compilation.kotlinStdLibCommonJar
+ kotlinStdLibJdkJar = compilation.kotlinStdLibJdkJar
+ kotlinReflectJar = compilation.kotlinReflectJar
+ kotlinScriptRuntimeJar = compilation.kotlinScriptRuntimeJar
+
+ val hostClasspaths = getClasspathFromClassloader(
+ KotlinCompilationUtil::class.java.classLoader
+ )
+ inheritedClasspath = hostClasspaths.filter {
+ // size of this classpath has a rather significant impact on kotlin compilation
+ // tests hence limit it to things we need and no more.
+ it.path.contains("room") ||
+ it.path.contains("androidx") ||
+ it.path.contains("junit") ||
+ it.path.contains("android.jar")
+ }
+ }
+ }
+
+ // ported from https://github.com/google/compile-testing/blob/master/src/main/java/com
+ // /google/testing/compile/Compiler.java#L231
+ private fun getClasspathFromClassloader(referenceClassLoader: ClassLoader): List<File> {
+ val platformClassLoader: ClassLoader = ClassLoader.getPlatformClassLoader()
+ var currentClassloader = referenceClassLoader
+ val systemClassLoader = ClassLoader.getSystemClassLoader()
+
+ // Concatenate search paths from all classloaders in the hierarchy
+ // 'till the system classloader.
+ val classpaths: MutableSet<String> = LinkedHashSet()
+ while (true) {
+ if (currentClassloader === systemClassLoader) {
+ classpaths.addAll(
+ System.getProperty("java.class.path")
+ .split(System.getProperty("path.separator"))
+ )
+ break
+ }
+ if (currentClassloader === platformClassLoader) {
+ break
+ }
+ check(currentClassloader is URLClassLoader) {
+ """Classpath for compilation could not be extracted
+ since $currentClassloader is not an instance of URLClassloader
+ """.trimIndent()
+ }
+ // We only know how to extract classpaths from URLClassloaders.
+ currentClassloader.urLs.forEach { url ->
+ check(url.protocol == "file") {
+ """Given classloader consists of classpaths which are unsupported for
+ compilation.
+ """.trimIndent()
+ }
+ classpaths.add(url.path)
+ }
+ currentClassloader = currentClassloader.parent
+ }
+ return classpaths.map { File(it) }.filter { it.exists() }
+ }
+}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index cf24bd3..cb942f4 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -85,8 +85,8 @@
}
/**
- * Runs the compilation test with all 3 backends (javac, kapt, ksp) if possible (e.g. javac
- * cannot test kotlin sources).
+ * Runs the compilation test with ksp and one of javac or kapt, depending on whether input has
+ * kotlin sources.
*
* The [handler] will be invoked only for the first round. If you need to test multi round
* processing, use `handlers = listOf(..., ...)`.
@@ -113,14 +113,18 @@
classpath: List<File> = emptyList(),
handlers: List<(XTestInvocation) -> Unit>
) {
+ val javaApRunner = if (sources.any { it is Source.KotlinSource }) {
+ KaptCompilationTestRunner
+ } else {
+ JavacCompilationTestRunner
+ }
runTests(
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
handlers = handlers
),
- JavacCompilationTestRunner,
- KaptCompilationTestRunner,
+ javaApRunner,
KspCompilationTestRunner
)
}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
index 25faf77..a8b1502 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
@@ -52,8 +52,7 @@
private val postCompilationAssertions = mutableListOf<CompilationResultSubject.() -> Unit>()
- val isKsp: Boolean
- get() = processingEnv.backend == XProcessingEnv.Backend.KSP
+ val isKsp: Boolean = processingEnv.backend == XProcessingEnv.Backend.KSP
/**
* Registers a block that will be called with a [CompilationResultSubject] when compilation
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 4284a63..464de02 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -52,6 +52,9 @@
}
override val typeElement by lazy {
+ // for primitive types, we could technically return null from here as they are not backed
+ // by a type element in javac but in Kotlin we have types for them, hence returning them
+ // is better.
val declaration = ksType.declaration as? KSClassDeclaration
declaration?.let {
env.wrapClassDeclaration(it)
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 0132761..498357d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -123,7 +123,7 @@
val QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE = "Query/Insert method parameters cannot " +
"start with underscore (_)."
- fun cannotFindQueryResultAdapter(returnTypeName: String) = "Not sure how to convert a " +
+ fun cannotFindQueryResultAdapter(returnTypeName: TypeName) = "Not sure how to convert a " +
"Cursor to this method's return type ($returnTypeName)."
val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index 614badf..b495976 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -206,7 +206,7 @@
context.checker.check(
resultBinder.adapter != null,
executableElement,
- ProcessorErrors.cannotFindQueryResultAdapter(returnType.toString())
+ ProcessorErrors.cannotFindQueryResultAdapter(returnType.typeName)
)
val inTransaction = executableElement.hasAnnotation(Transaction::class)
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 067a9d2..fdce370 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -92,7 +92,7 @@
)
}
if (matchedFields.isEmpty()) {
- context.logger.e(ProcessorErrors.cannotFindQueryResultAdapter(out.toString()))
+ context.logger.e(ProcessorErrors.cannotFindQueryResultAdapter(out.typeName))
}
} else {
matchedFields = remainingFields.map { it }
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
index 4c6b0c2..44ad888 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -324,7 +324,7 @@
if (rowAdapter == null) {
context.logger.e(
relation.field.element,
- cannotFindQueryResultAdapter(relation.pojoType.toString())
+ cannotFindQueryResultAdapter(relation.pojoType.typeName)
)
null
} else {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
index d8b0dda3..a0b0f76 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
@@ -17,16 +17,17 @@
package androidx.room.processor
import androidx.room.TypeConverter
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.typeName
import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_UNBOUND_GENERIC
-import androidx.room.testing.TestInvocation
+import androidx.room.testing.context
import androidx.room.vo.CustomTypeConverter
import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
@@ -39,17 +40,15 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import simpleRun
import java.util.Date
import javax.lang.model.element.Modifier
-import javax.tools.JavaFileObject
@RunWith(JUnit4::class)
class CustomConverterProcessorTest {
companion object {
val CONVERTER = ClassName.get("foo.bar", "MyConverter")!!
val CONVERTER_QNAME = CONVERTER.packageName() + "." + CONVERTER.simpleName()
- val CONTAINER = JavaFileObjects.forSourceString(
+ val CONTAINER = Source.java(
"foo.bar.Container",
"""
package foo.bar;
@@ -65,7 +64,7 @@
singleClass(createConverter(TypeName.SHORT.box(), TypeName.CHAR.box())) { converter, _ ->
assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.compilesWithoutError()
+ }
}
@Test
@@ -73,7 +72,7 @@
singleClass(createConverter(TypeName.SHORT, TypeName.CHAR.box())) { converter, _ ->
assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.compilesWithoutError()
+ }
}
@Test
@@ -81,7 +80,7 @@
singleClass(createConverter(TypeName.INT.box(), TypeName.DOUBLE)) { converter, _ ->
assertThat(converter?.fromTypeName, `is`(TypeName.INT.box()))
assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
- }.compilesWithoutError()
+ }
}
@Test
@@ -89,7 +88,7 @@
singleClass(createConverter(TypeName.INT, TypeName.DOUBLE)) { converter, _ ->
assertThat(converter?.fromTypeName, `is`(TypeName.INT))
assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
- }.compilesWithoutError()
+ }
}
@Test
@@ -108,9 +107,11 @@
val list = ParameterizedTypeName.get(List::class.typeName, typeVarT)
val typeVarK = TypeVariableName.get("K")
val map = ParameterizedTypeName.get(Map::class.typeName, typeVarK, typeVarT)
- singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) {
- _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_UNBOUND_GENERIC)
+ singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(TYPE_CONVERTER_UNBOUND_GENERIC)
+ }
+ }
}
@Test
@@ -122,13 +123,13 @@
singleClass(createConverter(list, map)) { converter, _ ->
assertThat(converter?.fromTypeName, `is`(list as TypeName))
assertThat(converter?.toTypeName, `is`(map as TypeName))
- }.compilesWithoutError()
+ }
}
@Test
fun testNoConverters() {
singleClass(
- JavaFileObjects.forSourceString(
+ Source.java(
CONVERTER_QNAME,
"""
package ${CONVERTER.packageName()};
@@ -136,14 +137,17 @@
}
"""
)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_EMPTY_CLASS)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(TYPE_CONVERTER_EMPTY_CLASS)
+ }
+ }
}
@Test
fun checkNoArgConstructor() {
singleClass(
- JavaFileObjects.forSourceString(
+ Source.java(
CONVERTER_QNAME,
"""
package ${CONVERTER.packageName()};
@@ -156,14 +160,17 @@
}
"""
)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
+ }
+ }
}
@Test
fun checkNoArgConstructor_withStatic() {
singleClass(
- JavaFileObjects.forSourceString(
+ Source.java(
CONVERTER_QNAME,
"""
package ${CONVERTER.packageName()};
@@ -180,13 +187,13 @@
assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
assertThat(converter?.toTypeName, `is`(TypeName.INT))
assertThat(converter?.isStatic, `is`(true))
- }.compilesWithoutError()
+ }
}
@Test
fun checkPublic() {
singleClass(
- JavaFileObjects.forSourceString(
+ Source.java(
CONVERTER_QNAME,
"""
package ${CONVERTER.packageName()};
@@ -198,12 +205,15 @@
}
"""
)
- ) { converter, _ ->
+ ) { converter, invocation ->
assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
assertThat(converter?.toTypeName, `is`(TypeName.INT))
assertThat(converter?.isStatic, `is`(true))
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MUST_BE_PUBLIC).and()
- .withErrorCount(2)
+ invocation.assertCompilationResult {
+ hasErrorContaining(TYPE_CONVERTER_MUST_BE_PUBLIC)
+ hasErrorCount(2)
+ }
+ }
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -216,7 +226,7 @@
val baseConverter = createConverter(list, map, listOf(typeVarT, typeVarK))
val extendingQName = "foo.bar.Extending"
- val extendingClass = JavaFileObjects.forSourceString(
+ val extendingClass = Source.java(
extendingQName,
"package foo.bar;\n" +
TypeSpec.classBuilder(ClassName.bestGuess(extendingQName)).apply {
@@ -229,7 +239,9 @@
}.build().toString()
)
- simpleRun(baseConverter, extendingClass) { invocation ->
+ runProcessorTest(
+ sources = listOf(baseConverter, extendingClass)
+ ) { invocation ->
val element = invocation.processingEnv.requireTypeElement(extendingQName)
val converter = CustomConverterProcessor(invocation.context, element)
.process().firstOrNull()
@@ -250,22 +262,25 @@
) as TypeName
)
)
- }.compilesWithoutError()
+ }
}
@Test
fun checkDuplicates() {
singleClass(
createConverter(TypeName.SHORT.box(), TypeName.CHAR.box(), duplicate = true)
- ) { converter, _ ->
+ ) { converter, invocation ->
assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.failsToCompile().withErrorContaining("Multiple methods define the same conversion")
+ invocation.assertCompilationResult {
+ hasErrorContaining("Multiple methods define the same conversion")
+ }
+ }
}
@Test
fun invalidConverterType() {
- val source = JavaFileObjects.forSourceString(
+ val source = Source.java(
"foo.bar.Container",
"""
package foo.bar;
@@ -274,15 +289,22 @@
public class Container {}
"""
)
- simpleRun(source) { invocation ->
+ runProcessorTest(listOf(source)) { invocation ->
val result = CustomConverterProcessor.findConverters(
invocation.context,
invocation.processingEnv.requireTypeElement("foo.bar.Container")
)
Truth.assertThat(result.converters).isEmpty()
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.typeConverterMustBeDeclared(TypeName.INT)
- )
+ invocation.assertCompilationResult {
+ if (invocation.isKsp) {
+ // for KSP it always has a type element but we rather assert the other error
+ // instead of not running the code path in ksp tests
+ hasErrorContaining(TYPE_CONVERTER_EMPTY_CLASS)
+ } else {
+ hasErrorContaining(ProcessorErrors.typeConverterMustBeDeclared(TypeName.INT))
+ }
+ }
+ }
}
private fun createConverter(
@@ -290,7 +312,7 @@
to: TypeName,
typeVariables: List<TypeVariableName> = emptyList(),
duplicate: Boolean = false
- ): JavaFileObject {
+ ): Source {
val code = TypeSpec.classBuilder(CONVERTER).apply {
addTypeVariables(typeVariables)
addModifiers(Modifier.PUBLIC)
@@ -310,17 +332,19 @@
addMethod(buildMethod("convertF2"))
}
}.build().toString()
- return JavaFileObjects.forSourceString(
+ return Source.java(
CONVERTER.toString(),
"package ${CONVERTER.packageName()};\n$code"
)
}
private fun singleClass(
- vararg jfo: JavaFileObject,
- handler: (CustomTypeConverter?, TestInvocation) -> Unit
- ): CompileTester {
- return simpleRun(*((jfo.toList() + CONTAINER).toTypedArray())) { invocation ->
+ vararg sources: Source,
+ handler: (CustomTypeConverter?, XTestInvocation) -> Unit
+ ) {
+ runProcessorTest(
+ sources = sources.toList() + CONTAINER
+ ) { invocation ->
val processed = CustomConverterProcessor.findConverters(
invocation.context,
invocation.processingEnv.requireTypeElement("foo.bar.Container")
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index d951b1a..51ddc28 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -18,18 +18,18 @@
import COMMON
import androidx.room.compiler.processing.isTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.RoomTypeNames
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
import androidx.room.vo.Dao
import androidx.room.vo.ReadQueryMethod
import androidx.room.vo.Warning
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import compileLibrarySource
+import com.squareup.javapoet.TypeName
import createVerifierFromEntitiesAndViews
+import getSystemClasspathFiles
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
@@ -38,7 +38,7 @@
import java.io.File
@RunWith(Parameterized::class)
-class DaoProcessorTest(val enableVerification: Boolean) {
+class DaoProcessorTest(private val enableVerification: Boolean) {
companion object {
const val DAO_PREFIX = """
@@ -64,15 +64,22 @@
}
}
"""
- ) { _, _ ->
- }.compilesWithoutError()
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorCount(0)
+ }
+ }
}
@Test
fun testNonAbstract() {
- singleDao("@Dao public class MyDao {}") { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE)
+ singleDao("@Dao public class MyDao {}") { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE
+ )
+ }
+ }
}
@Test
@@ -83,30 +90,39 @@
int getFoo();
}
"""
- ) { _, _ ->
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ }
+ }
}
@Test
fun testAbstractMethodWithoutQueryInLibraryClass() {
- val libraryClasspath = compileLibrarySource(
+ val librarySource = Source.java(
"test.library.MissingAnnotationsBaseDao",
"""
+ package test.library;
public interface MissingAnnotationsBaseDao {
int getFoo();
}
"""
)
+ val libraryClasspath = compileFiles(
+ listOf(librarySource)
+ )
singleDao(
"@Dao public interface MyDao extends test.library.MissingAnnotationsBaseDao {}",
- classpathFiles = libraryClasspath
- ) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(
- ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD +
- " - test.library.MissingAnnotationsBaseDao.getFoo()"
- )
+ classpathFiles = listOf(libraryClasspath) + getSystemClasspathFiles()
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasRawOutputContaining(
+ ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD +
+ " - test.library.MissingAnnotationsBaseDao.getFoo()"
+ )
+ hasErrorContaining(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ }
+ }
}
@Test
@@ -119,10 +135,11 @@
int getFoo(int x);
}
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ }
+ }
}
@Test
@@ -138,7 +155,7 @@
assertThat(dao.queryMethods.size, `is`(1))
val method = dao.queryMethods.first()
assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
+ }
}
@Test
@@ -154,7 +171,7 @@
assertThat(dao.queryMethods.size, `is`(1))
val method = dao.queryMethods.first()
assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
+ }
}
@Test
@@ -175,7 +192,7 @@
assertThat(dao.insertionMethods.size, `is`(1))
val insertMethod = dao.insertionMethods.first()
assertThat(insertMethod.name, `is`("insert"))
- }.compilesWithoutError()
+ }
}
@Test
@@ -191,7 +208,7 @@
assertThat(dao.queryMethods.size, `is`(1))
val method = dao.queryMethods.first()
assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
+ }
}
@Test
@@ -227,7 +244,7 @@
`is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH))
)
}
- }.compilesWithoutError()
+ }
}
@Test
@@ -263,7 +280,7 @@
`is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH))
)
}
- }.compilesWithoutError()
+ }
}
@Test
@@ -283,14 +300,16 @@
abstract java.util.List<Merged> loadUsers();
}
"""
- ) { dao, _ ->
+ ) { dao, invocation ->
assertThat(dao.queryMethods.size, `is`(1))
assertThat(
dao.queryMethods.filterIsInstance<ReadQueryMethod>().first().inTransaction,
`is`(false)
)
- }.compilesWithoutError()
- .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ invocation.assertCompilationResult {
+ hasWarning(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ }
+ }
}
@Test
@@ -311,14 +330,16 @@
abstract java.util.List<Merged> loadUsers();
}
"""
- ) { dao, _ ->
+ ) { dao, invocation ->
assertThat(dao.queryMethods.size, `is`(1))
assertThat(
dao.queryMethods.filterIsInstance<ReadQueryMethod>().first().inTransaction,
`is`(false)
)
- }.compilesWithoutError()
- .withWarningCount(0)
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
}
@Test
@@ -339,15 +360,17 @@
abstract java.util.List<Merged> loadUsers();
}
"""
- ) { dao, _ ->
+ ) { dao, invocation ->
// test sanity
assertThat(dao.queryMethods.size, `is`(1))
assertThat(
dao.queryMethods.filterIsInstance<ReadQueryMethod>().first().inTransaction,
`is`(true)
)
- }.compilesWithoutError()
- .withWarningCount(0)
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
}
@Test
@@ -363,7 +386,7 @@
assertThat(dao.queryMethods.size, `is`(1))
val method = dao.queryMethods.first()
assertThat(method.name, `is`("deleteAllIds"))
- }.compilesWithoutError()
+ }
}
@Test
@@ -375,71 +398,53 @@
abstract void getAllIds();
}
"""
- ) { dao, _ ->
+ ) { dao, invocation ->
assertThat(dao.queryMethods.size, `is`(1))
val method = dao.queryMethods.first()
assertThat(method.name, `is`("getAllIds"))
- }.failsToCompile().withErrorContaining(
- "Not sure how to convert a Cursor to this method's return type (void)"
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.cannotFindQueryResultAdapter(TypeName.VOID)
+ )
+ }
+ }
}
- fun singleDao(
+ private fun singleDao(
vararg inputs: String,
- classpathFiles: Set<File> = emptySet(),
- handler: (Dao, TestInvocation) -> Unit
- ): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(
- listOf(
- JavaFileObjects.forSourceString(
- "foo.bar.MyDao",
- DAO_PREFIX + inputs.joinToString("\n")
- ),
- COMMON.USER
+ classpathFiles: List<File> = emptyList(),
+ handler: (Dao, XTestInvocation) -> Unit
+ ) {
+ runProcessorTest(
+ sources = listOf(
+ Source.java(
+ "foo.bar.MyDao",
+ DAO_PREFIX + inputs.joinToString("\n")
+ ),
+ Source.fromJavaFileObject(COMMON.USER)
+ ),
+ classpath = classpathFiles
+ ) { invocation: XTestInvocation ->
+ val dao = invocation.roundEnv
+ .getTypeElementsAnnotatedWith(
+ androidx.room.Dao::class.java
)
- )
- .apply {
- if (classpathFiles.isNotEmpty()) {
- withClasspath(classpathFiles)
- }
+ .first()
+ check(dao.isTypeElement())
+ val dbVerifier = if (enableVerification) {
+ createVerifierFromEntitiesAndViews(invocation)
+ } else {
+ null
}
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(
- java.lang.SuppressWarnings::class,
- androidx.room.Dao::class,
- androidx.room.Entity::class,
- androidx.room.Relation::class,
- androidx.room.Transaction::class,
- androidx.room.ColumnInfo::class,
- androidx.room.PrimaryKey::class,
- androidx.room.Query::class
- )
- .nextRunHandler { invocation ->
- val dao = invocation.roundEnv
- .getTypeElementsAnnotatedWith(
- androidx.room.Dao::class.java
- )
- .first()
- check(dao.isTypeElement())
- val dbVerifier = if (enableVerification) {
- createVerifierFromEntitiesAndViews(invocation)
- } else {
- null
- }
- val dbType = invocation.context.processingEnv
- .requireType(RoomTypeNames.ROOM_DB)
- val parser = DaoProcessor(
- invocation.context,
- dao, dbType, dbVerifier
- )
-
- val parsedDao = parser.process()
- handler(parsedDao, invocation)
- true
- }
- .build()
+ val dbType = invocation.context.processingEnv
+ .requireType(RoomTypeNames.ROOM_DB)
+ val parser = DaoProcessor(
+ invocation.context,
+ dao, dbType, dbVerifier
)
+
+ val parsedDao = parser.process()
+ handler(parsedDao, invocation)
+ }
}
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 801d997..94c0785 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -19,6 +19,7 @@
import COMMON
import androidx.room.Dao
import androidx.room.Query
+import androidx.room.compiler.processing.XType
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.LifecyclesTypeNames
@@ -26,7 +27,6 @@
import androidx.room.ext.typeName
import androidx.room.parser.QueryType
import androidx.room.parser.Table
-import androidx.room.compiler.processing.XType
import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
import androidx.room.solver.query.result.ListQueryResultAdapter
@@ -910,7 +910,11 @@
assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName")))
assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields as List<Field>))
}?.failsToCompile()
- ?.withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo"))
+ ?.withErrorContaining(
+ cannotFindQueryResultAdapter(
+ ClassName.get("foo.bar", "MyClass", "Pojo")
+ )
+ )
?.and()
?.withWarningContaining(
ProcessorErrors.cursorPojoMismatch(
@@ -942,7 +946,11 @@
) { _, _, _ ->
}?.failsToCompile()
?.withErrorContaining("no such column: age")
- ?.and()?.withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo"))
+ ?.and()?.withErrorContaining(
+ cannotFindQueryResultAdapter(
+ ClassName.get("foo.bar", "MyClass", "Pojo")
+ )
+ )
?.and()?.withErrorCount(2)
?.withWarningCount(0)
}
@@ -1105,7 +1113,11 @@
return assertion
} else {
assertion.failsToCompile()
- .withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo"))
+ .withErrorContaining(
+ cannotFindQueryResultAdapter(
+ ClassName.get("foo.bar", "MyClass", "Pojo")
+ )
+ )
return null
}
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
index 0a14017..e886664 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
@@ -22,22 +22,38 @@
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.junit.runners.Parameterized
-@RunWith(JUnit4::class)
-class InProcessorTest {
+@RunWith(Parameterized::class)
+class InProcessorTest(
+ private val kotlinCode: Boolean
+) {
@Test
fun testInProcessorTestRuns() {
- val source = Source.java(
- qName = "foo.bar.MyClass",
- code = """
+ val source = if (kotlinCode) {
+ Source.kotlin(
+ filePath = "MyClass.kt",
+ code = """
+ package foo.bar
+ abstract class MyClass {
+ @androidx.room.Query("foo")
+ abstract fun setFoo(foo: String):Unit
+ }
+ """.trimIndent()
+ )
+ } else {
+ Source.java(
+ qName = "foo.bar.MyClass",
+ code = """
package foo.bar;
abstract public class MyClass {
@androidx.room.Query("foo")
abstract public void setFoo(String foo);
}
- """.trimIndent()
- )
+ """.trimIndent()
+ )
+ }
+
var runCount = 0
runProcessorTest(sources = listOf(source)) {
assertThat(
@@ -45,15 +61,23 @@
).isNotNull()
runCount++
}
- // run 3 times: javac, kapt, ksp (if enabled)
+ // run 1 or 2 times
+ // +1 if KSP is enabled
+ // 1 for javac or kapt depending on whether source is in kotlin or java
assertThat(
runCount
).isEqualTo(
- 2 + if (CompilationTestCapabilities.canTestWithKsp) {
+ 1 + if (CompilationTestCapabilities.canTestWithKsp) {
1
} else {
0
}
)
}
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "kotlinCode_{0}")
+ fun params() = arrayOf(true, false)
+ }
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index 389eaf6..a801b40 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -337,7 +337,7 @@
return getSystemClasspathFiles() + lib
}
-private fun getSystemClasspathFiles(): Set<File> {
+fun getSystemClasspathFiles(): Set<File> {
val pathSeparator = System.getProperty("path.separator")!!
return System.getProperty("java.class.path")!!.split(pathSeparator).map { File(it) }.toSet()
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
index a61bb70..fc1c911 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -16,21 +16,17 @@
package androidx.room.writer
-import androidx.annotation.NonNull
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.processor.DatabaseProcessor
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
import androidx.room.vo.Database
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
@RunWith(JUnit4::class)
class SQLiteOpenHelperWriterTest {
@@ -43,7 +39,7 @@
private const val ENTITY_PREFIX = DATABASE_PREFIX + """
@Entity%s
public class MyEntity {
- """
+ """
private const val ENTITY_SUFFIX = "}"
}
@@ -68,7 +64,7 @@
" PRIMARY KEY(`uuid`))"
)
)
- }.compilesWithoutError()
+ }
}
@Test
@@ -93,7 +89,7 @@
"`age` INTEGER NOT NULL, PRIMARY KEY(`uuid`, `name`))"
)
)
- }.compilesWithoutError()
+ }
}
@Test
@@ -117,7 +113,7 @@
" `name` TEXT, `age` INTEGER NOT NULL)"
)
)
- }.compilesWithoutError()
+ }
}
}
@@ -142,7 +138,7 @@
" `name` TEXT, `age` INTEGER NOT NULL)"
)
)
- }.compilesWithoutError()
+ }
}
}
@@ -151,31 +147,31 @@
singleView("SELECT uuid, name FROM MyEntity") { database, _ ->
val query = SQLiteOpenHelperWriter(database).createViewQuery(database.views.first())
assertThat(query, `is`("CREATE VIEW `MyView` AS SELECT uuid, name FROM MyEntity"))
- }.compilesWithoutError()
+ }
}
private fun singleEntity(
input: String,
attributes: Map<String, String> = mapOf(),
- handler: (Database, TestInvocation) -> Unit
- ): CompileTester {
+ handler: (Database, XTestInvocation) -> Unit
+ ) {
val attributesReplacement = if (attributes.isEmpty()) {
""
} else {
"(" + attributes.entries.joinToString(",") { "${it.key} = ${it.value}" } + ")"
}
- val entity = JavaFileObjects.forSourceString(
+ val entity = Source.java(
"foo.bar.MyEntity",
ENTITY_PREFIX.format(attributesReplacement) + input + ENTITY_SUFFIX
)
- return verify(listOf(entity), "", handler)
+ verify(listOf(entity), "", handler)
}
private fun singleView(
query: String,
- handler: (Database, TestInvocation) -> Unit
- ): CompileTester {
- val entity = JavaFileObjects.forSourceString(
+ handler: (Database, XTestInvocation) -> Unit
+ ) {
+ val entity = Source.java(
"foo.bar.MyEntity",
ENTITY_PREFIX.format("") + """
@PrimaryKey
@@ -184,9 +180,9 @@
@NonNull
String name;
int age;
- """ + ENTITY_SUFFIX
+ """ + ENTITY_SUFFIX
)
- val view = JavaFileObjects.forSourceString(
+ val view = Source.java(
"foo.bar.MyView",
DATABASE_PREFIX + """
@DatabaseView("$query")
@@ -194,39 +190,33 @@
public String uuid;
public String name;
}
- """
+ """
)
return verify(listOf(entity, view), "views = {MyView.class},", handler)
}
private fun verify(
- jfos: List<JavaFileObject> = emptyList(),
+ sources: List<Source> = emptyList(),
databaseAttribute: String,
- handler: (Database, TestInvocation) -> Unit
- ): CompileTester {
- val databaseCode = """
+ handler: (Database, XTestInvocation) -> Unit
+ ) {
+ val databaseCode = Source.java(
+ "foo.bar.MyDatabase",
+ """
package foo.bar;
import androidx.room.*;
@Database(entities = {MyEntity.class}, $databaseAttribute version = 3)
abstract public class MyDatabase extends RoomDatabase {
}
- """
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfos + JavaFileObjects.forSourceString("foo.bar.MyDatabase", databaseCode))
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(
- androidx.room.Database::class,
- NonNull::class
- )
- .nextRunHandler { invocation ->
- val db = invocation.roundEnv
- .getTypeElementsAnnotatedWith(androidx.room.Database::class.java)
- .first()
- handler(DatabaseProcessor(invocation.context, db).process(), invocation)
- true
- }
- .build()
- )
+ """
+ )
+ runProcessorTest(
+ sources = sources + databaseCode
+ ) { invocation ->
+ val db = invocation.roundEnv
+ .getTypeElementsAnnotatedWith(androidx.room.Database::class.java)
+ .first()
+ handler(DatabaseProcessor(invocation.context, db).process(), invocation)
+ }
}
}
diff --git a/settings.gradle b/settings.gradle
index ba08f5b..a8b94f33 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -30,12 +30,28 @@
WEAR,
}
-private static Set<BuildType> createRequestedFilter() {
+private String getRequestedProjectSubsetName() {
+ if (startParameter.projectProperties.containsKey("androidx.projects")) {
+ return startParameter.projectProperties["androidx.projects"].toUpperCase()
+ }
+ if (System.getenv().containsKey("ANDROIDX_PROJECTS")) {
+ return System.getenv()["ANDROIDX_PROJECTS"].toUpperCase()
+ }
+ return null
+}
+
+boolean isAllProjects() {
+ String projectSubsetName = getRequestedProjectSubsetName()
+ return requestedProjectSubsetName == null || requestedProjectSubsetName == "ALL"
+}
+
+private Set<BuildType> createRequestedFilter() {
Set<BuildType> filter = new HashSet<>()
- if (!System.getenv().containsKey("ANDROIDX_PROJECTS")) return null
- String[] requestedFilter = System.getenv("ANDROIDX_PROJECTS").split(",")
+ String projectSubsetName = getRequestedProjectSubsetName()
+ if (projectSubsetName == null) return null
+ String[] requestedFilter = projectSubsetName.split(",")
for (String requestedType : requestedFilter) {
- switch (requestedType.toUpperCase()) {
+ switch (requestedType) {
case "MAIN":
filter.add(BuildType.MAIN)
break
@@ -74,7 +90,8 @@
* Note that null value means all the projects should be included
*/
@Field
-Set<BuildType> requestedFilter = createRequestedFilter()
+Set<BuildType> requestedFilter
+requestedFilter = createRequestedFilter()
boolean shouldIncludeForFilter(List<BuildType> includeList) {
if (includeList.empty) return true
@@ -178,6 +195,7 @@
includeProject(":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-dep", "buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep", [BuildType.MAIN])
includeProject(":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-main", "buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-main", [BuildType.MAIN])
}
+includeProject(":buildSrc-tests:project-subsets", "buildSrc-tests/project-subsets", [BuildType.MAIN])
includeProject(":camera:camera-camera2", "camera/camera-camera2", [BuildType.MAIN])
includeProject(":camera:camera-camera2-pipe", "camera/camera-camera2-pipe", [BuildType.MAIN])
includeProject(":camera:camera-camera2-pipe-integration", "camera/camera-camera2-pipe-integration", [BuildType.MAIN])
@@ -299,6 +317,7 @@
includeProject(":core:core-animation-integration-tests:testapp", "core/core-animation-integration-tests/testapp", [BuildType.MAIN])
includeProject(":core:core-animation-testing", "core/core-animation-testing", [BuildType.MAIN])
includeProject(":core:core-appdigest", "core/core-appdigest", [BuildType.MAIN])
+includeProject(":core:core-google-shortcuts", "core/core-google-shortcuts", [BuildType.MAIN])
includeProject(":core:core-ktx", "core/core-ktx", [BuildType.MAIN])
includeProject(":cursoradapter:cursoradapter", "cursoradapter/cursoradapter", [BuildType.MAIN])
includeProject(":customview:customview", "customview/customview", [BuildType.MAIN])
@@ -655,11 +674,6 @@
includeProject(":icing:nativeLib", new File(externalRoot, "icing/nativeLib"), [BuildType.MAIN])
includeProject(":noto-emoji-compat", new File(externalRoot, "noto-fonts/emoji-compat"), [BuildType.MAIN])
-static boolean isAllProjects() {
- if (!System.getenv().containsKey("ANDROIDX_PROJECTS")) return true
- return System.getenv("ANDROIDX_PROJECTS") == "ALL"
-}
-
if (isAllProjects()) {
includeProject(":docs-tip-of-tree", "docs-tip-of-tree")
includeProject(":docs-public", "docs-public")
diff --git a/slidingpanelayout/slidingpanelayout/api/current.txt b/slidingpanelayout/slidingpanelayout/api/current.txt
index be84592..75407f9 100644
--- a/slidingpanelayout/slidingpanelayout/api/current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/current.txt
@@ -9,15 +9,15 @@
method @Deprecated public boolean canSlide();
method public void close();
method public boolean closePane();
- method @ColorInt public int getCoveredFadeColor();
+ method @Deprecated @ColorInt public int getCoveredFadeColor();
method public final int getLockMode();
method @Px public int getParallaxDistance();
- method @ColorInt public int getSliderFadeColor();
+ method @Deprecated @ColorInt public int getSliderFadeColor();
method public boolean isOpen();
method public boolean isSlideable();
method public void open();
method public boolean openPane();
- method public void setCoveredFadeColor(@ColorInt int);
+ method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
method public void setParallaxDistance(@Px int);
@@ -27,7 +27,7 @@
method @Deprecated public void setShadowResource(@DrawableRes int);
method public void setShadowResourceLeft(int);
method public void setShadowResourceRight(int);
- method public void setSliderFadeColor(@ColorInt int);
+ method @Deprecated public void setSliderFadeColor(@ColorInt int);
method @Deprecated public void smoothSlideClosed();
method @Deprecated public void smoothSlideOpen();
field public static final int LOCK_MODE_LOCKED_CLOSED = 2; // 0x2
diff --git a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
index be84592..75407f9 100644
--- a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
@@ -9,15 +9,15 @@
method @Deprecated public boolean canSlide();
method public void close();
method public boolean closePane();
- method @ColorInt public int getCoveredFadeColor();
+ method @Deprecated @ColorInt public int getCoveredFadeColor();
method public final int getLockMode();
method @Px public int getParallaxDistance();
- method @ColorInt public int getSliderFadeColor();
+ method @Deprecated @ColorInt public int getSliderFadeColor();
method public boolean isOpen();
method public boolean isSlideable();
method public void open();
method public boolean openPane();
- method public void setCoveredFadeColor(@ColorInt int);
+ method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
method public void setParallaxDistance(@Px int);
@@ -27,7 +27,7 @@
method @Deprecated public void setShadowResource(@DrawableRes int);
method public void setShadowResourceLeft(int);
method public void setShadowResourceRight(int);
- method public void setSliderFadeColor(@ColorInt int);
+ method @Deprecated public void setSliderFadeColor(@ColorInt int);
method @Deprecated public void smoothSlideClosed();
method @Deprecated public void smoothSlideOpen();
field public static final int LOCK_MODE_LOCKED_CLOSED = 2; // 0x2
diff --git a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
index be84592..75407f9 100644
--- a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
@@ -9,15 +9,15 @@
method @Deprecated public boolean canSlide();
method public void close();
method public boolean closePane();
- method @ColorInt public int getCoveredFadeColor();
+ method @Deprecated @ColorInt public int getCoveredFadeColor();
method public final int getLockMode();
method @Px public int getParallaxDistance();
- method @ColorInt public int getSliderFadeColor();
+ method @Deprecated @ColorInt public int getSliderFadeColor();
method public boolean isOpen();
method public boolean isSlideable();
method public void open();
method public boolean openPane();
- method public void setCoveredFadeColor(@ColorInt int);
+ method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
method public void setParallaxDistance(@Px int);
@@ -27,7 +27,7 @@
method @Deprecated public void setShadowResource(@DrawableRes int);
method public void setShadowResourceLeft(int);
method public void setShadowResourceRight(int);
- method public void setSliderFadeColor(@ColorInt int);
+ method @Deprecated public void setSliderFadeColor(@ColorInt int);
method @Deprecated public void smoothSlideClosed();
method @Deprecated public void smoothSlideOpen();
field public static final int LOCK_MODE_LOCKED_CLOSED = 2; // 0x2
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 8076cd9..9e2f06d 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -21,7 +21,6 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -57,14 +56,13 @@
/**
* SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level
- * of a UI. A left (or first) pane is treated as a content list or browser, subordinate to a
+ * of a UI. A left (or start) pane is treated as a content list or browser, subordinate to a
* primary detail view for displaying content.
*
- * <p>Child views may overlap if their combined width exceeds the available width
- * in the SlidingPaneLayout. When this occurs the user may slide the topmost view out of the way
- * by dragging it, or by navigating in the direction of the overlapped view using a keyboard.
- * If the content of the dragged child view is itself horizontally scrollable, the user may
- * grab it by the very edge.</p>
+ * <p>Child views overlap if their combined width exceeds the available width
+ * in the SlidingPaneLayout. Each of child views is expand out to fill the available width in
+ * the SlidingPaneLayout. When this occurs, the user may slide the topmost view out of the way
+ * by dragging it, and dragging back it from the very edge.</p>
*
* <p>Thanks to this sliding behavior, SlidingPaneLayout may be suitable for creating layouts
* that can smoothly adapt across many different screen sizes, expanding out fully on larger
@@ -82,40 +80,18 @@
* displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include
* switching between disparate functions of your app, such as jumping from a social stream view
* to a view of your personal profile - cases such as this should use the navigation drawer
- * pattern instead. ({@link androidx.drawerlayout.widget.DrawerLayout DrawerLayout} implements this pattern.)</p>
+ * pattern instead. ({@link androidx.drawerlayout.widget.DrawerLayout DrawerLayout} implements
+ * this pattern.)</p>
*
* <p>Like {@link android.widget.LinearLayout LinearLayout}, SlidingPaneLayout supports
* the use of the layout parameter <code>layout_weight</code> on child views to determine
* how to divide leftover space after measurement is complete. It is only relevant for width.
* When views do not overlap weight behaves as it does in a LinearLayout.</p>
- *
- * <p>When views do overlap, weight on a slideable pane indicates that the pane should be
- * sized to fill all available space in the closed state. Weight on a pane that becomes covered
- * indicates that the pane should be sized to fill all available space except a small minimum strip
- * that the user may use to grab the slideable view and pull it back over into a closed state.</p>
*/
public class SlidingPaneLayout extends ViewGroup implements Openable {
private static final String TAG = "SlidingPaneLayout";
/**
- * Default size of the overhang for a pane in the open state.
- * At least this much of a sliding pane will remain visible.
- * This indicates that there is more content available and provides
- * a "physical" edge to grab to pull it closed.
- */
- private static final int DEFAULT_OVERHANG_SIZE = 32; // dp;
-
- /**
- * If no fade color is given by default it will fade to 80% gray.
- */
- private static final int DEFAULT_FADE_COLOR = 0xcccccccc;
-
- /**
- * The fade color used for the sliding panel. 0 = no fading.
- */
- private int mSliderFadeColor = DEFAULT_FADE_COLOR;
-
- /**
* Minimum velocity that will be detected as a fling
*/
private static final int MIN_FLING_VELOCITY = 400; // dips per second
@@ -125,11 +101,6 @@
"androidx.slidingpanelayout.widget.SlidingPaneLayout";
/**
- * The fade color used for the panel covered by the slider. 0 = no fading.
- */
- private int mCoveredFadeColor;
-
- /**
* Drawable used to draw the shadow between panes by default.
*/
private Drawable mShadowDrawableLeft;
@@ -140,13 +111,6 @@
private Drawable mShadowDrawableRight;
/**
- * The size of the overhang in pixels.
- * This is the minimum section of the sliding panel that will
- * be visible in the open state to allow for a closing drag.
- */
- private final int mOverhangSize;
-
- /**
* True if a panel can slide with the current measurements
*/
private boolean mCanSlide;
@@ -259,13 +223,16 @@
public interface PanelSlideListener {
/**
* Called when a sliding pane's position changes.
- * @param panel The child view that was moved
+ *
+ * @param panel The child view that was moved
* @param slideOffset The new offset of this sliding pane within its range, from 0-1
*/
void onPanelSlide(@NonNull View panel, float slideOffset);
+
/**
* Called when a sliding pane becomes slid completely open. The pane may or may not
* be interactive at this point depending on how much of the pane is visible.
+ *
* @param panel The child view that was slid to an open position, revealing other panes
*/
void onPanelOpened(@NonNull View panel);
@@ -273,6 +240,7 @@
/**
* Called when a sliding pane becomes slid completely closed. The pane is now guaranteed
* to be interactive. It may now obscure other views in the layout.
+ *
* @param panel The child view that was slid to a closed position
*/
void onPanelClosed(@NonNull View panel);
@@ -286,9 +254,11 @@
@Override
public void onPanelSlide(View panel, float slideOffset) {
}
+
@Override
public void onPanelOpened(View panel) {
}
+
@Override
public void onPanelClosed(View panel) {
}
@@ -306,7 +276,6 @@
super(context, attrs, defStyle);
final float density = context.getResources().getDisplayMetrics().density;
- mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f);
setWillNotDraw(false);
@@ -331,7 +300,6 @@
/**
* @return The distance the lower pane will parallax by when the upper pane is fully closed.
- *
* @see #setParallaxDistance(int)
*/
@Px
@@ -343,17 +311,21 @@
* Set the color used to fade the sliding pane out when it is slid most of the way offscreen.
*
* @param color An ARGB-packed color value
+ * @deprecated SlidingPaneLayout no longer uses this field.
*/
+ @Deprecated
public void setSliderFadeColor(@ColorInt int color) {
- mSliderFadeColor = color;
}
/**
* @return The ARGB-packed color value used to fade the sliding pane
+ *
+ * @deprecated This field is no longer populated by SlidingPaneLayout.
*/
+ @Deprecated
@ColorInt
public int getSliderFadeColor() {
- return mSliderFadeColor;
+ return 0;
}
/**
@@ -361,17 +333,21 @@
* will become fully covered in the closed state.
*
* @param color An ARGB-packed color value
+ * @deprecated SlidingPaneLayout no longer uses this field.
*/
+ @Deprecated
public void setCoveredFadeColor(@ColorInt int color) {
- mCoveredFadeColor = color;
}
/**
* @return The ARGB-packed color value used to fade the fixed pane
+ *
+ * @deprecated This field is no longer populated by SlidingPaneLayout
*/
+ @Deprecated
@ColorInt
public int getCoveredFadeColor() {
- return mCoveredFadeColor;
+ return 0;
}
public void setPanelSlideListener(@Nullable PanelSlideListener listener) {
@@ -601,93 +577,49 @@
}
widthRemaining -= childWidth;
+ // Skip first child (list pane), the list pane is always a non-sliding pane.
+ if (i == 0) {
+ continue;
+ }
canSlide |= lp.slideable = widthRemaining < 0;
if (lp.slideable) {
mSlideableView = child;
}
}
-
- // Resolve weight and make sure non-sliding panels are smaller than the full screen.
+ // Second pass. Resolve weight.
+ // Child views overlap when the combined width of child views cannot fit into the
+ // available width. Each of child views is sized to fill all available space. If there is
+ // no overlap, distribute the extra width proportionally to weight.
if (canSlide || weightSum > 0) {
- final int fixedPanelWidthLimit = widthAvailable - mOverhangSize;
-
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
-
if (child.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
- if (child.getVisibility() == GONE) {
- continue;
- }
-
final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0;
final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth();
- if (canSlide && child != mSlideableView) {
- if (lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.weight > 0)) {
- // Fixed panels in a sliding configuration should
- // be clamped to the fixed panel limit.
- final int childHeightSpec;
- if (skippedFirstPass) {
- // Do initial height measurement if we skipped measuring this view
- // the first time around.
- if (lp.height == LayoutParams.WRAP_CONTENT) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
- MeasureSpec.AT_MOST);
- } else if (lp.height == LayoutParams.MATCH_PARENT) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
- MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height,
- MeasureSpec.EXACTLY);
- }
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(
- child.getMeasuredHeight(), MeasureSpec.EXACTLY);
- }
- final int childWidthSpec = MeasureSpec.makeMeasureSpec(
- fixedPanelWidthLimit, MeasureSpec.EXACTLY);
- child.measure(childWidthSpec, childHeightSpec);
- }
- } else if (lp.weight > 0) {
- int childHeightSpec;
- if (lp.width == 0) {
- // This was skipped the first time; figure out a real height spec.
- if (lp.height == LayoutParams.WRAP_CONTENT) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
- MeasureSpec.AT_MOST);
- } else if (lp.height == LayoutParams.MATCH_PARENT) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
- MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height,
- MeasureSpec.EXACTLY);
- }
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(
- child.getMeasuredHeight(), MeasureSpec.EXACTLY);
- }
+ int newWidth = measuredWidth;
+ int childWidthSpec = 0;
+ if (canSlide) {
+ // Child view consumes available space if the combined width cannot fit into
+ // the layout available width.
+ final int horizontalMargin = lp.leftMargin + lp.rightMargin;
+ newWidth = widthAvailable - horizontalMargin;
+ childWidthSpec = MeasureSpec.makeMeasureSpec(
+ newWidth, MeasureSpec.EXACTLY);
- if (canSlide) {
- // Consume available space
- final int horizontalMargin = lp.leftMargin + lp.rightMargin;
- final int newWidth = widthAvailable - horizontalMargin;
- final int childWidthSpec = MeasureSpec.makeMeasureSpec(
- newWidth, MeasureSpec.EXACTLY);
- if (measuredWidth != newWidth) {
- child.measure(childWidthSpec, childHeightSpec);
- }
- } else {
- // Distribute the extra width proportionally similar to LinearLayout
- final int widthToDistribute = Math.max(0, widthRemaining);
- final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum);
- final int childWidthSpec = MeasureSpec.makeMeasureSpec(
- measuredWidth + addedWidth, MeasureSpec.EXACTLY);
- child.measure(childWidthSpec, childHeightSpec);
- }
+ } else if (lp.weight > 0) {
+ // Distribute the extra width proportionally similar to LinearLayout
+ final int widthToDistribute = Math.max(0, widthRemaining);
+ final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum);
+ newWidth = measuredWidth + addedWidth;
+ childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY);
+ }
+ final int childHeightSpec = measureChildHeight(child, maxLayoutHeight);
+ if (measuredWidth != newWidth) {
+ child.measure(childWidthSpec, childHeightSpec);
}
}
}
@@ -704,14 +636,33 @@
}
}
+ private static int measureChildHeight(@NonNull View child,
+ int maxLayoutHeight) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childHeightSpec;
+ final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0;
+ if (skippedFirstPass) {
+ // This was skipped the first time; figure out a real height spec.
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
+ MeasureSpec.AT_MOST);
+ } else if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
+ MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height,
+ MeasureSpec.EXACTLY);
+ }
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(
+ child.getMeasuredHeight(), MeasureSpec.EXACTLY);
+ }
+ return childHeightSpec;
+ }
+
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final boolean isLayoutRtl = isLayoutRtlSupport();
- if (isLayoutRtl) {
- mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
- } else {
- mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
- }
final int width = r - l;
final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft();
final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight();
@@ -739,8 +690,7 @@
if (lp.slideable) {
final int margin = lp.leftMargin + lp.rightMargin;
- final int range = Math.min(nextXStart,
- width - paddingEnd - mOverhangSize) - xStart - margin;
+ final int range = Math.min(nextXStart, width - paddingEnd) - xStart - margin;
mSlideRange = range;
final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin;
lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 > width - paddingEnd;
@@ -776,14 +726,6 @@
if (mParallaxBy != 0) {
parallaxOtherViews(mSlideOffset);
}
- if (((LayoutParams) mSlideableView.getLayoutParams()).dimWhenOffset) {
- dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor);
- }
- } else {
- // Reset the dim level of all children; it's irrelevant when nothing moves.
- for (int i = 0; i < childCount; i++) {
- dimChildView(getChildAt(i), 0, mSliderFadeColor);
- }
}
updateObscuredViewsVisibility(mSlideableView);
}
@@ -1030,41 +972,18 @@
parallaxOtherViews(mSlideOffset);
}
- if (lp.dimWhenOffset) {
- dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor);
- }
dispatchOnPanelSlide(mSlideableView);
}
- @SuppressWarnings("deprecation")
- private void dimChildView(View v, float mag, int fadeColor) {
- final LayoutParams lp = (LayoutParams) v.getLayoutParams();
-
- if (mag > 0 && fadeColor != 0) {
- final int baseAlpha = (fadeColor & 0xff000000) >>> 24;
- int imag = (int) (baseAlpha * mag);
- int color = imag << 24 | (fadeColor & 0xffffff);
- if (lp.dimPaint == null) {
- lp.dimPaint = new Paint();
- }
- lp.dimPaint.setColorFilter(new android.graphics.PorterDuffColorFilter(
- color, PorterDuff.Mode.SRC_OVER));
- if (v.getLayerType() != View.LAYER_TYPE_HARDWARE) {
- v.setLayerType(View.LAYER_TYPE_HARDWARE, lp.dimPaint);
- }
- invalidateChildRegion(v);
- } else if (v.getLayerType() != View.LAYER_TYPE_NONE) {
- if (lp.dimPaint != null) {
- lp.dimPaint.setColorFilter(null);
- }
- final DisableLayerRunnable dlr = new DisableLayerRunnable(v);
- mPostedRunnables.add(dlr);
- ViewCompat.postOnAnimation(this, dlr);
- }
- }
-
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final boolean isLayoutRtl = isLayoutRtlSupport();
+ final boolean enableEdgeRightTracking = isLayoutRtl ^ isOpen();
+ if (enableEdgeRightTracking) {
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
+ } else {
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
+ }
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
boolean result;
final int save = canvas.save();
@@ -1143,7 +1062,7 @@
* Smoothly animate mDraggingPane to the target X position within its range.
*
* @param slideOffset position to animate to
- * @param velocity initial velocity in case of fling, or 0.
+ * @param velocity initial velocity in case of fling, or 0.
*/
boolean smoothSlideTo(float slideOffset, int velocity) {
if (!mCanSlide) {
@@ -1185,11 +1104,10 @@
}
/**
+ * @param d drawable to use as a shadow
* @deprecated Renamed to {@link #setShadowDrawableLeft(Drawable d)} to support LTR (left to
* right language) and {@link #setShadowDrawableRight(Drawable d)} to support RTL (right to left
* language) during opening/closing.
- *
- * @param d drawable to use as a shadow
*/
@Deprecated
public void setShadowDrawable(Drawable d) {
@@ -1287,9 +1205,6 @@
private void parallaxOtherViews(float slideOffset) {
final boolean isLayoutRtl = isLayoutRtlSupport();
- final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams();
- final boolean dimViews = slideLp.dimWhenOffset
- && (isLayoutRtl ? slideLp.rightMargin : slideLp.leftMargin) <= 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
@@ -1301,23 +1216,18 @@
final int dx = oldOffset - newOffset;
v.offsetLeftAndRight(isLayoutRtl ? -dx : dx);
-
- if (dimViews) {
- dimChildView(v, isLayoutRtl ? mParallaxOffset - 1
- : 1 - mParallaxOffset, mCoveredFadeColor);
- }
}
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
- * @param v View to test for horizontal scrollability
+ * @param v View to test for horizontal scrollability
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
- * @param dx Delta scrolled in pixels
- * @param x X coordinate of the active touch point
- * @param y Y coordinate of the active touch point
+ * @param dx Delta scrolled in pixels
+ * @param x X coordinate of the active touch point
+ * @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
@@ -1334,7 +1244,7 @@
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
&& y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
&& canScroll(child, true, dx, x + scrollX - child.getLeft(),
- y + scrollY - child.getTop())) {
+ y + scrollY - child.getTop())) {
return true;
}
}
@@ -1447,7 +1357,7 @@
int left;
if (isLayoutRtlSupport()) {
- int startToRight = getPaddingRight() + lp.rightMargin;
+ int startToRight = getPaddingRight() + lp.rightMargin;
if (xvel < 0 || (xvel == 0 && mSlideOffset > 0.5f)) {
startToRight += mSlideRange;
}
@@ -1507,14 +1417,19 @@
}
@Override
+ public void onEdgeTouched(int edgeFlags, int pointerId) {
+ mDragHelper.captureChildView(mSlideableView, pointerId);
+ }
+
+ @Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mDragHelper.captureChildView(mSlideableView, pointerId);
}
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
- private static final int[] ATTRS = new int[] {
- android.R.attr.layout_weight
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.layout_weight
};
/**
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index ace3c56..7327d38 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -168,7 +168,7 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class ComplicationHelperActivity extends android.app.Activity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback {
ctor public ComplicationHelperActivity();
method public static android.content.Intent createPermissionRequestHelperIntent(android.content.Context, android.content.ComponentName);
- method public static android.content.Intent createProviderChooserHelperIntent(android.content.Context, android.content.ComponentName, int, java.util.Collection<androidx.wear.complications.data.ComplicationType!>);
+ method public static android.content.Intent createProviderChooserHelperIntent(android.content.Context, android.content.ComponentName, int, java.util.Collection<androidx.wear.complications.data.ComplicationType!>, String?);
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String ACTION_PERMISSION_REQUEST_ONLY = "android.support.wearable.complications.ACTION_PERMISSION_REQUEST_ONLY";
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String ACTION_REQUEST_UPDATE_ALL_ACTIVE = "android.support.wearable.complications.ACTION_REQUEST_UPDATE_ALL_ACTIVE";
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String ACTION_START_PROVIDER_CHOOSER = "android.support.wearable.complications.ACTION_START_PROVIDER_CHOOSER";
@@ -198,6 +198,7 @@
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String EXTRA_PENDING_INTENT = "android.support.wearable.complications.EXTRA_PENDING_INTENT";
field public static final String EXTRA_PROVIDER_INFO = "android.support.wearable.complications.EXTRA_PROVIDER_INFO";
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String EXTRA_SUPPORTED_TYPES = "android.support.wearable.complications.EXTRA_SUPPORTED_TYPES";
+ field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String EXTRA_WATCHFACE_INSTANCE_ID = "androidx.wear.complications.EXTRA_WATCHFACE_INSTANCE_ID";
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String EXTRA_WATCH_FACE_COMPONENT_NAME = "android.support.wearable.complications.EXTRA_WATCH_FACE_COMPONENT_NAME";
}
diff --git a/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt b/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
index fdb6534..e1a8e20 100644
--- a/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
+++ b/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
@@ -72,16 +72,26 @@
}
}
+ @Test
+ public fun instanceId() {
+ assertThat(
+ createIntent(instanceId = "ID-1")
+ .getStringExtra(ProviderChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID)
+ ).isEqualTo("ID-1")
+ }
+
/** Creates an intent with default values for unspecified parameters. */
private fun createIntent(
watchFaceComponentName: ComponentName = defaultWatchFaceComponentName,
complicationId: Int = defaultComplicationId,
+ instanceId: String? = null,
vararg supportedTypes: ComplicationType = defaultSupportedTypes
) = ComplicationHelperActivity.createProviderChooserHelperIntent(
context,
watchFaceComponentName,
complicationId,
- supportedTypes.asList()
+ supportedTypes.asList(),
+ instanceId
)
private companion object {
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
index 727ae00..c74a757 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
@@ -212,17 +212,22 @@
* @param supportedTypes the types supported by the complication, in decreasing order of
* preference. If a provider can supply data for more than one of these types, the type
* chosen will be whichever was specified first.
+ * @param watchFaceInstanceId The ID of the watchface being edited.
*/
@NonNull
public static Intent createProviderChooserHelperIntent(
@NonNull Context context,
@NonNull ComponentName watchFace,
int watchFaceComplicationId,
- @NonNull Collection<ComplicationType> supportedTypes) {
+ @NonNull Collection<ComplicationType> supportedTypes,
+ @Nullable String watchFaceInstanceId) {
Intent intent = new Intent(context, ComplicationHelperActivity.class);
intent.setAction(ACTION_START_PROVIDER_CHOOSER);
intent.putExtra(ProviderChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME, watchFace);
intent.putExtra(ProviderChooserIntent.EXTRA_COMPLICATION_ID, watchFaceComplicationId);
+ if (watchFaceInstanceId != null) {
+ intent.putExtra(ProviderChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID, watchFaceInstanceId);
+ }
int[] wireSupportedTypes = new int[supportedTypes.size()];
int i = 0;
for (ComplicationType supportedType : supportedTypes) {
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderChooserIntent.java b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderChooserIntent.java
index e5bb125..6d3d444 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderChooserIntent.java
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderChooserIntent.java
@@ -84,6 +84,15 @@
"android.support.wearable.complications.EXTRA_COMPLICATION_ID";
/**
+ * Key for an extra that holds the watch face instance id.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final String EXTRA_WATCHFACE_INSTANCE_ID =
+ "androidx.wear.complications.EXTRA_WATCHFACE_INSTANCE_ID";
+
+ /**
* Key for an extra used to include details of the chosen provider in the activity result
* returned by the provider chooser.
*
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
index 78d69e4..88baad2 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
@@ -214,5 +214,7 @@
override fun onStop() {
super.onStop()
editorSession.close()
+ // Make sure the activity closes.
+ finish()
}
}
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
index 94c6a30..1e9fd092 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
@@ -38,6 +38,7 @@
import androidx.test.filters.MediumTest
import androidx.wear.complications.ComplicationBounds
import androidx.wear.complications.DefaultComplicationProviderPolicy
+import androidx.wear.complications.ProviderChooserIntent
import androidx.wear.complications.ProviderInfoRetriever
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationType
@@ -194,9 +195,15 @@
/** Fake ComplicationHelperActivity for testing. */
public class TestComplicationHelperActivity : Activity() {
+ public companion object {
+ public var lastIntent: Intent? = null
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ lastIntent = intent
+
setResult(
123,
Intent().apply {
@@ -717,6 +724,12 @@
0
)
).isEqualTo("Provider3")
+
+ assertThat(
+ TestComplicationHelperActivity.lastIntent?.extras?.getString(
+ ProviderChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID
+ )
+ ).isEqualTo(testInstanceId)
}
}
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 67d1102..24299b1 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -292,7 +292,7 @@
pendingComplicationProviderChooserResult = CompletableDeferred<Boolean>()
pendingComplicationProviderId = complicationId
chooseComplicationProvider.launch(
- ComplicationProviderChooserRequest(this, complicationId)
+ ComplicationProviderChooserRequest(this, complicationId, instanceId)
)
return pendingComplicationProviderChooserResult!!.await()
}
@@ -554,7 +554,8 @@
internal class ComplicationProviderChooserRequest(
internal val editorSession: EditorSession,
- internal val complicationId: Int
+ internal val complicationId: Int,
+ internal val instanceId: String?
)
internal class ComplicationProviderChooserResult(
@@ -576,7 +577,8 @@
context,
input.editorSession.watchFaceComponentName,
input.complicationId,
- input.editorSession.complicationState[input.complicationId]!!.supportedTypes
+ input.editorSession.complicationState[input.complicationId]!!.supportedTypes,
+ input.instanceId
)
if (useTestComplicationHelperActivity) {
intent.component = ComponentName(
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 2de2ff2..18a58f9 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -96,7 +96,6 @@
}
public static interface ComplicationsManager.TapCallback {
- method public default void onComplicationDoubleTapped(int complicationId);
method public default void onComplicationSingleTapped(int complicationId);
}
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 2de2ff2..18a58f9 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -96,7 +96,6 @@
}
public static interface ComplicationsManager.TapCallback {
- method public default void onComplicationDoubleTapped(int complicationId);
method public default void onComplicationSingleTapped(int complicationId);
}
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 544e8b9..70efcc5 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -96,7 +96,6 @@
}
public static interface ComplicationsManager.TapCallback {
- method public default void onComplicationDoubleTapped(int complicationId);
method public default void onComplicationSingleTapped(int complicationId);
}
diff --git a/wear/wear-watchface/samples/src/main/AndroidManifest.xml b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
index 77e6821..1bf118d 100644
--- a/wear/wear-watchface/samples/src/main/AndroidManifest.xml
+++ b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
@@ -61,7 +61,7 @@
android:resource="@drawable/watch_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL"/>
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
@@ -95,7 +95,7 @@
android:resource="@drawable/watch_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL"/>
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
@@ -129,7 +129,7 @@
android:resource="@drawable/watch_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL"/>
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
@@ -145,7 +145,7 @@
android:label="Config"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
- <action android:name="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL" />
+ <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 1a83c6e..27670a0 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -655,4 +655,33 @@
(unitSquareBounds.bottom * screen.height()).toInt()
)
}
+
+ @UiThread
+ internal fun dump(writer: IndentingPrintWriter) {
+ writer.println("Complication $id:")
+ writer.increaseIndent()
+ writer.println("fixedComplicationProvider=$fixedComplicationProvider")
+ writer.println("enabled=$enabled")
+ writer.println("renderer.isHighlighted=${renderer.isHighlighted}")
+ writer.println("boundsType=$boundsType")
+ writer.println("complicationConfigExtras=$complicationConfigExtras")
+ writer.println("supportedTypes=${supportedTypes.joinToString { it.toString() }}")
+ writer.println("complicationConfigExtras=$complicationConfigExtras")
+ writer.println(
+ "defaultProviderPolicy.primaryProvider=${defaultProviderPolicy.primaryProvider}"
+ )
+ writer.println(
+ "defaultProviderPolicy.secondaryProvider=${defaultProviderPolicy.secondaryProvider}"
+ )
+ writer.println(
+ "defaultProviderPolicy.systemProviderFallback=" +
+ "${defaultProviderPolicy.systemProviderFallback}"
+ )
+ writer.println("data=${renderer.getIdAndData()?.complicationData}")
+ val bounds = complicationBounds.perComplicationTypeBounds.map {
+ "${it.key} -> ${it.value}"
+ }
+ writer.println("bounds=[$bounds]")
+ writer.decreaseIndent()
+ }
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index 38aec04..112b510 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -21,7 +21,6 @@
import android.content.Context
import android.content.Intent
import android.icu.util.Calendar
-import android.os.Bundle
import android.support.wearable.watchface.accessibility.AccessibilityUtils
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import androidx.annotation.UiThread
@@ -68,14 +67,6 @@
* @param complicationId The watch face's id for the complication single tapped
*/
public fun onComplicationSingleTapped(complicationId: Int) {}
-
- /**
- * Called when the user double taps on a complication, launches the complication
- * configuration activity.
- *
- * @param complicationId The watch face's id for the complication double tapped
- */
- public fun onComplicationDoubleTapped(complicationId: Int) {}
}
private lateinit var watchFaceHostApi: WatchFaceHostApi
@@ -371,48 +362,6 @@
}
/**
- * Called when the user double taps on a complication, launches the complication
- * configuration activity unless the complication is fixed in which case it does
- * nothing.
- *
- * @param complicationId The watch face's id for the complication double tapped
- */
- @SuppressWarnings("SyntheticAccessor")
- @UiThread
- internal fun onComplicationDoubleTapped(complicationId: Int) {
- // Check if the complication is missing permissions.
- val complication = complications[complicationId] ?: return
- if (complication.fixedComplicationProvider) {
- return
- }
- val data = complication.renderer.getIdAndData() ?: return
- if (data.complicationData.type == ComplicationType.NO_PERMISSION) {
- watchFaceHostApi.getContext().startActivity(
- ComplicationHelperActivity.createPermissionRequestHelperIntent(
- watchFaceHostApi.getContext(),
- getComponentName(watchFaceHostApi.getContext())
- )
- )
- return
- }
- watchFaceHostApi.getContext().startActivity(
- ComplicationHelperActivity.createProviderChooserHelperIntent(
- watchFaceHostApi.getContext(),
- getComponentName(watchFaceHostApi.getContext()),
- complicationId,
- complication.supportedTypes
- ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).apply {
- complication.complicationConfigExtras?.let {
- replaceExtras(Bundle(it).apply { putAll(extras!!) })
- }
- }
- )
- for (complicationListener in complicationListeners) {
- complicationListener.onComplicationDoubleTapped(complicationId)
- }
- }
-
- /**
* Adds a [TapCallback] which is called whenever the user interacts with a complication.
*/
@UiThread
@@ -428,4 +377,14 @@
public fun removeTapListener(tapCallback: TapCallback) {
complicationListeners.remove(tapCallback)
}
+
+ @UiThread
+ internal fun dump(writer: IndentingPrintWriter) {
+ writer.println("ComplicationsManager:")
+ writer.increaseIndent()
+ for ((_, complication) in complications) {
+ complication.dump(writer)
+ }
+ writer.decreaseIndent()
+ }
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/IndentingPrintWriter.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/IndentingPrintWriter.kt
new file mode 100644
index 0000000..02005c5
--- /dev/null
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/IndentingPrintWriter.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 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.wear.watchface
+
+import android.util.Printer
+import java.io.PrintWriter
+import java.io.Writer
+
+/**
+ * Lightweight wrapper around [java.io.PrintWriter] that automatically indents newlines based
+ * on internal state.
+ *
+ * Delays writing indent until first actual write on a newline, enabling indent modification
+ * after newline.
+ */
+internal class IndentingPrintWriter(
+ writer: Writer,
+ private val singleIndent: String = "\t"
+) : Printer {
+ private val writer: PrintWriter = PrintWriter(writer)
+
+ /** Mutable version of current indent */
+ private val indentBuilder = StringBuilder()
+
+ /** Cache of current [.indentBuilder] value */
+ private var currentIndent: CharArray? = null
+
+ /**
+ * Flag indicating if we're currently sitting on an empty line, and that next write should be
+ * prefixed with the current indent.
+ */
+ private var emptyLine = true
+
+ /** Increases the indentation level for future lines. */
+ fun increaseIndent() {
+ indentBuilder.append(singleIndent)
+ currentIndent = null
+ }
+
+ /** Decreases the indentation level for future lines. */
+ fun decreaseIndent() {
+ indentBuilder.delete(0, singleIndent.length)
+ currentIndent = null
+ }
+
+ /** Prints `string`, followed by a newline. */
+ // Printer
+ override fun println(string: String) {
+ print(string)
+ print("\n")
+ }
+
+ /** Prints `string`, or `"null"` */
+ fun print(string: String?) {
+ val str = string ?: "null"
+ write(str, 0, str.length)
+ }
+
+ /** Ensures that all pending data is sent out to the target */
+ fun flush() {
+ writer.flush()
+ }
+
+ private fun write(string: String, offset: Int, count: Int) {
+ val bufferEnd = offset + count
+ var lineStart = offset
+ var lineEnd = offset
+
+ // March through incoming buffer looking for newlines
+ while (lineEnd < bufferEnd) {
+ val ch = string[lineEnd++]
+ if (ch == '\n') {
+ maybeWriteIndent()
+ writer.write(string, lineStart, lineEnd - lineStart)
+ lineStart = lineEnd
+ emptyLine = true
+ }
+ }
+ if (lineStart != lineEnd) {
+ maybeWriteIndent()
+ writer.write(string, lineStart, lineEnd - lineStart)
+ }
+ }
+
+ private fun maybeWriteIndent() {
+ if (emptyLine) {
+ emptyLine = false
+ if (indentBuilder.isNotEmpty()) {
+ if (currentIndent == null) {
+ currentIndent = indentBuilder.toString().toCharArray()
+ }
+ writer.write(currentIndent, 0, currentIndent!!.size)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index 91e83df..0ae9d02 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
@@ -95,6 +95,14 @@
observers.remove(observer)
}
}
+
+ override fun toString(): String {
+ return if (hasValue()) {
+ value.toString()
+ } else {
+ "<unset>"
+ }
+ }
}
/**
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
index 1589292..19d5c06 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
@@ -155,4 +155,15 @@
selectedComplicationId,
outlineTint
)
+
+ internal fun dump(writer: IndentingPrintWriter) {
+ writer.println("RenderParameters:")
+ writer.increaseIndent()
+ writer.println("drawMode=${drawMode.name}")
+ writer.println("selectedComplicationId=$selectedComplicationId")
+ writer.println("outlineTint=$outlineTint")
+ val params = layerParameters.map { "${it.key} -> ${it.value.name}" }.joinToString { it }
+ writer.println("layerParameters=[$params]")
+ writer.decreaseIndent()
+ }
}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index d2a98e8..0f5beda 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -260,6 +260,9 @@
}
}
+ @UiThread
+ internal abstract fun dump(writer: IndentingPrintWriter)
+
/**
* Watch faces that require [Canvas] rendering should extend their [Renderer] from this class.
*/
@@ -350,6 +353,19 @@
bounds: Rect,
calendar: Calendar
)
+
+ internal override fun dump(writer: IndentingPrintWriter) {
+ writer.println("CanvasRenderer:")
+ writer.increaseIndent()
+ writer.println("canvasType=$canvasType")
+ writer.println("screenBounds=$screenBounds")
+ writer.println(
+ "interactiveDrawModeUpdateDelayMillis=$interactiveDrawModeUpdateDelayMillis"
+ )
+ writer.println("shouldAnimate=${shouldAnimate()}")
+ renderParameters.dump(writer)
+ writer.decreaseIndent()
+ }
}
/**
@@ -633,5 +649,17 @@
*/
@UiThread
public abstract fun render(calendar: Calendar)
+
+ internal override fun dump(writer: IndentingPrintWriter) {
+ writer.println("GlesRenderer:")
+ writer.increaseIndent()
+ writer.println("screenBounds=$screenBounds")
+ writer.println(
+ "interactiveDrawModeUpdateDelayMillis=$interactiveDrawModeUpdateDelayMillis"
+ )
+ writer.println("shouldAnimate=${shouldAnimate()}")
+ renderParameters.dump(writer)
+ writer.decreaseIndent()
+ }
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 118497d..02c3166 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -426,8 +426,6 @@
CancellableUniqueTask(watchFaceHostApi.getHandler())
private val pendingUpdateTime: CancellableUniqueTask =
CancellableUniqueTask(watchFaceHostApi.getHandler())
- private val pendingPostDoubleTap: CancellableUniqueTask =
- CancellableUniqueTask(watchFaceHostApi.getHandler())
internal val componentName =
ComponentName(
@@ -721,7 +719,6 @@
internal fun onDestroy() {
pendingSingleTap.cancel()
pendingUpdateTime.cancel()
- pendingPostDoubleTap.cancel()
renderer.onDestroy()
watchState.isAmbient.removeObserver(ambientObserver)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !watchState.isHeadless) {
@@ -892,6 +889,7 @@
) {
val tappedComplication = complicationsManager.getComplicationAt(x, y)
if (tappedComplication == null) {
+ System.out.println("<<< tappedComplication == null")
clearGesture()
tapListener?.onTap(originalTapType, x, y)
return
@@ -923,22 +921,7 @@
clearGesture()
return
}
- if (pendingPostDoubleTap.isPending()) {
- return
- }
- if (pendingSingleTap.isPending()) {
- // The user tapped twice rapidly on the same complication so treat this as
- // a double tap.
- complicationsManager.onComplicationDoubleTapped(tappedComplication.id)
- clearGesture()
-
- // Block subsequent taps for a short time, to prevent accidental triple taps.
- pendingPostDoubleTap.postDelayedUnique(
- ViewConfiguration.getDoubleTapTimeout().toLong()
- ) {
- // NOP.
- }
- } else {
+ if (!pendingSingleTap.isPending()) {
// Give the user immediate visual feedback, the UI feels sluggish if we defer
// this.
complicationsManager.bringAttentionToComplication(tappedComplication.id)
@@ -973,4 +956,26 @@
lastTappedComplicationId = null
pendingSingleTap.cancel()
}
+
+ @UiThread
+ fun dump(writer: IndentingPrintWriter) {
+ writer.println("WatchFaceImpl ($componentName): ")
+ writer.increaseIndent()
+ writer.println("calendar=$calendar")
+ writer.println("mockTime.maxTime=${mockTime.maxTime}")
+ writer.println("mockTime.minTime=${mockTime.minTime}")
+ writer.println("mockTime.speed=${mockTime.speed}")
+ writer.println("nextDrawTimeMillis=$nextDrawTimeMillis")
+ writer.println("muteMode=$muteMode")
+ writer.println("pendingSingleTap=${pendingSingleTap.isPending()}")
+ writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
+ writer.println("lastTappedComplicationId=$lastTappedComplicationId")
+ writer.println("lastTappedPosition=$lastTappedPosition")
+ writer.println("userStyleRepository.userStyle=${userStyleRepository.userStyle}")
+ writer.println("userStyleRepository.schema=${userStyleRepository.schema}")
+ watchState.dump(writer)
+ complicationsManager.dump(writer)
+ renderer.dump(writer)
+ writer.decreaseIndent()
+ }
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index e9c4b6e..7105cd7 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -65,6 +65,7 @@
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.data.IdAndComplicationStateWireFormat
import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.editor.EditorService
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleRepository
import androidx.wear.watchface.style.UserStyleSetting
@@ -72,7 +73,9 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.launch
+import java.io.FileDescriptor
import java.io.FileNotFoundException
+import java.io.PrintWriter
import java.util.concurrent.CountDownLatch
/** The wire format for [ComplicationData]. */
@@ -1137,6 +1140,56 @@
Log.e(TAG, "Failed to set accessibility labels: ", e)
}
}
+
+ @UiThread
+ fun dump(writer: IndentingPrintWriter) {
+ require(uiThreadHandler.looper.isCurrentThread) {
+ "dump must be called from the UIThread"
+ }
+ writer.println("WatchFaceEngine:")
+ writer.increaseIndent()
+ when {
+ this::iWatchFaceService.isInitialized -> writer.println("WSL style init flow")
+ this::watchFaceImpl.isInitialized -> writer.println("Androidx style init flow")
+ expectPreRInitFlow() -> writer.println("Expecting WSL style init")
+ else -> writer.println("Expecting androidx style style init")
+ }
+
+ if (this::iWatchFaceService.isInitialized) {
+ writer.println(
+ "iWatchFaceService.asBinder().isBinderAlive=" +
+ "${iWatchFaceService.asBinder().isBinderAlive}"
+ )
+ if (iWatchFaceService.asBinder().isBinderAlive) {
+ writer.println("iWatchFaceService.apiVersion=${iWatchFaceService.apiVersion}")
+ }
+ }
+ writer.println("watchFaceInitStarted=$watchFaceInitStarted")
+ writer.println("asyncWatchFaceConstructionPending=$asyncWatchFaceConstructionPending")
+
+ if (this::interactiveInstanceId.isInitialized) {
+ writer.println("interactiveInstanceId=$interactiveInstanceId")
+ }
+
+ writer.println("frameCallbackPending=$frameCallbackPending")
+ writer.println("destroyed=$destroyed")
+
+ if (!destroyed && this::watchFaceImpl.isInitialized) {
+ watchFaceImpl.dump(writer)
+ }
+ writer.decreaseIndent()
+ }
+ }
+
+ @UiThread
+ override fun dump(fd: FileDescriptor, writer: PrintWriter, args: Array<String>) {
+ super.dump(fd, writer, args)
+ val indentingPrintWriter = IndentingPrintWriter(writer)
+ indentingPrintWriter.println("AndroidX WatchFaceService $packageName")
+ InteractiveInstanceManager.dump(indentingPrintWriter)
+ EditorService.globalEditorService.dump(indentingPrintWriter)
+ HeadlessWatchFaceImpl.dump(indentingPrintWriter)
+ indentingPrintWriter.flush()
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index 8b664c3..d0cb375 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -18,6 +18,7 @@
import android.app.NotificationManager
import androidx.annotation.RestrictTo
+import androidx.annotation.UiThread
/**
* Describes the current state of the wearable including some hardware details such as whether or
@@ -72,7 +73,23 @@
/** Whether or not this is a headless watchface. */
public val isHeadless: Boolean
-)
+) {
+ @UiThread
+ internal fun dump(writer: IndentingPrintWriter) {
+ writer.println("WatchState:")
+ writer.increaseIndent()
+ writer.println("interruptionFilter=$interruptionFilter")
+ writer.println("isAmbient=$isAmbient")
+ writer.println("isBatteryLowAndNotCharging=$isBatteryLowAndNotCharging")
+ writer.println("isVisible=$isVisible")
+ writer.println("hasLowBitAmbient=$hasLowBitAmbient")
+ writer.println("hasBurnInProtection=$hasBurnInProtection")
+ writer.println("analogPreviewReferenceTimeMillis=$analogPreviewReferenceTimeMillis")
+ writer.println("digitalPreviewReferenceTimeMillis=$digitalPreviewReferenceTimeMillis")
+ writer.println("isHeadless=$isHeadless")
+ writer.decreaseIndent()
+ }
+}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index b73c697..a468194 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -18,6 +18,8 @@
import android.os.Handler
import androidx.annotation.RequiresApi
+import androidx.annotation.UiThread
+import androidx.wear.watchface.IndentingPrintWriter
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.control.data.ComplicationScreenshotParams
import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
@@ -33,6 +35,32 @@
private val uiThreadHandler: Handler
) : IHeadlessWatchFace.Stub() {
+ internal companion object {
+ @UiThread
+ fun dump(indentingPrintWriter: IndentingPrintWriter) {
+ indentingPrintWriter.println("HeadlessWatchFace instances:")
+ indentingPrintWriter.increaseIndent()
+ for (instance in headlessInstances) {
+ require(instance.uiThreadHandler.looper.isCurrentThread) {
+ "dump must be called from the UIThread"
+ }
+ indentingPrintWriter.println("HeadlessWatchFaceImpl:")
+ indentingPrintWriter.increaseIndent()
+ instance.engine?.dump(indentingPrintWriter)
+ indentingPrintWriter.decreaseIndent()
+ }
+ indentingPrintWriter.decreaseIndent()
+ }
+
+ private val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
+ }
+
+ init {
+ uiThreadHandler.runOnHandler {
+ headlessInstances.add(this)
+ }
+ }
+
override fun getApiVersion() = IHeadlessWatchFace.API_VERSION
override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
@@ -50,7 +78,10 @@
engine!!.watchFaceImpl.userStyleRepository.schema.toWireFormat()
override fun release() {
- engine?.onDestroy()
- engine = null
+ uiThreadHandler.runOnHandler {
+ headlessInstances.remove(this)
+ engine?.onDestroy()
+ engine = null
+ }
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
index ed13e05..d18f6e5 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint
import androidx.annotation.RequiresApi
+import androidx.annotation.UiThread
+import androidx.wear.watchface.IndentingPrintWriter
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
/** Keeps track of [InteractiveWatchFaceImpl]s. */
@@ -27,7 +29,17 @@
private class RefCountedInteractiveWatchFaceInstance(
val impl: InteractiveWatchFaceImpl,
var refcount: Int
- )
+ ) {
+ @UiThread
+ fun dump(writer: IndentingPrintWriter) {
+ writer.println("InteractiveInstanceManager:")
+ writer.increaseIndent()
+ writer.println("impl.instanceId=${impl.instanceId}")
+ writer.println("refcount=$refcount")
+ impl.engine.dump(writer)
+ writer.decreaseIndent()
+ }
+ }
class PendingWallpaperInteractiveWatchFaceInstance(
val params: WallpaperInteractiveWatchFaceInstanceParams,
@@ -104,5 +116,22 @@
return returnValue
}
}
+
+ @UiThread
+ fun dump(writer: IndentingPrintWriter) {
+ writer.println("InteractiveInstanceManager instances:")
+ writer.increaseIndent()
+ pendingWallpaperInteractiveWatchFaceInstance?.let {
+ writer.println(
+ "Pending WallpaperInteractiveWatchFaceInstance id ${it.params.instanceId}"
+ )
+ }
+ synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+ for ((_, value) in instances) {
+ value.dump(writer)
+ }
+ }
+ writer.decreaseIndent()
+ }
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
index 95b06d6..3138b6a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
@@ -18,6 +18,7 @@
import android.os.IBinder
import androidx.annotation.RestrictTo
+import androidx.wear.watchface.IndentingPrintWriter
import androidx.wear.watchface.editor.data.EditorStateWireFormat
/**
@@ -72,4 +73,18 @@
}
}
}
+
+ internal fun dump(writer: IndentingPrintWriter) {
+ writer.println("EditorService:")
+ writer.increaseIndent()
+ synchronized(lock) {
+ for ((id, observer) in observers) {
+ writer.println("id = $id, alive = ${observer.asBinder().isBinderAlive}")
+ if (observer.asBinder().isBinderAlive) {
+ writer.println("$apiVersion = {observer.apiVersion}")
+ }
+ }
+ }
+ writer.decreaseIndent()
+ }
}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 657398d..fdc5d9b 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -48,8 +48,8 @@
private val handler: Handler,
private val tapListener: WatchFace.TapListener?
) : WatchFaceService() {
+ var singleTapCount = 0
var complicationSingleTapped: Int? = null
- var complicationDoubleTapped: Int? = null
var complicationSelected: Int? = null
var mockSystemTimeMillis = 0L
var lastUserStyle: UserStyle? = null
@@ -67,10 +67,7 @@
object : ComplicationsManager.TapCallback {
override fun onComplicationSingleTapped(complicationId: Int) {
complicationSingleTapped = complicationId
- }
-
- override fun onComplicationDoubleTapped(complicationId: Int) {
- complicationDoubleTapped = complicationId
+ singleTapCount++
}
})
}
@@ -84,7 +81,6 @@
fun clearTappedState() {
complicationSingleTapped = null
- complicationDoubleTapped = null
}
init {
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 47cc2ac..8bb7060 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -92,7 +92,7 @@
@Config(manifest = Config.NONE)
@RunWith(WatchFaceTestRunner::class)
-class WatchFaceServiceTest {
+public class WatchFaceServiceTest {
private val handler = mock<Handler>()
private val iWatchFaceService = mock<IWatchFaceService>()
@@ -104,7 +104,7 @@
`when`(surfaceHolder.surfaceFrame).thenReturn(ONE_HUNDRED_BY_ONE_HUNDRED_RECT)
}
- companion object {
+ private companion object {
val ONE_HUNDRED_BY_ONE_HUNDRED_RECT = Rect(0, 0, 100, 100)
}
@@ -444,7 +444,7 @@
}
@Before
- fun setUp() {
+ public fun setUp() {
`when`(handler.getLooper()).thenReturn(Looper.myLooper())
// Capture tasks posted to mHandler and insert in mPendingTasks which is under our control.
@@ -476,7 +476,7 @@
}
@After
- fun validate() {
+ public fun validate() {
if (this::interactiveWatchFaceInstanceWCS.isInitialized) {
interactiveWatchFaceInstanceWCS.release()
}
@@ -489,7 +489,7 @@
}
@Test
- fun maybeUpdateDrawMode_setsCorrectDrawMode() {
+ public fun maybeUpdateDrawMode_setsCorrectDrawMode() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -528,7 +528,7 @@
}
@Test
- fun onDraw_calendar_setFromSystemTime() {
+ public fun onDraw_calendar_setFromSystemTime() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -542,7 +542,7 @@
}
@Test
- fun onDraw_calendar_affectedCorrectly_with2xMockTime() {
+ public fun onDraw_calendar_affectedCorrectly_with2xMockTime() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -569,7 +569,7 @@
}
@Test
- fun onDraw_calendar_affectedCorrectly_withMockTimeWrapping() {
+ public fun onDraw_calendar_affectedCorrectly_withMockTimeWrapping() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -641,7 +641,7 @@
}
@Test
- fun singleTaps_correctlyDetected_and_highlightComplications() {
+ public fun singleTaps_correctlyDetected_and_highlightComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -678,82 +678,11 @@
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
assertThat(complicationDrawableLeft.isHighlighted).isFalse()
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(2)
}
@Test
- fun doubleTaps_correctlyDetected_and_highlightComplications() {
- initEngine(
- WatchFaceType.ANALOG,
- listOf(leftComplication, rightComplication),
- UserStyleSchema(emptyList())
- )
-
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
- assertThat(complicationDrawableRight.isHighlighted).isFalse()
-
- // Tap left complication.
- doubleTapAt(30, 50, ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
- assertThat(testWatchFaceService.complicationDoubleTapped).isEqualTo(LEFT_COMPLICATION_ID)
- assertThat(complicationDrawableLeft.isHighlighted).isTrue()
-
- runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
-
- // Tap right complication.
- testWatchFaceService.reset()
- doubleTapAt(70, 50, ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
- assertThat(testWatchFaceService.complicationDoubleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
- assertThat(complicationDrawableRight.isHighlighted).isTrue()
-
- runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableRight.isHighlighted).isFalse()
-
- // Tap on blank space.
- testWatchFaceService.reset()
- doubleTapAt(1, 1, ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
-
- runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
- }
-
- @Test
- fun doubleTap_onFixedComplication_ignored() {
- val fixedLeftComplication =
- Complication.createRoundRectComplicationBuilder(
- LEFT_COMPLICATION_ID,
- CanvasComplicationDrawable(
- complicationDrawableLeft,
- watchState.asWatchState()
- ).apply {
- setIdAndData(createIdAndComplicationData(LEFT_COMPLICATION_ID), false)
- },
- listOf(
- ComplicationType.RANGED_VALUE,
- ComplicationType.LONG_TEXT,
- ComplicationType.SHORT_TEXT,
- ComplicationType.MONOCHROMATIC_IMAGE,
- ComplicationType.SMALL_IMAGE
- ),
- DefaultComplicationProviderPolicy(SystemProviders.SUNRISE_SUNSET),
- ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
- ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
- .setFixedComplicationProvider(true)
- .build()
-
- initEngine(
- WatchFaceType.ANALOG,
- listOf(fixedLeftComplication, rightComplication),
- UserStyleSchema(emptyList())
- )
-
- // Double tap left complication.
- doubleTapAt(30, 50, ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
- }
-
- @Test
- fun fastTap_onDifferentComplications_ignored() {
+ public fun fastTap_onDifferentComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -768,20 +697,20 @@
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
tapAt(70, 50)
- // Both complications get temporarily highlighted but neither onComplicationSingleTapped
- // nor onComplicationDoubleTapped fire.
+ // Both complications get temporarily highlighted but only the second one registers a tap.
assertThat(complicationDrawableLeft.isHighlighted).isTrue()
assertThat(complicationDrawableRight.isHighlighted).isTrue()
assertThat(testWatchFaceService.complicationSingleTapped).isNull()
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
assertThat(complicationDrawableLeft.isHighlighted).isFalse()
assertThat(complicationDrawableRight.isHighlighted).isFalse()
+ assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
}
@Test
- fun slow_doubleTap_recogisedAsSingleTap() {
+ public fun slow_doubleTap_recognisedAsSingleTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -795,11 +724,11 @@
doubleTapAt(30, 50, ViewConfiguration.getDoubleTapTimeout().toLong() * 2)
assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(LEFT_COMPLICATION_ID)
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
}
@Test
- fun tripleTap_recogisedAsDoubleTap() {
+ public fun tripleTap_recognisedAsSingleTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -812,12 +741,12 @@
// Quickly tap left complication thrice.
tripleTapAt(30, 50, ViewConfiguration.getDoubleTapTimeout().toLong() / 2)
- assertThat(testWatchFaceService.complicationSingleTapped).isNull()
- assertThat(testWatchFaceService.complicationDoubleTapped).isEqualTo(LEFT_COMPLICATION_ID)
+ assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(LEFT_COMPLICATION_ID)
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
}
@Test
- fun tapCancel_after_tapDown_at_same_location_HandledAsSingleTap() {
+ public fun tapCancel_after_tapDown_at_same_location_HandledAsSingleTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -832,7 +761,7 @@
}
@Test
- fun tapDown_then_tapDown_tapCancel_HandledAsSingleTap() {
+ public fun tapDown_then_tapDown_tapCancel_HandledAsSingleTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -850,10 +779,11 @@
watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, 70, 50)
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
}
@Test
- fun tapDown_tapCancel_different_positions_CancelsTap() {
+ public fun tapDown_tapCancel_different_positions_CancelsTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -868,11 +798,11 @@
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
assertThat(testWatchFaceService.complicationSingleTapped).isNull()
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(0)
}
@Test
- fun singleTap_recognisedAfterTripleTap() {
+ public fun singleTap_recognisedAfterTripleTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -893,11 +823,11 @@
assertThat(complicationDrawableRight.isHighlighted).isTrue()
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
- assertThat(testWatchFaceService.complicationDoubleTapped).isNull()
+ assertThat(testWatchFaceService.singleTapCount).isEqualTo(3)
}
@Test
- fun tapListener_tap() {
+ public fun tapListener_tap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -913,7 +843,7 @@
}
@Test
- fun tapListener_tapComplication() {
+ public fun tapListener_tapComplication() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -929,7 +859,7 @@
}
@Test
- fun interactiveFrameRate_reducedWhenBatteryLow() {
+ public fun interactiveFrameRate_reducedWhenBatteryLow() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -954,7 +884,7 @@
}
@Test
- fun interactiveFrameRate_restoreWhenPowerConnectedAfterBatteryLow() {
+ public fun interactiveFrameRate_restoreWhenPowerConnectedAfterBatteryLow() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -981,7 +911,7 @@
}
@Test
- fun computeDelayTillNextFrame_accountsForSlowDraw() {
+ public fun computeDelayTillNextFrame_accountsForSlowDraw() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -998,7 +928,7 @@
}
@Test
- fun computeDelayTillNextFrame_dropsFramesForVerySlowDraw() {
+ public fun computeDelayTillNextFrame_dropsFramesForVerySlowDraw() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1014,7 +944,7 @@
}
@Test
- fun computeDelayTillNextFrame_perservesPhaseForVerySlowDraw() {
+ public fun computeDelayTillNextFrame_perservesPhaseForVerySlowDraw() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1033,7 +963,7 @@
}
@Test
- fun computeDelayTillNextFrame_beginFrameTimeInTheFuture() {
+ public fun computeDelayTillNextFrame_beginFrameTimeInTheFuture() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1049,7 +979,7 @@
}
@Test
- fun getComplicationIdAt_returnsCorrectComplications() {
+ public fun getComplicationIdAt_returnsCorrectComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1067,7 +997,7 @@
}
@Test
- fun getBackgroundComplicationId_returnsCorrectId() {
+ public fun getBackgroundComplicationId_returnsCorrectId() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1099,7 +1029,7 @@
}
@Test
- fun getStoredUserStyleNotSupported_userStyle_isPersisted() {
+ public fun getStoredUserStyleNotSupported_userStyle_isPersisted() {
// The style should get persisted in a file because this test is set up using the legacy
// Wear 2.0 APIs.
initEngine(
@@ -1158,7 +1088,7 @@
}
@Test
- fun initWallpaperInteractiveWatchFaceInstanceWithUserStyle() {
+ public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyle() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
@@ -1194,7 +1124,7 @@
}
@Test
- fun initWallpaperInteractiveWatchFaceInstanceWithUserStyleThatDoesntMatchSchema() {
+ public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyleThatDoesntMatchSchema() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
@@ -1218,7 +1148,7 @@
}
@Test
- fun wear2ImmutablePropertiesSetCorrectly() {
+ public fun wear2ImmutablePropertiesSetCorrectly() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
@@ -1233,7 +1163,7 @@
}
@Test
- fun wear2ImmutablePropertiesSetCorrectly2() {
+ public fun wear2ImmutablePropertiesSetCorrectly2() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
@@ -1248,7 +1178,7 @@
}
@Test
- fun wallpaperInteractiveWatchFaceImmutablePropertiesSetCorrectly() {
+ public fun wallpaperInteractiveWatchFaceImmutablePropertiesSetCorrectly() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
@@ -1272,7 +1202,7 @@
}
@Test
- fun onCreate_calls_setActiveComplications_withCorrectIDs() {
+ public fun onCreate_calls_setActiveComplications_withCorrectIDs() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
@@ -1288,7 +1218,7 @@
}
@Test
- fun onCreate_calls_setContentDescriptionLabels_withCorrectArgs() {
+ public fun onCreate_calls_setContentDescriptionLabels_withCorrectArgs() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
@@ -1306,7 +1236,7 @@
}
@Test
- fun setActiveComplications_afterDisablingSeveralComplications() {
+ public fun setActiveComplications_afterDisablingSeveralComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
@@ -1321,7 +1251,7 @@
}
@Test
- fun initial_setContentDescriptionLabels_afterDisablingSeveralComplications() {
+ public fun initial_setContentDescriptionLabels_afterDisablingSeveralComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
@@ -1351,7 +1281,7 @@
}
@Test
- fun moveComplications() {
+ public fun moveComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1411,7 +1341,7 @@
}
@Test
- fun getOptionForIdentifier_ListViewStyleSetting() {
+ public fun getOptionForIdentifier_ListViewStyleSetting() {
// Check the correct Options are returned for known option names.
assertThat(colorStyleSetting.getOptionForId(redStyleOption.id)).isEqualTo(
redStyleOption
@@ -1428,7 +1358,7 @@
}
@Test
- fun centerX_and_centerY_containUpToDateValues() {
+ public fun centerX_and_centerY_containUpToDateValues() {
initEngine(WatchFaceType.ANALOG, emptyList(), UserStyleSchema(emptyList()))
assertThat(renderer.centerX).isEqualTo(50f)
@@ -1450,7 +1380,7 @@
}
@Test
- fun requestStyleBeforeSetBinder() {
+ public fun requestStyleBeforeSetBinder() {
var userStyleRepository =
UserStyleRepository(UserStyleSchema(emptyList()))
var testRenderer = TestRenderer(
@@ -1489,7 +1419,7 @@
}
@Test
- fun defaultProvidersWithFallbacks_newApi() {
+ public fun defaultProvidersWithFallbacks_newApi() {
val provider1 = ComponentName("com.app1", "com.app1.App1")
val provider2 = ComponentName("com.app2", "com.app2.App2")
val complication = Complication.createRoundRectComplicationBuilder(
@@ -1517,7 +1447,7 @@
}
@Test
- fun defaultProvidersWithFallbacks_oldApi() {
+ public fun defaultProvidersWithFallbacks_oldApi() {
val provider1 = ComponentName("com.app1", "com.app1.App1")
val provider2 = ComponentName("com.app2", "com.app2.App2")
val complication = Complication.createRoundRectComplicationBuilder(
@@ -1553,7 +1483,7 @@
}
@Test
- fun previewReferenceTimeMillisAnalog() {
+ public fun previewReferenceTimeMillisAnalog() {
val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
@@ -1583,7 +1513,7 @@
}
@Test
- fun previewReferenceTimeMillisDigital() {
+ public fun previewReferenceTimeMillisDigital() {
val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
@@ -1613,7 +1543,7 @@
}
@Test
- fun getComplicationDetails() {
+ public fun getComplicationDetails() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
@@ -1669,7 +1599,7 @@
}
@Test
- fun shouldAnimateOverrideControlsEnteringAmbientMode() {
+ public fun shouldAnimateOverrideControlsEnteringAmbientMode() {
var userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
var testRenderer = object : TestRenderer(
surfaceHolder,
@@ -1712,7 +1642,7 @@
}
@Test
- fun complicationsUserStyleSettingSelectionAppliesChanges() {
+ public fun complicationsUserStyleSettingSelectionAppliesChanges() {
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
@@ -1757,7 +1687,7 @@
}
@Test
- fun partialComplicationOverrides() {
+ public fun partialComplicationOverrides() {
val bothComplicationsOption = ComplicationsOption(
LEFT_AND_RIGHT_COMPLICATIONS,
"Left And Right",
@@ -1832,7 +1762,7 @@
}
@Test
- fun partialComplicationOverrideAppliedToInitialStyle() {
+ public fun partialComplicationOverrideAppliedToInitialStyle() {
val bothComplicationsOption = ComplicationsOption(
LEFT_AND_RIGHT_COMPLICATIONS,
"Left And Right",
@@ -1870,7 +1800,7 @@
}
@Test
- fun observeComplicationData() {
+ public fun observeComplicationData() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -1924,7 +1854,7 @@
}
@Test
- fun complication_isActiveAt() {
+ public fun complication_isActiveAt() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication),
@@ -1990,7 +1920,7 @@
}
@Test
- fun updateInvalidCOmpliationIdDoesNotCrash() {
+ public fun updateInvalidCOmpliationIdDoesNotCrash() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication),
@@ -2023,7 +1953,7 @@
}
@Test
- fun invalidateRendererBeforeFullInit() {
+ public fun invalidateRendererBeforeFullInit() {
renderer = TestRenderer(
surfaceHolder,
UserStyleRepository(UserStyleSchema(emptyList())),
@@ -2036,7 +1966,7 @@
}
@Test
- fun watchStateObservableWatchDataMembersHaveValues() {
+ public fun watchStateObservableWatchDataMembersHaveValues() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
@@ -2062,7 +1992,7 @@
}
@Test
- fun setIsBatteryLowAndNotChargingFromBatteryStatus() {
+ public fun setIsBatteryLowAndNotChargingFromBatteryStatus() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
@@ -2116,7 +2046,7 @@
}
@Test
- fun isAmbientInitalisedEvenWithoutPropertiesSent() {
+ public fun isAmbientInitalisedEvenWithoutPropertiesSent() {
userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
complicationsManager = ComplicationsManager(emptyList(), userStyleRepository)
renderer = TestRenderer(
@@ -2146,7 +2076,7 @@
}
@Test
- fun onDestroy_clearsInstanceRecord() {
+ public fun onDestroy_clearsInstanceRecord() {
val instanceId = "interactiveInstanceId"
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
@@ -2171,7 +2101,7 @@
}
@Test
- fun renderParameters_secondaryConstructor() {
+ public fun renderParameters_secondaryConstructor() {
val params = RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
@@ -2188,7 +2118,7 @@
}
@Test
- fun renderParameters_secondaryConstructorEnforcesNoDrawOutlined() {
+ public fun renderParameters_secondaryConstructorEnforcesNoDrawOutlined() {
try {
RenderParameters(
DrawMode.INTERACTIVE,