Merge "Ruler API Feedback" into androidx-main
diff --git a/autofill/autofill/src/main/java/androidx/autofill/DeleteMe.kt b/autofill/autofill/src/main/java/androidx/autofill/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/autofill/autofill/src/main/java/androidx/autofill/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
index 1e43fc2..0025f4c 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
@@ -23,8 +23,8 @@
import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BENCHMARK_PREFIX
import androidx.baselineprofile.gradle.utils.Dependencies
-import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_REQUIRED
-import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED
+import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
+import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED_INCLUSIVE
import androidx.baselineprofile.gradle.utils.camelCase
import androidx.baselineprofile.gradle.utils.copyBuildTypeSources
import androidx.baselineprofile.gradle.utils.createExtendedBuildTypes
@@ -53,8 +53,8 @@
AgpPluginId.ID_ANDROID_APPLICATION_PLUGIN,
AgpPluginId.ID_ANDROID_LIBRARY_PLUGIN
),
- minAgpVersion = MIN_AGP_VERSION_REQUIRED,
- maxAgpVersion = MAX_AGP_VERSION_REQUIRED
+ minAgpVersionInclusive = MIN_AGP_VERSION_REQUIRED_INCLUSIVE,
+ maxAgpVersionExclusive = MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
) {
private val ApplicationExtension.debugSigningConfig
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
index 0f3480d..217841b 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
@@ -31,8 +31,8 @@
import androidx.baselineprofile.gradle.utils.INTERMEDIATES_BASE_FOLDER
import androidx.baselineprofile.gradle.utils.KOTLIN_MULTIPLATFORM_PLUGIN_ID
import androidx.baselineprofile.gradle.utils.KotlinMultiPlatformUtils
-import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_REQUIRED
-import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED
+import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
+import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED_INCLUSIVE
import androidx.baselineprofile.gradle.utils.R8Utils
import androidx.baselineprofile.gradle.utils.RELEASE
import androidx.baselineprofile.gradle.utils.camelCase
@@ -64,8 +64,8 @@
AgpPluginId.ID_ANDROID_APPLICATION_PLUGIN,
AgpPluginId.ID_ANDROID_LIBRARY_PLUGIN
),
- minAgpVersion = MIN_AGP_VERSION_REQUIRED,
- maxAgpVersion = MAX_AGP_VERSION_REQUIRED
+ minAgpVersionInclusive = MIN_AGP_VERSION_REQUIRED_INCLUSIVE,
+ maxAgpVersionExclusive = MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
) {
// List of the non debuggable build types
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index a7ef11b..33347c5 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -34,8 +34,8 @@
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_TARGET_PACKAGE_NAME
import androidx.baselineprofile.gradle.utils.InstrumentationTestRunnerArgumentsAgp82
-import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_REQUIRED
-import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED
+import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
+import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED_INCLUSIVE
import androidx.baselineprofile.gradle.utils.RELEASE
import androidx.baselineprofile.gradle.utils.TestedApksAgp83
import androidx.baselineprofile.gradle.utils.camelCase
@@ -64,8 +64,8 @@
private class BaselineProfileProducerAgpPlugin(private val project: Project) : AgpPlugin(
project = project,
supportedAgpPlugins = setOf(AgpPluginId.ID_ANDROID_TEST_PLUGIN),
- minAgpVersion = MIN_AGP_VERSION_REQUIRED,
- maxAgpVersion = MAX_AGP_VERSION_REQUIRED
+ minAgpVersionInclusive = MIN_AGP_VERSION_REQUIRED_INCLUSIVE,
+ maxAgpVersionExclusive = MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
) {
companion object {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index ea70bdf..ae1c025 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -46,8 +46,8 @@
internal abstract class AgpPlugin(
private val project: Project,
private val supportedAgpPlugins: Set<AgpPluginId>,
- private val minAgpVersion: AndroidPluginVersion,
- private val maxAgpVersion: AndroidPluginVersion,
+ private val minAgpVersionInclusive: AndroidPluginVersion,
+ private val maxAgpVersionExclusive: AndroidPluginVersion,
) {
protected val logger: Logger
@@ -93,7 +93,7 @@
private fun configureWithAndroidPlugin() {
- checkAgpVersion(min = minAgpVersion, max = maxAgpVersion)
+ checkAgpVersion()
onBeforeFinalizeDsl()
@@ -224,22 +224,22 @@
protected fun agpVersion() = project.agpVersion()
- private fun checkAgpVersion(min: AndroidPluginVersion, max: AndroidPluginVersion) {
+ private fun checkAgpVersion() {
val agpVersion = project.agpVersion()
- if (agpVersion < min) {
+ if (agpVersion < minAgpVersionInclusive) {
throw GradleException(
"""
This version of the Baseline Profile Gradle Plugin requires the Android Gradle Plugin to be
- at least version $MIN_AGP_VERSION_REQUIRED. The current version is $agpVersion.
+ at least version $minAgpVersionInclusive. The current version is $agpVersion.
Please update your project.
""".trimIndent()
)
}
- if (agpVersion > max) {
+ if (agpVersion >= maxAgpVersionExclusive) {
logger.warn(
"""
- This version of the Baseline Profile Gradle Plugin was tested at most with the Android
- Gradle Plugin version $MAX_AGP_VERSION_REQUIRED and it may not work as intended.
+ This version of the Baseline Profile Gradle Plugin was tested with versions below Android
+ Gradle Plugin version $maxAgpVersionExclusive and it may not work as intended.
Current version is $agpVersion.
""".trimIndent()
)
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index ebe0a317..c82ead6 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -19,8 +19,8 @@
import com.android.build.api.AndroidPluginVersion
// Minimum AGP version required
-internal val MIN_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 4, 0)
+internal val MIN_AGP_VERSION_REQUIRED_INCLUSIVE = AndroidPluginVersion(8, 0, 0)
+internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 5, 0).alpha(1)
// Prefix for the build type baseline profile
internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/biometric/biometric/src/main/res/values-fa/strings.xml b/biometric/biometric/src/main/res/values-fa/strings.xml
index ac22959..6e9482f 100644
--- a/biometric/biometric/src/main/res/values-fa/strings.xml
+++ b/biometric/biometric/src/main/res/values-fa/strings.xml
@@ -32,11 +32,11 @@
<string name="fingerprint_dialog_icon_description" msgid="5462024216548165325">"نماد اثر انگشت"</string>
<string name="use_fingerprint_label" msgid="6961788485681412417">"استفاده از اثر انگشت"</string>
<string name="use_face_label" msgid="6533512708069459542">"استفاده از چهره"</string>
- <string name="use_biometric_label" msgid="6524145989441579428">"استفاده از زیستسنجشی"</string>
+ <string name="use_biometric_label" msgid="6524145989441579428">"استفاده از دادههای زیستسنجشی"</string>
<string name="use_screen_lock_label" msgid="5459869335976243512">"از قفل صفحه استفاده کنید"</string>
<string name="use_fingerprint_or_screen_lock_label" msgid="7577690399303139443">"استفاده از اثر انگشت یا قفل صفحه"</string>
<string name="use_face_or_screen_lock_label" msgid="2116180187159450292">"استفاده از قفل صفحه یا چهره"</string>
- <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"استفاده از زیستسنجشی یا قفل صفحه"</string>
+ <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"استفاده از دادههای زیستسنجشی یا قفل صفحه"</string>
<string name="fingerprint_prompt_message" msgid="7449360011861769080">"برای ادامه، از اثر انگشتتان استفاده کنید"</string>
<string name="face_prompt_message" msgid="2282389249605674226">"برای ادامه، از چهرهتان استفاده کنید"</string>
<string name="biometric_prompt_message" msgid="1160635338192065472">"برای ادامه، از زیستسنجشی استفاده کنید"</string>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index ea50cd9..6e64208 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -45,7 +45,7 @@
import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnJvmCompilation
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
-import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.HasUnitTestBuilder
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
@@ -688,34 +688,34 @@
}
}
- private fun HasAndroidTest.configureTests() {
- configureLicensePackaging()
- androidTest?.packaging?.resources?.apply { excludeVersionFiles(this) }
- }
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
+ private fun HasDeviceTests.configureTests() {
+ deviceTests.forEach { deviceTest ->
+ deviceTest.packaging.resources.apply {
+ excludeVersionFiles(this)
- @Suppress("UnstableApiUsage") // usage of experimentalProperties
- private fun Variant.artRewritingWorkaround() {
- // b/279234807
- experimentalProperties.put("android.experimental.art-profile-r8-rewriting", false)
- }
-
- @Suppress("UnstableApiUsage") // usage of experimentalProperties
- private fun Variant.aotCompileMicrobenchmarks(project: Project) {
- if (project.hasBenchmarkPlugin()) {
- experimentalProperties.put("android.experimental.force-aot-compilation", true)
+ // Workaround a limitation in AGP that fails to merge these META-INF license files.
+ pickFirsts.add("/META-INF/AL2.0")
+ // In addition to working around the above issue, we exclude the LGPL2.1 license as
+ // we're
+ // approved to distribute code via AL2.0 and the only dependencies which pull in LGPL2.1
+ // are currently dual-licensed with AL2.0 and LGPL2.1. The affected dependencies are:
+ // - net.java.dev.jna:jna:5.5.0
+ excludes.add("/META-INF/LGPL2.1")
+ }
}
}
- private fun HasAndroidTest.configureLicensePackaging() {
- androidTest?.packaging?.resources?.apply {
- // Workaround a limitation in AGP that fails to merge these META-INF license files.
- pickFirsts.add("/META-INF/AL2.0")
- // In addition to working around the above issue, we exclude the LGPL2.1 license as
- // we're
- // approved to distribute code via AL2.0 and the only dependencies which pull in LGPL2.1
- // are currently dual-licensed with AL2.0 and LGPL2.1. The affected dependencies are:
- // - net.java.dev.jna:jna:5.5.0
- excludes.add("/META-INF/LGPL2.1")
+ private fun Variant.artRewritingWorkaround() {
+ // b/279234807
+ @Suppress("UnstableApiUsage") // usage of experimentalProperties
+ experimentalProperties.put("android.experimental.art-profile-r8-rewriting", false)
+ }
+
+ private fun Variant.aotCompileMicrobenchmarks(project: Project) {
+ if (project.hasBenchmarkPlugin()) {
+ @Suppress("UnstableApiUsage") // usage of experimentalProperties
+ experimentalProperties.put("android.experimental.force-aot-compilation", true)
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
index ca5e6cb..53b5d51 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
@@ -21,7 +21,7 @@
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.BuiltArtifactsLoader
-import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import javax.inject.Inject
import org.gradle.api.DefaultTask
@@ -200,33 +200,33 @@
"ftlCoreTelecomDeviceSet" to listOf(NEXUS_6P, A10, PETTYL, HWCOR, Q2Q),
)
+internal fun Project.registerRunner(
+ name: String,
+ artifacts: Artifacts,
+ namespace: Provider<String>
+) {
+ devicesToRunOn.forEach { (taskPrefix, model) ->
+ tasks.register("$taskPrefix$name", FtlRunner::class.java) { task ->
+ task.device.set(model)
+ task.apkPackageName.set(namespace)
+ task.testFolder.set(artifacts.get(SingleArtifact.APK))
+ task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+ }
+ }
+}
+
fun Project.configureFtlRunner(androidComponentsExtension: AndroidComponentsExtension<*, *, *>) {
androidComponentsExtension.apply {
onVariants { variant ->
- var name: String? = null
- var artifacts: Artifacts? = null
- var apkPackageName: Provider<String>? = null
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
when {
- variant is HasAndroidTest -> {
- name = variant.androidTest?.name
- artifacts = variant.androidTest?.artifacts
- apkPackageName = variant.androidTest?.namespace
+ variant is HasDeviceTests -> {
+ variant.deviceTests.forEach { deviceTest ->
+ registerRunner(deviceTest.name, deviceTest.artifacts, deviceTest.namespace)
+ }
}
project.plugins.hasPlugin("com.android.test") -> {
- name = variant.name
- artifacts = variant.artifacts
- apkPackageName = variant.namespace
- }
- }
- if (name == null || artifacts == null || apkPackageName == null) {
- return@onVariants
- }
- devicesToRunOn.forEach { (taskPrefix, model) ->
- tasks.register("$taskPrefix$name", FtlRunner::class.java) { task ->
- task.device.set(model)
- task.apkPackageName.set(apkPackageName)
- task.testFolder.set(artifacts.get(SingleArtifact.APK))
- task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+ registerRunner(variant.name, variant.artifacts, variant.namespace)
}
}
}
@@ -235,16 +235,9 @@
fun Project.configureFtlRunner(componentsExtension: KotlinMultiplatformAndroidComponentsExtension) {
componentsExtension.onVariant { variant ->
- val name = variant.androidTest?.name ?: return@onVariant
- val artifacts = variant.androidTest?.artifacts ?: return@onVariant
- val apkPackageName = variant.androidTest?.namespace ?: return@onVariant
- devicesToRunOn.forEach { (taskPrefix, model) ->
- tasks.register("$taskPrefix$name", FtlRunner::class.java) { task ->
- task.device.set(model)
- task.apkPackageName.set(apkPackageName)
- task.testFolder.set(artifacts.get(SingleArtifact.APK))
- task.testLoader.set(artifacts.getBuiltArtifactsLoader())
- }
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
+ variant.deviceTests.forEach { deviceTest ->
+ registerRunner(deviceTest.name, deviceTest.artifacts, deviceTest.namespace)
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
index 92eb1f6..8d3766c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
@@ -17,7 +17,8 @@
package androidx.build.clang
import androidx.build.androidExtension
-import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.HasDeviceTests
+import com.android.build.api.variant.SourceDirectories
import com.android.utils.appendCapitalized
import org.gradle.api.Project
import org.gradle.kotlin.dsl.get
@@ -84,35 +85,43 @@
variantBuildType
)
) { variant ->
- val jniLibsSources = if (forTest) {
- check(variant is HasAndroidTest) {
- "Variant $variant does not have a test target"
+ fun setup(name: String, jniLibsSources: SourceDirectories.Layered?) {
+ checkNotNull(jniLibsSources) {
+ "Cannot find jni libs sources for variant: " +
+ "$variant($variantBuildType / $forTest)"
}
- variant.androidTest?.sources?.jniLibs
- } else {
- variant.sources.jniLibs
- }
- checkNotNull(jniLibsSources) {
- "Cannot find jni libs sources for variant: $variant($variantBuildType / $forTest)"
- }
- val combineTask = project.tasks.register(
- "createJniLibsDirectoryFor".appendCapitalized(
- nativeCompilation.archiveName,
- if (forTest) "forTest" else "forMain",
- androidTarget.name
- ),
- CombineObjectFilesTask::class.java
- )
- combineTask.configureFrom(nativeCompilation) {
- it.family == Family.ANDROID
+ val combineTask = project.tasks.register(
+ "createJniLibsDirectoryFor".appendCapitalized(
+ nativeCompilation.archiveName,
+ "for",
+ name,
+ androidTarget.name
+ ),
+ CombineObjectFilesTask::class.java
+ )
+ combineTask.configureFrom(nativeCompilation) {
+ it.family == Family.ANDROID
+ }
+
+ jniLibsSources.addGeneratedSourceDirectory(
+ taskProvider = combineTask,
+ wiredWith = {
+ it.outputDirectory
+ }
+ )
}
- jniLibsSources.addGeneratedSourceDirectory(
- taskProvider = combineTask,
- wiredWith = {
- it.outputDirectory
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
+ if (forTest) {
+ check(variant is HasDeviceTests) {
+ "Variant $variant does not have a test target"
}
- )
+ variant.deviceTests.forEach { deviceTest ->
+ setup(deviceTest.name, deviceTest.sources.jniLibs)
+ }
+ } else {
+ setup(variant.name, variant.sources.jniLibs)
+ }
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index d7417a4..3f18ee4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -111,11 +111,9 @@
fun Project.getMetalavaClasspath(): FileCollection {
val configuration =
- configurations.findByName("metalava")
- ?: configurations.create("metalava") {
- it.dependencies.add(dependencies.create(getLibraryByName("metalava")))
- it.isCanBeConsumed = false
- }
+ configurations.detachedConfiguration(
+ dependencies.create(getLibraryByName("metalava"))
+ )
return project.files(configuration)
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index d38bad3..2d777b8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -30,10 +30,9 @@
import com.android.build.api.artifact.Artifacts
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.KotlinMultiplatformAndroidTarget
-import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnDeviceCompilation
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
-import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.TestAndroidComponentsExtension
@@ -64,8 +63,8 @@
variantName: String,
artifacts: Artifacts,
minSdk: Int,
- testRunner: String,
- instrumentationRunnerArgs: Map<String, String>
+ testRunner: Provider<String>,
+ instrumentationRunnerArgs: Provider<Map<String, String>>
) {
val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
@@ -115,12 +114,7 @@
task.outputJson.set(getFileInTestConfigDirectory(jsonName))
task.presubmit.set(isPresubmitBuild())
task.instrumentationArgs.putAll(instrumentationRunnerArgs)
- // Disable work tests on < API 18: b/178127496
- if (path.startsWith(":work:")) {
- task.minSdk.set(maxOf(18, minSdk))
- } else {
- task.minSdk.set(minSdk)
- }
+ task.minSdk.set(minSdk)
val hasBenchmarkPlugin = hasBenchmarkPlugin()
task.hasBenchmarkPlugin.set(hasBenchmarkPlugin)
task.testRunner.set(testRunner)
@@ -172,6 +166,7 @@
}
}
+ // Migrate away when b/280680434 is fixed.
// For tests modules, the instrumentation apk is pulled from the <variant>TestedApks
// configuration. Note that also the associated test configuration task name is different
// from the application one.
@@ -290,7 +285,7 @@
variantName: String,
artifacts: Artifacts,
minSdk: Int,
- testRunner: String,
+ testRunner: Provider<String>,
) {
val mediaTask = getOrCreateMediaTestConfigTask(this)
@@ -404,40 +399,44 @@
}
}
+@Suppress("UnstableApiUsage") // usage of HasDeviceTests
fun Project.configureTestConfigGeneration(baseExtension: BaseExtension) {
extensions.getByType(AndroidComponentsExtension::class.java).apply {
onVariants { variant ->
- var name: String? = null
- var artifacts: Artifacts? = null
when {
- variant is HasAndroidTest -> {
- name = variant.androidTest?.name
- artifacts = variant.androidTest?.artifacts
+ variant is HasDeviceTests -> {
+ variant.deviceTests.forEach { deviceTest ->
+ when {
+ path.contains("media:version-compat-tests:") -> {
+ createOrUpdateMediaTestConfigurationGenerationTask(
+ deviceTest.name,
+ deviceTest.artifacts,
+ // replace minSdk after b/328495232 is fixed
+ baseExtension.defaultConfig.minSdk!!,
+ deviceTest.instrumentationRunner,
+ )
+ }
+ else -> {
+ createTestConfigurationGenerationTask(
+ deviceTest.name,
+ deviceTest.artifacts,
+ // replace minSdk after b/328495232 is fixed
+ baseExtension.defaultConfig.minSdk!!,
+ deviceTest.instrumentationRunner,
+ deviceTest.instrumentationRunnerArguments
+ )
+ }
+ }
+ }
}
project.plugins.hasPlugin("com.android.test") -> {
- name = variant.name
- artifacts = variant.artifacts
- }
- }
- if (name == null || artifacts == null) {
- return@onVariants
- }
- when {
- path.contains("media:version-compat-tests:") -> {
- createOrUpdateMediaTestConfigurationGenerationTask(
- name,
- artifacts,
- baseExtension.defaultConfig.minSdk!!,
- baseExtension.defaultConfig.testInstrumentationRunner!!,
- )
- }
- else -> {
createTestConfigurationGenerationTask(
- name,
- artifacts,
+ variant.name,
+ variant.artifacts,
+ // replace minSdk after b/328495232 is fixed
baseExtension.defaultConfig.minSdk!!,
- baseExtension.defaultConfig.testInstrumentationRunner!!,
- baseExtension.defaultConfig.testInstrumentationRunnerArguments
+ provider { baseExtension.defaultConfig.testInstrumentationRunner!! },
+ provider { baseExtension.defaultConfig.testInstrumentationRunnerArguments }
)
}
}
@@ -450,17 +449,15 @@
componentsExtension: KotlinMultiplatformAndroidComponentsExtension
) {
componentsExtension.onVariant { variant ->
- val name = variant.androidTest?.name ?: return@onVariant
- val artifacts = variant.androidTest?.artifacts ?: return@onVariant
- kotlinMultiplatformAndroidTarget.compilations.withType(
- KotlinMultiplatformAndroidTestOnDeviceCompilation::class.java
- ) {
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
+ variant.deviceTests.forEach { deviceTest ->
createTestConfigurationGenerationTask(
- name,
- artifacts,
+ deviceTest.name,
+ deviceTest.artifacts,
+ // replace minSdk after b/328495232 is fixed
kotlinMultiplatformAndroidTarget.minSdk!!,
- it.instrumentationRunner!!,
- mapOf()
+ deviceTest.instrumentationRunner,
+ deviceTest.instrumentationRunnerArguments,
)
}
}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
index 8d29c124..1f3e69a 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
@@ -21,7 +21,7 @@
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.BuiltArtifactsLoader
-import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.HasDeviceTests
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.Project
@@ -76,29 +76,27 @@
fun setupTestApkCopy(project: Project) {
project.extensions.getByType(AndroidComponentsExtension::class.java).apply {
onVariants { variant ->
- var name: String? = null
- var artifacts: Artifacts? = null
+ fun registerAndAddToBuildOnServer(name: String, artifacts: Artifacts) {
+ val apkCopy =
+ project.tasks.register("copyTestApk$name", ApkCopyTask::class.java) { task ->
+ task.apkFolder.set(artifacts.get(SingleArtifact.APK))
+ task.apkLoader.set(artifacts.getBuiltArtifactsLoader())
+ val file = "apks/${project.path.substring(1).replace(':', '-')}-$name.apk"
+ task.outputApk.set(File(project.getDistributionDirectory(), file))
+ }
+ project.addToBuildOnServer(apkCopy)
+ }
+ @Suppress("UnstableApiUsage") // usage of HasDeviceTests
when {
- variant is HasAndroidTest -> {
- name = variant.androidTest?.name
- artifacts = variant.androidTest?.artifacts
+ variant is HasDeviceTests -> {
+ variant.deviceTests.forEach { deviceTest ->
+ registerAndAddToBuildOnServer(deviceTest.name, deviceTest.artifacts)
+ }
}
project.plugins.hasPlugin("com.android.test") -> {
- name = variant.name
- artifacts = variant.artifacts
+ registerAndAddToBuildOnServer(variant.name, variant.artifacts)
}
}
- if (name == null || artifacts == null) {
- return@onVariants
- }
- val apkCopy =
- project.tasks.register("copyTestApk", ApkCopyTask::class.java) { task ->
- task.apkFolder.set(artifacts.get(SingleArtifact.APK))
- task.apkLoader.set(artifacts.getBuiltArtifactsLoader())
- val file = "apks/${project.path.substring(1).replace(':', '-')}-$name.apk"
- task.outputApk.set(File(project.getDistributionDirectory(), file))
- }
- project.addToBuildOnServer(apkCopy)
}
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index 6a7c736..189497c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -36,6 +36,9 @@
import com.google.common.util.concurrent.ListenableFuture
import javax.inject.Inject
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
internal val cameraAdapterIds = atomic(0)
@@ -88,7 +91,9 @@
}
override fun release(): ListenableFuture<Void> {
- return threads.scope.launch { useCaseManager.close() }.asListenableFuture()
+ return threads.scope.launch { useCaseManager.close() }.asListenableFuture().apply {
+ addListener({ threads.scope.cancel() }, Dispatchers.Default.asExecutor())
+ }
}
override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
index 20b4dfc..04d93f1 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
@@ -46,6 +46,9 @@
if (ControlZoomRatioRangeAssertionErrorQuirk.isEnabled()) {
quirks.add(ControlZoomRatioRangeAssertionErrorQuirk())
}
+ if (DisableAbortCapturesOnStopWithSessionProcessorQuirk.isEnabled()) {
+ quirks.add(DisableAbortCapturesOnStopWithSessionProcessorQuirk())
+ }
if (FlashAvailabilityBufferUnderflowQuirk.isEnabled()) {
quirks.add(FlashAvailabilityBufferUnderflowQuirk())
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DisableAbortCapturesOnStopWithSessionProcessorQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DisableAbortCapturesOnStopWithSessionProcessorQuirk.kt
new file mode 100644
index 0000000..c84cf3b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DisableAbortCapturesOnStopWithSessionProcessorQuirk.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCaptureSession
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.SessionProcessor
+
+/**
+ * A quirk to not abort captures on stop during [SessionProcessor] sessions on certain platforms.
+ *
+ * QuirkSummary
+ * - Bug Id: 325088903
+ * - Description: When stopping a capture session, calling [CameraCaptureSession.abortCaptures] may
+ * cause [SessionProcessor.deInitSession] to hang indefinitely. By default, CameraPipe aborts
+ * captures on stop to speed up switching between capture sessions. With this quirk, this behavior
+ * is disabled on devices from selected vendors.
+ * - Device(s): Devices on devices from selected vendors.
+ *
+ * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class DisableAbortCapturesOnStopWithSessionProcessorQuirk : Quirk {
+ companion object {
+ fun isEnabled(): Boolean {
+ return Build.BRAND.equals("SAMSUNG", ignoreCase = true)
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
index 04cffc6..db8129e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
@@ -23,7 +23,6 @@
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraStream
-import androidx.camera.camera2.pipe.compat.CameraPipeKeys
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.adapter.RequestProcessorAdapter
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
@@ -60,8 +59,42 @@
) {
private val lock = Any()
+ enum class State {
+ /**
+ * [CREATED] is the initial state, and indicates that the [SessionProcessorManager] has
+ * been created but not initialized yet.
+ */
+ CREATED,
+
+ /**
+ * [INITIALIZED] indicates that the [SessionProcessor] has been initialized and we've
+ * received the updated session configurations. See also:
+ * [SessionProcessor.deInitSession].
+ */
+ INITIALIZED,
+
+ /**
+ * [STARTED] indicates that we've provided our
+ * [androidx.camera.core.impl.RequestProcessor] implementation to [SessionProcessor].
+ * See also [SessionProcessor.onCaptureSessionStart].
+ */
+ STARTED,
+
+ /**
+ * [CLOSING] indicates that we're ending our capture session, and we'll no longer accept
+ * any further capture requests. See also: [SessionProcessor.onCaptureSessionEnd].
+ */
+ CLOSING,
+
+ /**
+ * [CLOSED] indicates that the underlying capture session has been completely closed
+ * and we've de-initialized the session. See also: [SessionProcessor.deInitSession].
+ */
+ CLOSED,
+ }
+
@GuardedBy("lock")
- private var captureSessionStarted = false
+ private var state: State = State.CREATED
@GuardedBy("lock")
private var sessionOptions = CaptureRequestOptions.Builder().build()
@@ -82,32 +115,39 @@
internal var sessionConfig: SessionConfig? = null
set(value) = synchronized(lock) {
field = checkNotNull(value)
- if (!captureSessionStarted) return
+ if (state != State.STARTED) return
checkNotNull(requestProcessor).sessionConfig = value
sessionOptions =
CaptureRequestOptions.Builder.from(value.implementationOptions).build()
updateOptions()
}
+ internal fun isClosed() = synchronized(lock) {
+ state == State.CLOSED || state == State.CLOSING
+ }
+
internal fun initialize(
useCaseManager: UseCaseManager,
useCases: List<UseCase>,
timeoutMillis: Long = 5_000L,
+ configure: (UseCaseManager.Companion.UseCaseManagerConfig?) -> Unit,
) = scope.launch {
val sessionConfigAdapter = SessionConfigAdapter(useCases, null)
val deferrableSurfaces = sessionConfigAdapter.deferrableSurfaces
val surfaces = getSurfaces(deferrableSurfaces, timeoutMillis)
- if (!isActive) return@launch
+ if (!isActive) return@launch configure(null)
if (surfaces.isEmpty()) {
- Log.error { "Surface list is empty" }
- return@launch
+ Log.error { "Cannot initialize ${this@SessionProcessorManager}: Surface list is empty" }
+ return@launch configure(null)
}
if (surfaces.contains(null)) {
- Log.error { "Some Surfaces are invalid!" }
+ Log.error {
+ "Cannot initialize ${this@SessionProcessorManager}: Some Surfaces are invalid!"
+ }
sessionConfigAdapter.reportSurfaceInvalid(
deferrableSurfaces[surfaces.indexOf(null)]
)
- return@launch
+ return@launch configure(null)
}
var previewOutputSurface: OutputSurface? = null
var captureOutputSurface: OutputSurface? = null
@@ -146,28 +186,40 @@
"postviewOutputSurface = $postviewOutputSurface"
}
- try {
- DeferrableSurfaces.incrementAll(deferrableSurfaces)
- postviewDeferrableSurface?.incrementUseCount()
- } catch (exception: DeferrableSurface.SurfaceClosedException) {
- sessionConfigAdapter.reportSurfaceInvalid(exception.deferrableSurface)
- return@launch
- }
- val processorSessionConfig = try {
- sessionProcessor.initSession(
- cameraInfoInternal,
- OutputSurfaceConfiguration.create(
- previewOutputSurface!!,
- captureOutputSurface!!,
- analysisOutputSurface,
- postviewOutputSurface,
- ),
- )
- } catch (throwable: Throwable) {
- Log.error(throwable) { "initSession() failed" }
- DeferrableSurfaces.decrementAll(deferrableSurfaces)
- postviewDeferrableSurface?.decrementUseCount()
- throw throwable
+ // IMPORTANT: The critical section (covered by synchronized) is intentionally expanded to
+ // cover the sections where we increment and decrement (on failure) the use count on the
+ // DeferrableSurfaces. This is needed because the SessionProcessorManager could be closed
+ // while we're still initializing, and we need to make sure we either initialize to a point
+ // where all the lifetimes of Surfaces are setup or we don't initialize at all beyond this
+ // point.
+ val processorSessionConfig = synchronized(lock) {
+ if (isClosed()) return@launch configure(null)
+ try {
+ DeferrableSurfaces.incrementAll(deferrableSurfaces)
+ postviewDeferrableSurface?.incrementUseCount()
+ } catch (exception: DeferrableSurface.SurfaceClosedException) {
+ sessionConfigAdapter.reportSurfaceInvalid(exception.deferrableSurface)
+ return@launch configure(null)
+ }
+ try {
+ Log.debug { "Invoking $sessionProcessor SessionProcessor#initSession" }
+ sessionProcessor.initSession(
+ cameraInfoInternal,
+ OutputSurfaceConfiguration.create(
+ previewOutputSurface!!,
+ captureOutputSurface!!,
+ analysisOutputSurface,
+ postviewOutputSurface,
+ ),
+ ).also {
+ state = State.INITIALIZED
+ }
+ } catch (throwable: Throwable) {
+ Log.error(throwable) { "initSession() failed" }
+ DeferrableSurfaces.decrementAll(deferrableSurfaces)
+ postviewDeferrableSurface?.decrementUseCount()
+ throw throwable
+ }
}
// DecrementAll the output surfaces when ProcessorSurface terminates.
@@ -183,7 +235,7 @@
val cameraGraphConfig = useCaseManager.createCameraGraphConfig(
processorSessionConfigAdapter,
streamConfigMap,
- mapOf(CameraPipeKeys.ignore3ARequiredParameters to true)
+ isExtensions = true,
)
val useCaseManagerConfig = UseCaseManager.Companion.UseCaseManagerConfig(
@@ -193,25 +245,30 @@
streamConfigMap,
)
- useCaseManager.tryResumeUseCaseManager(useCaseManagerConfig)
+ return@launch configure(useCaseManagerConfig)
}
internal fun onCaptureSessionStart(requestProcessor: RequestProcessorAdapter) {
var captureConfigsToIssue: List<CaptureConfig>?
var captureCallbacksToIssue: List<CaptureCallback>?
synchronized(lock) {
- check(!captureSessionStarted)
+ if (state != State.INITIALIZED) {
+ Log.warn { "onCaptureSessionStart called on an uninitialized extensions session" }
+ return
+ }
requestProcessor.sessionConfig = sessionConfig
this.requestProcessor = requestProcessor
- captureSessionStarted = true
captureConfigsToIssue = pendingCaptureConfigs
captureCallbacksToIssue = pendingCaptureCallbacks
pendingCaptureConfigs = null
pendingCaptureCallbacks = null
+
+ Log.debug { "Invoking SessionProcessor#onCaptureSessionStart" }
+ sessionProcessor.onCaptureSessionStart(requestProcessor)
+
+ state = State.STARTED
}
- Log.debug { "Invoking SessionProcessor#onCaptureSessionStart" }
- sessionProcessor.onCaptureSessionStart(requestProcessor)
startRepeating(object : CaptureCallback {})
captureConfigsToIssue?.let { captureConfigs ->
submitCaptureConfigs(captureConfigs, checkNotNull(captureCallbacksToIssue))
@@ -220,31 +277,38 @@
internal fun startRepeating(captureCallback: CaptureCallback) {
synchronized(lock) {
- if (!captureSessionStarted) return
+ if (state != State.STARTED) return
+ Log.debug { "Invoking SessionProcessor#startRepeating" }
+ sessionProcessor.startRepeating(captureCallback)
}
- Log.debug { "Invoking SessionProcessor#startRepeating" }
- sessionProcessor.startRepeating(captureCallback)
}
internal fun stopRepeating() {
synchronized(lock) {
- if (!captureSessionStarted) return
+ if (state != State.STARTED) return
+ Log.debug { "Invoking SessionProcessor#stopRepeating" }
+ sessionProcessor.stopRepeating()
}
- Log.debug { "Invoking SessionProcessor#stopRepeating" }
- sessionProcessor.stopRepeating()
}
internal fun submitCaptureConfigs(
captureConfigs: List<CaptureConfig>,
captureCallbacks: List<CaptureCallback>,
- ) {
+ ) = synchronized(lock) {
check(captureConfigs.size == captureCallbacks.size)
- synchronized(lock) {
- if (!captureSessionStarted) {
- pendingCaptureConfigs = captureConfigs
- pendingCaptureCallbacks = captureCallbacks
- return
+ if (state != State.STARTED) {
+ // The lifetime of image capture requests is separate from the extensions lifetime.
+ // It is therefore possible for capture requests to be issued when the capture session
+ // hasn't yet started (before invoking SessionProcessor.onCaptureSessionStart). This is
+ // a copy of camera-camera2's behavior where it stores the last capture configs that
+ // weren't submitted.
+ Log.info {
+ "SessionProcessor#submitCaptureConfigs: Session not yet started. " +
+ "The capture requests will be submitted later"
}
+ pendingCaptureConfigs = captureConfigs
+ pendingCaptureCallbacks = captureCallbacks
+ return
}
for ((config, callback) in captureConfigs.zip(captureCallbacks)) {
if (config.templateType == CameraDevice.TEMPLATE_STILL_CAPTURE) {
@@ -281,10 +345,30 @@
}
}
- internal fun onCaptureSessionEnd() {
- sessionProcessor.onCaptureSessionEnd()
+ internal fun prepareClose() = synchronized(lock) {
+ if (state == State.STARTED) {
+ sessionProcessor.onCaptureSessionEnd()
+ }
+ // If we have an initialized SessionProcessor session (i.e., initSession was called), we
+ // need to make sure close() invokes deInitSession and only does it when necessary.
+ if (state == State.INITIALIZED || state == State.STARTED) {
+ state = State.CLOSING
+ } else {
+ state = State.CLOSED
+ }
}
+ internal fun close() = synchronized(lock) {
+ // These states indicate that we had previously initialized a session (but not yet
+ // de-initialized), and thus we need to de-initialize the session here.
+ if (state == State.INITIALIZED || state == State.STARTED || state == State.CLOSING) {
+ Log.debug { "Invoking $sessionProcessor SessionProcessor#deInitSession" }
+ sessionProcessor.deInitSession()
+ }
+ state = State.CLOSED
+ }
+
+ @GuardedBy("lock")
private fun updateOptions() {
val builder = Camera2ImplConfig.Builder().apply {
insertAllOptions(sessionOptions)
@@ -293,10 +377,6 @@
sessionProcessor.setParameters(builder.build())
}
- internal fun close() {
- sessionProcessor.deInitSession()
- }
-
companion object {
private suspend fun getSurfaces(
deferrableSurfaces: List<DeferrableSurface>,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 5710e9c..f802d66 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -22,6 +22,7 @@
import android.hardware.camera2.CaptureRequest
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState.GraphStateError
import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
import androidx.camera.camera2.pipe.RequestTemplate
@@ -42,7 +43,7 @@
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
internal val useCaseCameraIds = atomic(0)
@@ -99,6 +100,7 @@
) : UseCaseCamera {
private val debugId = useCaseCameraIds.incrementAndGet()
private val closed = atomic(false)
+ private lateinit var stateCollectJob: Job
override var runningUseCases = setOf<UseCase>()
set(value) {
@@ -129,12 +131,20 @@
useCaseGraphConfig.apply {
cameraStateAdapter.onGraphUpdated(graph)
}
- threads.scope.launch {
+ stateCollectJob = threads.scope.launch {
useCaseGraphConfig.apply {
graph.graphState.collect {
cameraStateAdapter.onGraphStateUpdated(graph, it)
- if (closed.value && it is GraphStateStopped) {
- cancel()
+
+ // Even if the UseCaseCamera is closed, we should still update the GraphState
+ // before cancelling the job, because it could be the last UseCaseCamera created
+ // (i.e., no new UseCaseCamera to update CameraStateAdapter that this one as
+ // stopped/closed).
+ if (closed.value &&
+ it is GraphStateStopped ||
+ it is GraphStateError
+ ) {
+ stateCollectJob.cancel()
}
// TODO: b/323614735: Technically our RequestProcessor implementation could be
@@ -168,9 +178,14 @@
threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
debug { "Closing $this" }
requestControl.close()
- sessionProcessorManager?.onCaptureSessionEnd()
+ sessionProcessorManager?.prepareClose()
useCaseGraphConfig.graph.close()
- sessionProcessorManager?.close()
+ if (sessionProcessorManager != null) {
+ useCaseGraphConfig.graph.graphState.first {
+ it is GraphStateStopped || it is GraphStateError
+ }
+ sessionProcessorManager.close()
+ }
useCaseSurfaceManager.stopAsync().await()
}
} else {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index a2447e6..8aff64a8 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -26,6 +26,7 @@
import android.os.Build
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode
import androidx.camera.camera2.pipe.CameraId
@@ -33,6 +34,7 @@
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.OutputStream
import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.compat.CameraPipeKeys
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
@@ -43,6 +45,7 @@
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnDisconnectQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnVideoQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.DisableAbortCapturesOnStopWithSessionProcessorQuirk
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
@@ -64,8 +67,10 @@
import androidx.camera.core.impl.stabilization.StabilizationMode
import javax.inject.Inject
import javax.inject.Provider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.runBlocking
/**
* This class keeps track of the currently attached and active [UseCase]'s for a specific camera.
@@ -143,6 +148,12 @@
@GuardedBy("lock")
private var deferredUseCaseManagerConfig: UseCaseManagerConfig? = null
+ @GuardedBy("lock")
+ private var pendingSessionProcessorInitialization = false
+
+ @GuardedBy("lock")
+ private val pendingUseCasesToNotifyCameraControlReady = mutableSetOf<UseCase>()
+
private val meteringRepeating by lazy {
MeteringRepeating.Builder(
cameraProperties,
@@ -187,7 +198,7 @@
* changes are identified (i.e., a new use case is added), the subsequent actions would trigger
* a recreation of the current CameraGraph if there is one.
*/
- fun attach(useCases: List<UseCase>) = synchronized(lock) {
+ fun attach(useCases: List<UseCase>): Unit = synchronized(lock) {
if (useCases.isEmpty()) {
Log.warn { "Attach [] from $this (Ignored)" }
return
@@ -209,9 +220,13 @@
}
}
- unattachedUseCases.forEach { useCase ->
- // Notify CameraControl is ready after the UseCaseCamera is created
- useCase.onCameraControlReady()
+ if (sessionProcessor != null || !shouldCreateCameraGraphImmediately) {
+ pendingUseCasesToNotifyCameraControlReady.addAll(unattachedUseCases)
+ } else {
+ unattachedUseCases.forEach { useCase ->
+ // Notify CameraControl is ready after the UseCaseCamera is created
+ useCase.onCameraControlReady()
+ }
}
}
@@ -220,7 +235,7 @@
* changes are identified (i.e., an existing use case is removed), the subsequent actions would
* trigger a recreation of the current CameraGraph.
*/
- fun detach(useCases: List<UseCase>) = synchronized(lock) {
+ fun detach(useCases: List<UseCase>): Unit = synchronized(lock) {
if (useCases.isEmpty()) {
Log.warn { "Detaching [] from $this (Ignored)" }
return
@@ -247,6 +262,7 @@
}
refreshAttachedUseCases(attachedUseCases)
}
+ pendingUseCasesToNotifyCameraControlReady.removeAll(useCases)
}
/**
@@ -305,6 +321,13 @@
@GuardedBy("lock")
private fun refreshRunningUseCases() {
+ // refreshRunningUseCases() is called after we activate, deactivate, update or have finished
+ // attaching use cases. If the SessionProcessor is still being initialized, we cannot
+ // refresh the current set of running use cases (we don't have a UseCaseCamera), but we
+ // can safely abort here, because once the SessionProcessor is initialized, we'll resume
+ // the process of creating UseCaseCamera components, finish attaching use cases and finally
+ // invoke refreshingRunningUseCases().
+ if (pendingSessionProcessorInitialization) return
val runningUseCases = getRunningUseCases()
when {
shouldAddRepeatingUseCase(runningUseCases) -> addRepeatingUseCase()
@@ -313,23 +336,36 @@
}
}
+ @OptIn(ExperimentalCoroutinesApi::class)
@GuardedBy("lock")
private fun refreshAttachedUseCases(newUseCases: Set<UseCase>) {
val useCases = newUseCases.toList()
// Close prior camera graph
- camera.let {
+ camera.let { useCaseCamera ->
_activeComponent = null
- it?.close()?.let { closingJob ->
- closingCameraJobs.add(closingJob)
- closingJob.invokeOnCompletion {
- synchronized(lock) {
- closingCameraJobs.remove(closingJob)
+ useCaseCamera?.close()?.let { closingJob ->
+ if (sessionProcessorManager != null) {
+ // If the current session was created for extensions. We need to make sure
+ // the closing procedures are done. This is needed because the same
+ // SessionProcessor instance may be reused in the next extensions session, and
+ // we need to make sure we de-initialize the current SessionProcessor session.
+ runBlocking { closingJob.join() }
+ } else {
+ closingCameraJobs.add(closingJob)
+ closingJob.invokeOnCompletion {
+ synchronized(lock) {
+ closingCameraJobs.remove(closingJob)
+ }
}
}
}
}
- sessionProcessorManager = null
+ sessionProcessorManager?.let {
+ it.close()
+ sessionProcessorManager = null
+ pendingSessionProcessorInitialization = false
+ }
// Update list of active useCases
if (useCases.isEmpty()) {
@@ -340,14 +376,45 @@
return
}
+ if (sessionProcessor != null || !shouldCreateCameraGraphImmediately) {
+ // We will need to set the UseCaseCamera to null since the new UseCaseCamera along with
+ // its respective CameraGraph configurations won't be ready until:
+ //
+ // - SessionProcessorManager finishes the initialization, _acquires the lock_, and
+ // resume UseCaseManager successfully
+ // - And/or, the UseCaseManager is ready to be resumed under concurrent camera settings.
+ for (control in allControls) {
+ control.useCaseCamera = null
+ }
+ }
+
if (sessionProcessor != null) {
Log.debug { "Setting up UseCaseManager with SessionProcessorManager" }
sessionProcessorManager = SessionProcessorManager(
sessionProcessor!!,
cameraInfoInternal.get(),
useCaseThreads.get().scope,
- ).also {
- it.initialize(this, useCases)
+ ).also { manager ->
+ pendingSessionProcessorInitialization = true
+ manager.initialize(this, useCases) { config ->
+ synchronized(lock) {
+ if (manager.isClosed()) {
+ // We've been cancelled by other use case transactions. This means the
+ // attached set of use cases have been updated in the meantime, and the
+ // UseCaseManagerConfig we have here is obsolete, so we can simply abort
+ // here.
+ return@initialize
+ }
+ if (config == null) {
+ Log.error { "Failed to initialize SessionProcessor" }
+ manager.close()
+ sessionProcessorManager = null
+ return@initialize
+ }
+ pendingSessionProcessorInitialization = false
+ this@UseCaseManager.tryResumeUseCaseManager(config)
+ }
+ }
}
return
} else {
@@ -361,10 +428,12 @@
graphConfig,
streamConfigMap
)
- tryResumeUseCaseManager(useCaseManagerConfig)
+ this.tryResumeUseCaseManager(useCaseManagerConfig)
}
}
+ @VisibleForTesting
+ @GuardedBy("lock")
internal fun tryResumeUseCaseManager(useCaseManagerConfig: UseCaseManagerConfig) {
if (!shouldCreateCameraGraphImmediately) {
deferredUseCaseManagerConfig = useCaseManagerConfig
@@ -374,12 +443,11 @@
beginComponentCreation(useCaseManagerConfig, cameraGraph)
}
- internal fun resumeDeferredComponentCreation(cameraGraph: CameraGraph) {
- val config = synchronized(lock) { deferredUseCaseManagerConfig }
- checkNotNull(config)
- beginComponentCreation(config, cameraGraph)
+ internal fun resumeDeferredComponentCreation(cameraGraph: CameraGraph) = synchronized(lock) {
+ beginComponentCreation(checkNotNull(deferredUseCaseManagerConfig), cameraGraph)
}
+ @GuardedBy("lock")
private fun beginComponentCreation(
useCaseManagerConfig: UseCaseManagerConfig,
cameraGraph: CameraGraph
@@ -416,6 +484,12 @@
refreshRunningUseCases()
}
+
+ Log.debug { "Notifying $pendingUseCasesToNotifyCameraControlReady camera control ready" }
+ for (useCase in pendingUseCasesToNotifyCameraControlReady) {
+ useCase.onCameraControlReady()
+ }
+ pendingUseCasesToNotifyCameraControlReady.clear()
}
@GuardedBy("lock")
@@ -484,7 +558,7 @@
internal fun createCameraGraphConfig(
sessionConfigAdapter: SessionConfigAdapter,
streamConfigMap: MutableMap<CameraStream.Config, DeferrableSurface>,
- defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
+ isExtensions: Boolean = false,
): CameraGraph.Config {
return Companion.createCameraGraphConfig(
sessionConfigAdapter,
@@ -494,7 +568,7 @@
cameraConfig,
cameraQuirks,
cameraGraphFlags,
- defaultParameters,
+ isExtensions,
)
}
@@ -593,7 +667,7 @@
cameraConfig: CameraConfig,
cameraQuirks: CameraQuirks,
cameraGraphFlags: CameraGraph.Flags?,
- defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
+ isExtensions: Boolean = false,
): CameraGraph.Config {
var containsVideo = false
var operatingMode = OperatingMode.NORMAL
@@ -648,7 +722,9 @@
}
}
val shouldCloseCaptureSessionOnDisconnect =
- if (CameraQuirks.isImmediateSurfaceReleaseAllowed()) {
+ if (isExtensions) {
+ true
+ } else if (CameraQuirks.isImmediateSurfaceReleaseAllowed()) {
// If we can release Surfaces immediately, we'll finalize the session when the
// camera graph is closed (through FinalizeSessionOnCloseQuirk), and thus we won't
// need to explicitly close the capture session.
@@ -664,12 +740,26 @@
}
val shouldCloseCameraDeviceOnClose =
DeviceQuirks[CloseCameraDeviceOnCameraGraphCloseQuirk::class.java] != null
+ val shouldAbortCapturesOnStop =
+ if (isExtensions &&
+ DeviceQuirks[DisableAbortCapturesOnStopWithSessionProcessorQuirk::class.java] !=
+ null
+ ) {
+ false
+ } else {
+ /**
+ * @see [CameraGraph.Flags.abortCapturesOnStop]
+ */
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+ }
val combinedFlags = cameraGraphFlags?.copy(
+ abortCapturesOnStop = shouldAbortCapturesOnStop,
quirkCloseCaptureSessionOnDisconnect = shouldCloseCaptureSessionOnDisconnect,
quirkCloseCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
)
?: CameraGraph.Flags(
+ abortCapturesOnStop = shouldAbortCapturesOnStop,
quirkCloseCaptureSessionOnDisconnect = shouldCloseCaptureSessionOnDisconnect,
quirkCloseCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
)
@@ -694,6 +784,14 @@
videoStabilizationMode = CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON
}
}
+ val defaultParameters: Map<*, Any?> = if (isExtensions) {
+ mapOf(CameraPipeKeys.ignore3ARequiredParameters to true)
+ } else {
+ emptyMap<Any, Any?>()
+ } + mapOf(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE to videoStabilizationMode)
+
+ // TODO: b/327517884 - Add a quirk to not abort captures on stop for certain OEMs during
+ // extension sessions.
// Build up a config (using TEMPLATE_PREVIEW by default)
return CameraGraph.Config(
@@ -702,9 +800,7 @@
exclusiveStreamGroups = streamGroupMap.values.toList(),
sessionMode = operatingMode,
defaultListeners = listOf(callbackMap, requestListener),
- defaultParameters = defaultParameters + mapOf(
- CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE to videoStabilizationMode
- ),
+ defaultParameters = defaultParameters,
flags = combinedFlags,
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
index 7e3c3a6..23f6c65 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
@@ -33,6 +33,7 @@
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import junit.framework.TestCase
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Rule
import org.junit.Test
@@ -150,14 +151,19 @@
class FakeTestUseCase(
config: FakeUseCaseConfig,
) : FakeUseCase(config) {
+ var cameraControlReady = false
fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
updateSessionConfig(sessionConfigBuilder.build())
notifyActive()
}
+
+ override fun onCameraControlReady() {
+ cameraControlReady = true
+ }
}
-class TestDeferrableSurface : DeferrableSurface() {
+open class TestDeferrableSurface : DeferrableSurface() {
private val surfaceTexture = SurfaceTexture(0).also {
it.setDefaultBufferSize(0, 0)
}
@@ -172,3 +178,13 @@
surfaceTexture.release()
}
}
+
+class BlockingTestDeferrableSurface : TestDeferrableSurface() {
+ private val deferred = CompletableDeferred<Surface>()
+
+ override fun provideSurface(): ListenableFuture<Surface> {
+ return deferred.asListenableFuture()
+ }
+
+ fun resume() = deferred.complete(testSurface)
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index 2b3445c..d2216fb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -176,12 +176,22 @@
return CompletableDeferred(Result3A(Result3A.Status.OK))
}
+ override suspend fun lock3AForCapture(
+ triggerAf: Boolean,
+ waitForAwb: Boolean,
+ frameLimit: Int,
+ timeLimitNs: Long
+ ): Deferred<Result3A> {
+ lock3AForCaptureSemaphore.release()
+ return CompletableDeferred(Result3A(Result3A.Status.OK))
+ }
+
override fun submit(requests: List<Request>) {
requestHandler(requests)
submitSemaphore.release()
}
- override suspend fun unlock3APostCapture(): Deferred<Result3A> {
+ override suspend fun unlock3APostCapture(cancelAf: Boolean): Deferred<Result3A> {
unlock3APostCaptureSemaphore.release()
return CompletableDeferred(Result3A(Result3A.Status.OK))
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManagerTest.kt
index db12111..f65dfdc 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManagerTest.kt
@@ -21,7 +21,6 @@
import android.os.Build
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.adapter.FakeTestUseCase
import androidx.camera.camera2.pipe.integration.adapter.RequestProcessorAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
@@ -29,35 +28,36 @@
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator
-import androidx.camera.core.CameraInfo
-import androidx.camera.core.ImageAnalysis
+import androidx.camera.camera2.pipe.integration.testing.FakeSessionProcessor
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.OutputSurfaceConfiguration
-import androidx.camera.core.impl.RequestProcessor
import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.SessionProcessor.CaptureCallback
-import androidx.camera.core.impl.SessionProcessorSurface
import androidx.camera.core.streamsharing.StreamSharing
import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
import androidx.testutils.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertNull
import kotlin.test.Test
+import kotlin.test.assertNotNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.runner.RunWith
import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+@ExperimentalCoroutinesApi
@OptIn(ExperimentalCamera2Interop::class)
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.M)
@@ -69,87 +69,7 @@
@get:Rule
val mainDispatcherRule = MainDispatcherRule(testDispatcher)
- private val fakeSessionProcessor = object : SessionProcessor {
- val previewOutputConfigId = 0
- val imageCaptureOutputConfigId = 1
- val imageAnalysisOutputConfigId = 2
-
- var lastParameters: androidx.camera.core.impl.Config? = null
- var startCapturesCount = 0
-
- override fun initSession(
- cameraInfo: CameraInfo,
- outputSurfaceConfiguration: OutputSurfaceConfiguration,
- ): SessionConfig {
- Log.debug { "$this#initSession" }
- val previewSurface = SessionProcessorSurface(
- outputSurfaceConfiguration.previewOutputSurface.surface,
- previewOutputConfigId
- ).also {
- it.setContainerClass(Preview::class.java)
- }
- val imageCaptureSurface = SessionProcessorSurface(
- outputSurfaceConfiguration.imageCaptureOutputSurface.surface,
- imageCaptureOutputConfigId
- ).also {
- it.setContainerClass(ImageCapture::class.java)
- }
- val imageAnalysisSurface =
- outputSurfaceConfiguration.imageAnalysisOutputSurface?.surface?.let { surface ->
- SessionProcessorSurface(
- surface,
- imageAnalysisOutputConfigId
- ).also {
- it.setContainerClass(ImageAnalysis::class.java)
- }
- }
- return SessionConfig.Builder().apply {
- setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
- addSurface(previewSurface)
- addSurface(imageCaptureSurface)
- imageAnalysisSurface?.let { addSurface(it) }
- }.build()
- }
-
- override fun deInitSession() {
- Log.debug { "$this#deInitSession" }
- }
-
- override fun setParameters(config: androidx.camera.core.impl.Config) {
- Log.debug { "$this#setParameters" }
- lastParameters = config
- }
-
- override fun onCaptureSessionStart(requestProcessor: RequestProcessor) {
- Log.debug { "$this#onCaptureSessionStart" }
- }
-
- override fun onCaptureSessionEnd() {
- TODO("Not yet implemented")
- }
-
- override fun startRepeating(callback: CaptureCallback): Int {
- Log.debug { "$this#startRepeating" }
- return 0
- }
-
- override fun stopRepeating() {
- Log.debug { "$this#stopRepeating" }
- }
-
- override fun startCapture(
- postviewEnabled: Boolean,
- callback: CaptureCallback
- ): Int {
- Log.debug { "$this#startCapture" }
- startCapturesCount++
- return 0
- }
-
- override fun abortCapture(captureSequenceId: Int) {
- TODO("Not yet implemented")
- }
- }
+ private val fakeSessionProcessor = FakeSessionProcessor()
private val fakeCameraId = CameraId.fromCamera2Id("0")
private val fakeCameraInfoAdapter = FakeCameraInfoAdapterCreator.createCameraInfoAdapter(
fakeCameraId
@@ -178,9 +98,12 @@
sessionProcessorManager.initialize(
useCaseManager,
listOf(fakePreviewUseCase, fakeImageCaptureUseCase)
- ).join()
- verify(useCaseManager).createCameraGraphConfig(any(), any(), any())
- verify(useCaseManager).tryResumeUseCaseManager(any())
+ ) { useCaseManagerConfig ->
+ assertNotNull(useCaseManagerConfig)
+ }
+
+ advanceUntilIdle()
+ verify(useCaseManager).createCameraGraphConfig(any(), any(), eq(true))
}
@Test
@@ -203,9 +126,12 @@
sessionProcessorManager.initialize(
useCaseManager,
listOf(fakeStreamSharingUseCase, fakeImageCaptureUseCase)
- ).join()
- verify(useCaseManager).createCameraGraphConfig(any(), any(), any())
- verify(useCaseManager).tryResumeUseCaseManager(any())
+ ) { useCaseManagerConfig ->
+ assertNotNull(useCaseManagerConfig)
+ }
+
+ advanceUntilIdle()
+ verify(useCaseManager).createCameraGraphConfig(any(), any(), eq(true))
}
@Test
@@ -228,8 +154,11 @@
sessionProcessorManager.initialize(
useCaseManager,
listOf(fakePreviewUseCase, fakeImageCaptureUseCase)
- ).join()
+ ) { useCaseManagerConfig ->
+ assertNotNull(useCaseManagerConfig)
+ }
sessionProcessorManager.sessionConfig = SessionConfig.Builder().build()
+ advanceUntilIdle()
val mockRequestProcessorAdapter: RequestProcessorAdapter = mock()
sessionProcessorManager.onCaptureSessionStart(mockRequestProcessorAdapter)
@@ -256,6 +185,33 @@
assertThat(fakeSessionProcessor.startCapturesCount).isEqualTo(1)
}
+ @Test
+ fun testSessionProcessorManagerConfiguresNullWhenClosed() = runTest {
+ val useCaseManager: UseCaseManager = mock()
+ whenever(useCaseManager.createCameraGraphConfig(any(), any(), any())).thenReturn(
+ CameraGraph.Config(fakeCameraId, emptyList())
+ )
+ val fakePreviewUseCase = createFakeTestUseCase(
+ "Preview",
+ CameraDevice.TEMPLATE_PREVIEW,
+ Preview::class.java
+ )
+ val fakeImageCaptureUseCase = createFakeTestUseCase(
+ "ImageCapture",
+ CameraDevice.TEMPLATE_STILL_CAPTURE,
+ ImageCapture::class.java
+ )
+
+ sessionProcessorManager.prepareClose()
+ sessionProcessorManager.close()
+ sessionProcessorManager.initialize(
+ useCaseManager,
+ listOf(fakePreviewUseCase, fakeImageCaptureUseCase)
+ ) { useCaseManagerConfig ->
+ assertNull(useCaseManagerConfig)
+ }
+ }
+
private fun <T> createFakeTestUseCase(
name: String,
template: Int,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 4685e28..127b304 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -18,14 +18,18 @@
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
import android.os.Build
import android.util.Size
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.adapter.BlockingTestDeferrableSurface
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
+import androidx.camera.camera2.pipe.integration.adapter.FakeTestUseCase
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.adapter.TestDeferrableSurface
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
@@ -35,25 +39,35 @@
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.testing.FakeCamera2CameraControlCompat
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
+import androidx.camera.camera2.pipe.integration.testing.FakeSessionProcessor
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraComponentBuilder
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.fakes.FakeUseCase
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import com.google.common.util.concurrent.MoreExecutors
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asCoroutineDispatcher
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import kotlin.test.assertFalse
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
@@ -63,6 +77,7 @@
import org.robolectric.shadows.ShadowCameraManager
import org.robolectric.shadows.StreamConfigurationMapBuilder
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class UseCaseManagerTest {
@@ -72,19 +87,7 @@
}.build()
private val useCaseManagerList = mutableListOf<UseCaseManager>()
private val useCaseList = mutableListOf<UseCase>()
- private val useCaseThreads by lazy {
- val dispatcher = Dispatchers.Default
- val cameraScope = CoroutineScope(
- Job() +
- dispatcher
- )
-
- UseCaseThreads(
- cameraScope,
- dispatcher.asExecutor(),
- dispatcher
- )
- }
+ private lateinit var useCaseThreads: UseCaseThreads
@After
fun tearDown() = runBlocking {
@@ -93,8 +96,9 @@
}
@Test
- fun enabledUseCasesEmpty_whenUseCaseAttachedOnly() {
+ fun enabledUseCasesEmpty_whenUseCaseAttachedOnly() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val useCase = createPreview()
@@ -107,8 +111,9 @@
}
@Test
- fun enabledUseCasesNotEmpty_whenUseCaseEnabled() {
+ fun enabledUseCasesNotEmpty_whenUseCaseEnabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val useCase = createPreview()
useCaseManager.attach(listOf(useCase))
@@ -122,8 +127,80 @@
}
@Test
- fun meteringRepeatingNotEnabled_whenPreviewEnabled() {
+ fun attachingUseCasesWithSessionProcessor_ShouldSucceed() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val previewUseCase = createFakePreview()
+ val imageCaptureUseCase = createFakeImageCapture()
+
+ val fakeSessionProcessor: SessionProcessor = FakeSessionProcessor()
+
+ // Act
+ useCaseManager.sessionProcessor = fakeSessionProcessor
+ useCaseManager.activate(previewUseCase)
+ useCaseManager.activate(imageCaptureUseCase)
+ useCaseManager.attach(listOf(previewUseCase, imageCaptureUseCase))
+ advanceUntilIdle()
+
+ // Assert
+ assertNotNull(useCaseManager.camera)
+ assertThat(useCaseManager.camera!!.runningUseCases).containsExactly(
+ previewUseCase,
+ imageCaptureUseCase
+ )
+ }
+
+ @Test
+ fun attachingUseCases_ShouldSupersedeUseCasesPendingInitialization() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val previewDeferrableSurface = createBlockingTestDeferrableSurface(Preview::class.java)
+ val imageCaptureDeferrableSurface =
+ createBlockingTestDeferrableSurface(ImageCapture::class.java)
+ val imageAnalysisDeferrableSurface =
+ createBlockingTestDeferrableSurface(ImageAnalysis::class.java)
+ val previewUseCase = createFakePreview(previewDeferrableSurface)
+ val imageCaptureUseCase = createFakeImageCapture(imageCaptureDeferrableSurface)
+ val imageAnalysisUseCase = createFakeImageAnalysis(imageAnalysisDeferrableSurface)
+ val fakeSessionProcessor = FakeSessionProcessor()
+
+ // Act
+ useCaseManager.sessionProcessor = fakeSessionProcessor
+ useCaseManager.activate(previewUseCase)
+ useCaseManager.activate(imageCaptureUseCase)
+ useCaseManager.attach(listOf(previewUseCase, imageCaptureUseCase))
+ advanceUntilIdle()
+ // Here SessionProcessorProcessor.initialize would stall due to not getting its Surfaces.
+ // When initialization is still pending, the current UseCaseCamera should be null (i.e.,
+ // no attached or running use cases).
+ assertNull(useCaseManager.camera)
+
+ // Attaching an ImageAnalysis use case, which should refresh the attached use cases, and
+ // supersede the current set of use cases.
+ useCaseManager.activate(imageAnalysisUseCase)
+ useCaseManager.attach(listOf(imageAnalysisUseCase))
+ // Resume the DeferrableSurfaces to allow them to be retrieved.
+ previewDeferrableSurface.resume()
+ imageCaptureDeferrableSurface.resume()
+ imageAnalysisDeferrableSurface.resume()
+ advanceUntilIdle()
+
+ // Assert
+ assertNotNull(useCaseManager.camera)
+ // Check that the new set of running use cases is Preview, ImageCapture and ImageAnalysis.
+ assertThat(useCaseManager.camera!!.runningUseCases).containsExactly(
+ previewUseCase,
+ imageCaptureUseCase,
+ imageAnalysisUseCase
+ )
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenPreviewEnabled() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val preview = createPreview()
val imageCapture = createImageCapture()
@@ -139,8 +216,9 @@
}
@Test
- fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() {
+ fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
@@ -159,8 +237,9 @@
}
@Test
- fun meteringRepeatingDisabled_whenPreviewBecomesEnabled() {
+ fun meteringRepeatingDisabled_whenPreviewBecomesEnabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
@@ -177,8 +256,9 @@
}
@Test
- fun meteringRepeatingEnabled_afterAllUseCasesButImageCaptureDisabled() {
+ fun meteringRepeatingEnabled_afterAllUseCasesButImageCaptureDisabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val preview = createPreview()
val imageCapture = createImageCapture()
@@ -200,8 +280,9 @@
}
@Test
- fun onlyOneUseCaseCameraBuilt_whenAllUseCasesButImageCaptureDisabled() {
+ fun onlyOneUseCaseCameraBuilt_whenAllUseCasesButImageCaptureDisabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseCameraBuilder = FakeUseCaseCameraComponentBuilder()
val useCaseManager = createUseCaseManager(
useCaseCameraComponentBuilder = useCaseCameraBuilder
@@ -222,8 +303,9 @@
}
@Test
- fun meteringRepeatingDisabled_whenAllUseCasesDisabled() {
+ fun meteringRepeatingDisabled_whenAllUseCasesDisabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
@@ -238,8 +320,9 @@
}
@Test
- fun onlyOneUseCaseCameraBuilt_whenAllUseCasesDisabled() {
+ fun onlyOneUseCaseCameraBuilt_whenAllUseCasesDisabled() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseCameraBuilder = FakeUseCaseCameraComponentBuilder()
val useCaseManager = createUseCaseManager(
useCaseCameraComponentBuilder = useCaseCameraBuilder
@@ -258,8 +341,9 @@
}
@Test
- fun onStateAttachedInvokedExactlyOnce_whenUseCaseAttachedAndMeteringRepeatingAdded() {
+ fun onStateAttachedInvokedExactlyOnce_whenUseCaseAttachedAndMeteringRepeatingAdded() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val useCaseManager = createUseCaseManager()
val imageCapture = createImageCapture()
val useCase = FakeUseCase().also {
@@ -276,24 +360,27 @@
}
@Test
- fun onStateAttachedInvokedExactlyOnce_whenUseCaseAttachedAndMeteringRepeatingNotAdded() {
- // Arrange
- val useCaseManager = createUseCaseManager()
- val preview = createPreview()
- val useCase = FakeUseCase()
+ fun onStateAttachedInvokedExactlyOnce_whenUseCaseAttachedAndMeteringRepeatingNotAdded() =
+ runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview()
+ val useCase = FakeUseCase()
- // Act
- useCaseManager.activate(preview)
- useCaseManager.activate(useCase)
- useCaseManager.attach(listOf(preview, useCase))
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(useCase)
+ useCaseManager.attach(listOf(preview, useCase))
- // Assert
- assertThat(useCase.stateAttachedCount).isEqualTo(1)
- }
+ // Assert
+ assertThat(useCase.stateAttachedCount).isEqualTo(1)
+ }
@Test
- fun controlsNotified_whenRunningUseCasesChanged() {
+ fun controlsNotified_whenRunningUseCasesChanged() = runTest {
// Arrange
+ initializeUseCaseThreads(this)
val fakeControl = object : UseCaseCameraControl, RunningUseCasesChangeListener {
var runningUseCases: Set<UseCase> = emptySet()
@@ -309,7 +396,9 @@
override fun onRunningUseCasesChanged() {}
}
- val useCaseManager = createUseCaseManager(setOf(fakeControl))
+ val useCaseManager = createUseCaseManager(
+ controls = setOf(fakeControl)
+ )
val preview = createPreview()
val useCase = FakeUseCase()
@@ -322,6 +411,86 @@
assertThat(fakeControl.runningUseCases).isEqualTo(setOf(preview, useCase))
}
+ @Test
+ fun useCasesNotifiedOnCameraControlReady_whenAttachingWithSessionProcessor() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val previewUseCase = createFakePreview()
+ val imageCaptureUseCase = createFakeImageCapture()
+
+ val fakeSessionProcessor: SessionProcessor = FakeSessionProcessor()
+
+ useCaseManager.sessionProcessor = fakeSessionProcessor
+ useCaseManager.activate(previewUseCase)
+ useCaseManager.activate(imageCaptureUseCase)
+ useCaseManager.attach(listOf(previewUseCase, imageCaptureUseCase))
+ advanceUntilIdle()
+
+ assertNotNull(useCaseManager.camera)
+ assertThat(useCaseManager.camera!!.runningUseCases).containsExactly(
+ previewUseCase,
+ imageCaptureUseCase
+ )
+ assertTrue(previewUseCase.cameraControlReady)
+ assertTrue(imageCaptureUseCase.cameraControlReady)
+ }
+
+ @Test
+ fun allUseCasesNotifiedOnCameraControlReady_whenSessionProcessorPending() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val previewDeferrableSurface = createBlockingTestDeferrableSurface(Preview::class.java)
+ val imageCaptureDeferrableSurface =
+ createBlockingTestDeferrableSurface(ImageCapture::class.java)
+ val imageAnalysisDeferrableSurface =
+ createBlockingTestDeferrableSurface(ImageAnalysis::class.java)
+ val previewUseCase = createFakePreview(previewDeferrableSurface)
+ val imageCaptureUseCase = createFakeImageCapture(imageCaptureDeferrableSurface)
+ val imageAnalysisUseCase = createFakeImageAnalysis(imageAnalysisDeferrableSurface)
+ val fakeSessionProcessor = FakeSessionProcessor()
+
+ // Act
+ useCaseManager.sessionProcessor = fakeSessionProcessor
+ useCaseManager.activate(previewUseCase)
+ useCaseManager.activate(imageCaptureUseCase)
+ useCaseManager.attach(listOf(previewUseCase, imageCaptureUseCase))
+ advanceUntilIdle()
+ // Here SessionProcessorProcessor.initialize due to not getting its Surfaces. While we're
+ // still initializing, the current UseCaseCamera should be null (i.e., no attached or
+ // running use cases).
+ // Assert
+ assertNull(useCaseManager.camera)
+ // We haven't finished initialization, and therefore the controls aren't ready.
+ assertFalse(previewUseCase.cameraControlReady)
+ assertFalse(imageCaptureUseCase.cameraControlReady)
+
+ // Attaching an ImageAnalysis use case, which should refresh the attached use cases, and
+ // supersede the current set of use cases.
+ useCaseManager.activate(imageAnalysisUseCase)
+ useCaseManager.attach(listOf(imageAnalysisUseCase))
+ // Resume the DeferrableSurfaces to allow them to be retrieved.
+ previewDeferrableSurface.resume()
+ imageCaptureDeferrableSurface.resume()
+ imageAnalysisDeferrableSurface.resume()
+ advanceUntilIdle()
+
+ // Assert
+ assertNotNull(useCaseManager.camera)
+ // Check that the new set of running use cases is Preview, ImageCapture and ImageAnalysis.
+ assertThat(useCaseManager.camera!!.runningUseCases).containsExactly(
+ previewUseCase,
+ imageCaptureUseCase,
+ imageAnalysisUseCase
+ )
+ // Despite only attaching the ImageAnalysis use case in the prior step. All not-yet-notified
+ // use cases should be notified that their camera controls are ready.
+ assertTrue(previewUseCase.cameraControlReady)
+ assertTrue(imageCaptureUseCase.cameraControlReady)
+ assertTrue(imageAnalysisUseCase.cameraControlReady)
+ }
+
@OptIn(ExperimentalCamera2Interop::class)
@Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun createUseCaseManager(
@@ -345,17 +514,6 @@
characteristics = characteristicsMap
)
val fakeCamera = FakeCamera()
- val fakeUseCaseThreads = run {
- val executor = MoreExecutors.directExecutor()
- val dispatcher = executor.asCoroutineDispatcher()
- val cameraScope = CoroutineScope(Job() + dispatcher)
-
- UseCaseThreads(
- cameraScope,
- executor,
- dispatcher,
- )
- }
return UseCaseManager(
cameraPipe = CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext())),
cameraConfig = CameraConfig(cameraId),
@@ -369,7 +527,7 @@
),
camera2CameraControl = Camera2CameraControl.create(
FakeCamera2CameraControlCompat(),
- useCaseThreads,
+ checkNotNull(useCaseThreads),
ComboRequestListener()
),
cameraStateAdapter = CameraStateAdapter(),
@@ -382,16 +540,84 @@
displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext()),
context = ApplicationProvider.getApplicationContext(),
cameraInfoInternal = { fakeCamera.cameraInfoInternal },
- useCaseThreads = { fakeUseCaseThreads },
+ useCaseThreads = { useCaseThreads },
).also {
useCaseManagerList.add(it)
}
}
+ private fun initializeUseCaseThreads(testScope: TestScope) {
+ val dispatcher = StandardTestDispatcher(testScope.testScheduler)
+ useCaseThreads = UseCaseThreads(
+ testScope,
+ dispatcher.asExecutor(),
+ dispatcher,
+ )
+ }
+
+ private fun createFakePreview(customDeferrableSurface: DeferrableSurface? = null) =
+ createFakeTestUseCase(
+ "Preview",
+ CameraDevice.TEMPLATE_PREVIEW,
+ Preview::class.java,
+ customDeferrableSurface,
+ )
+
+ private fun createFakeImageCapture(customDeferrableSurface: DeferrableSurface? = null) =
+ createFakeTestUseCase(
+ "ImageCapture",
+ CameraDevice.TEMPLATE_STILL_CAPTURE,
+ ImageCapture::class.java,
+ customDeferrableSurface,
+ )
+
+ private fun createFakeImageAnalysis(customDeferrableSurface: DeferrableSurface? = null) =
+ createFakeTestUseCase(
+ "ImageAnalysis",
+ CameraDevice.TEMPLATE_PREVIEW,
+ ImageAnalysis::class.java,
+ customDeferrableSurface,
+ )
+
+ private fun <T> createFakeTestUseCase(
+ name: String,
+ template: Int,
+ containerClass: Class<T>,
+ customDeferrableSurface: DeferrableSurface? = null,
+ ): FakeTestUseCase {
+ val deferrableSurface =
+ customDeferrableSurface ?: createTestDeferrableSurface(containerClass)
+ return FakeTestUseCase(
+ FakeUseCaseConfig.Builder().setTargetName(name).useCaseConfig
+ ).apply {
+ setupSessionConfig(
+ SessionConfig.Builder().also { sessionConfigBuilder ->
+ sessionConfigBuilder.setTemplateType(template)
+ sessionConfigBuilder.addSurface(deferrableSurface)
+ }
+ )
+ }
+ }
+
+ private fun <T> createTestDeferrableSurface(containerClass: Class<T>): TestDeferrableSurface {
+ return TestDeferrableSurface().apply {
+ setContainerClass(containerClass)
+ terminationFuture.addListener({ cleanUp() }, useCaseThreads.backgroundExecutor)
+ }
+ }
+
+ private fun <T> createBlockingTestDeferrableSurface(containerClass: Class<T>):
+ BlockingTestDeferrableSurface {
+ return BlockingTestDeferrableSurface().apply {
+ setContainerClass(containerClass)
+ terminationFuture.addListener({ cleanUp() }, useCaseThreads.backgroundExecutor)
+ }
+ }
+
private fun createImageCapture(): ImageCapture =
ImageCapture.Builder()
- .setCaptureOptionUnpacker { _, _ -> }
- .setSessionOptionUnpacker { _, _, _ -> }
+ .setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
+ .setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
.build().also {
it.simulateActivation()
useCaseList.add(it)
@@ -399,8 +625,8 @@
private fun createPreview(): Preview =
Preview.Builder()
- .setCaptureOptionUnpacker { _, _ -> }
- .setSessionOptionUnpacker { _, _, _ -> }
+ .setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
+ .setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
.build().apply {
setSurfaceProvider(
CameraXExecutors.mainThreadExecutor(),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
index 8249625..0ac0345 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
@@ -96,6 +96,15 @@
throw NotImplementedError("Not used in testing")
}
+ override suspend fun lock3AForCapture(
+ triggerAf: Boolean,
+ waitForAwb: Boolean,
+ frameLimit: Int,
+ timeLimitNs: Long
+ ): Deferred<Result3A> {
+ throw NotImplementedError("Not used in testing")
+ }
+
override fun setTorch(torchState: TorchState): Deferred<Result3A> {
throw NotImplementedError("Not used in testing")
}
@@ -159,7 +168,7 @@
throw NotImplementedError("Not used in testing")
}
- override suspend fun unlock3APostCapture(): Deferred<Result3A> {
+ override suspend fun unlock3APostCapture(cancelAf: Boolean): Deferred<Result3A> {
throw NotImplementedError("Not used in testing")
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeSessionProcessor.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeSessionProcessor.kt
new file mode 100644
index 0000000..beff862
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeSessionProcessor.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.testing
+
+import android.hardware.camera2.CameraDevice
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.impl.OutputSurfaceConfiguration
+import androidx.camera.core.impl.RequestProcessor
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SessionProcessorSurface
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class FakeSessionProcessor : SessionProcessor {
+ val previewOutputConfigId = 0
+ val imageCaptureOutputConfigId = 1
+ val imageAnalysisOutputConfigId = 2
+
+ var lastParameters: androidx.camera.core.impl.Config? = null
+ var startCapturesCount = 0
+
+ override fun initSession(
+ cameraInfo: CameraInfo,
+ outputSurfaceConfiguration: OutputSurfaceConfiguration,
+ ): SessionConfig {
+ Log.debug { "$this#initSession" }
+ val previewSurface = SessionProcessorSurface(
+ outputSurfaceConfiguration.previewOutputSurface.surface,
+ previewOutputConfigId
+ ).also {
+ it.setContainerClass(Preview::class.java)
+ }
+ val imageCaptureSurface = SessionProcessorSurface(
+ outputSurfaceConfiguration.imageCaptureOutputSurface.surface,
+ imageCaptureOutputConfigId
+ ).also {
+ it.setContainerClass(ImageCapture::class.java)
+ }
+ val imageAnalysisSurface =
+ outputSurfaceConfiguration.imageAnalysisOutputSurface?.surface?.let { surface ->
+ SessionProcessorSurface(
+ surface,
+ imageAnalysisOutputConfigId
+ ).also {
+ it.setContainerClass(ImageAnalysis::class.java)
+ }
+ }
+ return SessionConfig.Builder().apply {
+ setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+ addSurface(previewSurface)
+ addSurface(imageCaptureSurface)
+ imageAnalysisSurface?.let { addSurface(it) }
+ }.build()
+ }
+
+ override fun deInitSession() {
+ Log.debug { "$this#deInitSession" }
+ }
+
+ override fun setParameters(config: androidx.camera.core.impl.Config) {
+ Log.debug { "$this#setParameters" }
+ lastParameters = config
+ }
+
+ override fun onCaptureSessionStart(requestProcessor: RequestProcessor) {
+ Log.debug { "$this#onCaptureSessionStart" }
+ }
+
+ override fun onCaptureSessionEnd() {
+ Log.debug { "$this#onCaptureSessionEnd" }
+ }
+
+ override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
+ Log.debug { "$this#startRepeating" }
+ return 0
+ }
+
+ override fun stopRepeating() {
+ Log.debug { "$this#stopRepeating" }
+ }
+
+ override fun startCapture(
+ postviewEnabled: Boolean,
+ callback: SessionProcessor.CaptureCallback
+ ): Int {
+ Log.debug { "$this#startCapture" }
+ startCapturesCount++
+ return 0
+ }
+
+ override fun abortCapture(captureSequenceId: Int) {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index 09c9c59..59e1fbe 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -31,6 +31,15 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmInline
value class AfMode(val value: Int) {
+ fun isOn(): Boolean {
+ return value != CameraMetadata.CONTROL_AF_MODE_OFF
+ }
+
+ fun isContinuous(): Boolean {
+ return value == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO ||
+ value == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+ }
+
companion object {
val OFF = AfMode(CameraMetadata.CONTROL_AF_MODE_OFF)
val AUTO = AfMode(CameraMetadata.CONTROL_AF_MODE_AUTO)
@@ -50,6 +59,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmInline
value class AeMode(val value: Int) {
+ fun isOn(): Boolean {
+ return value != CameraMetadata.CONTROL_AE_MODE_OFF
+ }
+
companion object {
val OFF = AeMode(CameraMetadata.CONTROL_AE_MODE_OFF)
val ON = AeMode(CameraMetadata.CONTROL_AE_MODE_ON)
@@ -70,6 +83,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmInline
value class AwbMode(val value: Int) {
+ fun isOn(): Boolean {
+ return value != CameraMetadata.CONTROL_AWB_MODE_OFF
+ }
+
companion object {
val OFF = AwbMode(CameraMetadata.CONTROL_AWB_MODE_OFF)
val AUTO = AwbMode(CameraMetadata.CONTROL_AWB_MODE_AUTO)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 8016c46..954d5ea 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -507,6 +507,7 @@
* operation to complete.
* @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
* this operation to complete.
+ *
* @return [Result3A], which will contain the latest frame number at which the locks were
* applied or the frame number at which the method returned early because either frame
* limit or time limit was reached.
@@ -514,7 +515,33 @@
suspend fun lock3AForCapture(
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
- timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
+ ): Deferred<Result3A>
+
+ /**
+ * This methods does pre-capture metering sequence and locks auto-focus. Once the operation
+ * completes, we can proceed to take high-quality pictures.
+ *
+ * Note: Flash will be used during pre-capture metering and during image capture if the AE
+ * mode was set to [AeMode.ON_AUTO_FLASH] or [AeMode.ON_ALWAYS_FLASH], thus firing it for
+ * low light captures or for every capture, respectively.
+ *
+ * @param triggerAf Whether to trigger AF, enabled by default.
+ * @param waitForAwb Whether to wait for AWB to converge/lock, disabled by default.
+ * @param frameLimit the maximum number of frames to wait before we give up waiting for this
+ * operation to complete.
+ * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
+ * this operation to complete.
+ *
+ * @return [Result3A], which will contain the latest frame number at which the locks were
+ * applied or the frame number at which the method returned early because either frame
+ * limit or time limit was reached.
+ */
+ suspend fun lock3AForCapture(
+ triggerAf: Boolean = true,
+ waitForAwb: Boolean = false,
+ frameLimit: Int = DEFAULT_FRAME_LIMIT,
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A>
/**
@@ -523,8 +550,10 @@
* capture, and if not image capture request is submitted the auto-exposure may not resume
* it's normal scan. This method brings focus and exposure back to normal after high quality
* image captures using [lock3AForCapture] method.
+ *
+ * @param cancelAf Whether to trigger AF cancel, enabled by default.
*/
- suspend fun unlock3APostCapture(): Deferred<Result3A>
+ suspend fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A>
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
index 9d21ba8..79001b0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
@@ -313,6 +313,12 @@
}
}
Debug.traceStop()
+ } else {
+ // We still need to indicate the stop signal because the graph state would transition to
+ // GraphStateStarting when the graph is being started.
+ Debug.traceStart { "$graphListener#onGraphStopped" }
+ graphListener.onGraphStopped(null)
+ Debug.traceStop()
}
var shouldFinalizeSession = false
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
index 1929570..3322589 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
@@ -65,7 +65,7 @@
internal object NoOpGraphListener : GraphListener {
override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {}
- override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {}
+ override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {}
override fun onGraphModified(requestProcessor: GraphRequestProcessor) {}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index e60758c..9433599 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -182,12 +182,31 @@
timeLimitNs: Long
): Deferred<Result3A> {
check(!closed.value) { "Cannot call lock3AForCapture on $this after close." }
- return controller3A.lock3AForCapture(lockedCondition, frameLimit, timeLimitNs)
+ return controller3A.lock3AForCapture(
+ lockedCondition,
+ frameLimit,
+ timeLimitNs
+ )
}
- override suspend fun unlock3APostCapture(): Deferred<Result3A> {
+ override suspend fun lock3AForCapture(
+ triggerAf: Boolean,
+ waitForAwb: Boolean,
+ frameLimit: Int,
+ timeLimitNs: Long
+ ): Deferred<Result3A> {
+ check(!closed.value) { "Cannot call lock3AForCapture on $this after close." }
+ return controller3A.lock3AForCapture(
+ triggerAf,
+ waitForAwb,
+ frameLimit,
+ timeLimitNs
+ )
+ }
+
+ override suspend fun unlock3APostCapture(cancelAf: Boolean): Deferred<Result3A> {
check(!closed.value) { "Cannot call unlock3APostCapture on $this after close." }
- return controller3A.unlock3APostCapture()
+ return controller3A.unlock3APostCapture(cancelAf)
}
override fun toString(): String = "CameraGraph.Session-$debugId"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 7cf11a9..83dda3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -18,7 +18,10 @@
package androidx.camera.camera2.pipe.graph
+import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_OFF
import android.hardware.camera2.CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_OFF
+import android.hardware.camera2.CameraMetadata.CONTROL_AWB_MODE_OFF
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK
import android.hardware.camera2.CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER
@@ -93,12 +96,23 @@
CaptureResult.CONTROL_AE_STATE_LOCKED
)
+ private val awbPostPrecaptureStateList =
+ listOf(
+ CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_STATE_LOCKED
+ )
+
val parameterForAfTriggerStart =
mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START)
val parameterForAfTriggerCancel =
mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL)
+ private val parametersForAePrecapture =
+ mapOf<CaptureRequest.Key<*>, Any>(
+ CONTROL_AE_PRECAPTURE_TRIGGER to CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
+
private val parametersForAePrecaptureAndAfTrigger =
mapOf<CaptureRequest.Key<*>, Any>(
CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START,
@@ -132,17 +146,18 @@
CaptureResult.CONTROL_AWB_STATE_CONVERGED
)
- private val defaultLock3AForCaptureLockCondition = mapOf<CaptureResult.Key<*>, List<Any>>(
- CaptureResult.CONTROL_AE_STATE to aePostPrecaptureStateList,
- CaptureResult.CONTROL_AF_STATE to afLockedStateList
- ).toConditionChecker()
+ private val unlock3APostCaptureLockAeParams = mapOf(CONTROL_AE_LOCK to true)
- private val unlock3APostCaptureLockAeParams =
+ private val unlock3APostCaptureLockAeAndCancelAfParams =
mapOf(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL, CONTROL_AE_LOCK to true)
private val unlock3APostCaptureUnlockAeParams =
mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false)
+ private val aePrecaptureCancelParams = mapOf<CaptureRequest.Key<*>, Any>(
+ CONTROL_AE_PRECAPTURE_TRIGGER to CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
+
private val aePrecaptureAndAfCancelParams = mapOf<CaptureRequest.Key<*>, Any>(
CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL,
CONTROL_AE_PRECAPTURE_TRIGGER to CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
@@ -416,25 +431,129 @@
return listener.result
}
+ /**
+ * Triggers 3A state updates and waits for locking/convergence for high quality image capture.
+ *
+ * By default, both AE precapture and AF are triggered, however this method does not try to lock
+ * the AE/AWB explicitly. So AE/AWB states will reach up to converged state only, not locked
+ * state (unless they were already locked). Use the [lock3A] method afterwards if locking AE/AWB
+ * is also required.
+ *
+ * The exit condition of the API can be customized with [lockedCondition] parameter. See
+ * `lock3AForCapture(triggerCondition, lockedCondition, frameLimit, timeLimitNs)` for details.
+ *
+ * @param lockedCondition Optional customized exit condition for the result.
+ *
+ * @return A [Deferred] containing a [Result3A] which will contain the latest frame number at
+ * which the locks were applied or the frame number at which the method returned early because
+ * either frame limit or time limit was reached.
+ */
suspend fun lock3AForCapture(
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
- timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
+ ) = lock3AForCapture(
+ triggerCondition = null,
+ lockedCondition = lockedCondition,
+ frameLimit = frameLimit,
+ timeLimitNs = timeLimitNs,
+ )
+
+ /**
+ * Triggers 3A state updates and waits for locking/convergence for high quality image capture.
+ *
+ * By default, both AE precapture and AF are triggered, however this method does not try to lock
+ * the AE/AWB explicitly. So AE/AWB states will reach up to converged state only, not locked
+ * state (unless they were already locked). Use the [lock3A] method afterwards if locking AE/AWB
+ * is also required.
+ *
+ * It is possible to not trigger AF explicitly by disabling the [triggerAf] parameter. However,
+ * if [the AF mode is [CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE] or
+ * [CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO], the AF algorithm will continuously scan for
+ * good focus state even without an explicit AF trigger and may have been effected by the AE
+ * precapture trigger or scenery change. So, this method will still wait for AF to be converged
+ * for these AF modes even if the AF trigger is disabled.
+ *
+ * @param triggerAf Whether to trigger AF.
+ * @param waitForAwb Whether to wait for AWB to converge/lock.
+ *
+ * @return A [Deferred] containing a [Result3A] which will contain the latest frame number at
+ * which the locks were applied or the frame number at which the method returned early because
+ * either frame limit or time limit was reached.
+ */
+ suspend fun lock3AForCapture(
+ triggerAf: Boolean = true,
+ waitForAwb: Boolean = false,
+ frameLimit: Int = DEFAULT_FRAME_LIMIT,
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
+ ): Deferred<Result3A> {
+ val triggerCondition = if (triggerAf) {
+ parametersForAePrecaptureAndAfTrigger
+ } else {
+ parametersForAePrecapture
+ }
+
+ return lock3AForCapture(
+ triggerCondition = triggerCondition,
+ lockedCondition = createLock3AForCaptureExitConditions(
+ isAfTriggered = triggerAf,
+ waitForAwb = waitForAwb
+ ),
+ frameLimit = frameLimit,
+ timeLimitNs = timeLimitNs,
+ )
+ }
+
+ /**
+ * Triggers 3A state updates and waits for locking/convergence for high quality image capture.
+ *
+ * By default, both AE precapture and AF are triggered, however this method does not try to lock
+ * the AE/AWB explicitly. So AE/AWB states will reach up to converged state only, not locked
+ * state (unless they were already locked). Use the [lock3A] method afterwards if locking AE/AWB
+ * is also required.
+ *
+ * @param triggerCondition Customized trigger condition. If not provided, both AE precapture
+ * and AF.
+ * @param lockedCondition Customized exit condition for the result. If not provided,
+ * `createLock3AForCaptureExitConditions(isAfTriggered = true,
+ * waitForAwb = false)` will be used for default condition.
+ *
+ * @return A [Deferred] containing a [Result3A] which will contain the latest frame number at
+ * which the locks were applied or the frame number at which the method returned early because
+ * either frame limit or time limit was reached.
+ */
+ private suspend fun lock3AForCapture(
+ triggerCondition: Map<CaptureRequest.Key<*>, Any>? = null,
+ lockedCondition: ((FrameMetadata) -> Boolean)? = null,
+ frameLimit: Int = DEFAULT_FRAME_LIMIT,
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
if (!graphProcessor.hasRepeatingRequest()) {
return deferredResult3ASubmitFailed
}
- val listener =
- Result3AStateListenerImpl(
- lockedCondition ?: defaultLock3AForCaptureLockCondition,
- frameLimit,
- timeLimitNs
- )
+
+ val finalTriggerCondition = triggerCondition ?: parametersForAePrecaptureAndAfTrigger
+ var isAfTriggered = false
+ finalTriggerCondition.forEach { entry ->
+ if (entry.value == CONTROL_AE_PRECAPTURE_TRIGGER_START) {
+ isAfTriggered = true
+ }
+ }
+
+ val listener = Result3AStateListenerImpl(
+ lockedCondition ?: createLock3AForCaptureExitConditions(
+ isAfTriggered = isAfTriggered,
+ waitForAwb = false, // no need to wait for AWB in default case
+ ),
+ frameLimit,
+ timeLimitNs
+ )
+
graphListener3A.addListener(listener)
debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
- if (!graphProcessor.trySubmit(parametersForAePrecaptureAndAfTrigger)) {
+ if (!graphProcessor.trySubmit(finalTriggerCondition)) {
debug {
"lock3AForCapture - request to trigger ae precapture metering and af failed, " +
"returning early."
@@ -447,15 +566,15 @@
return listener.result
}
- suspend fun unlock3APostCapture(): Deferred<Result3A> {
+ suspend fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
if (!graphProcessor.hasRepeatingRequest()) {
return deferredResult3ASubmitFailed
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return unlock3APostCaptureAndroidMAndAbove()
+ return unlock3APostCaptureAndroidMAndAbove(cancelAf)
}
- return unlock3APostCaptureAndroidLAndBelow()
+ return unlock3APostCaptureAndroidLAndBelow(cancelAf)
}
/**
@@ -464,9 +583,17 @@
* REF :
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
- private suspend fun unlock3APostCaptureAndroidLAndBelow(): Deferred<Result3A> {
+ private suspend fun unlock3APostCaptureAndroidLAndBelow(
+ cancelAf: Boolean = true
+ ): Deferred<Result3A> {
debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
- if (!graphProcessor.trySubmit(unlock3APostCaptureLockAeParams)) {
+ if (!graphProcessor.trySubmit(
+ if (cancelAf) {
+ unlock3APostCaptureLockAeAndCancelAfParams
+ } else {
+ unlock3APostCaptureLockAeParams
+ }
+ )) {
debug { "unlock3AForCapture - request to cancel af and lock ae as failed." }
return deferredResult3ASubmitFailed
}
@@ -492,9 +619,12 @@
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
@RequiresApi(23)
- private suspend fun unlock3APostCaptureAndroidMAndAbove(): Deferred<Result3A> {
+ private suspend fun unlock3APostCaptureAndroidMAndAbove(
+ cancelAf: Boolean = true
+ ): Deferred<Result3A> {
debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
- if (!graphProcessor.trySubmit(aePrecaptureAndAfCancelParams)) {
+ val cancelParams = if (cancelAf) aePrecaptureAndAfCancelParams else aePrecaptureCancelParams
+ if (!graphProcessor.trySubmit(cancelParams)) {
debug {
"unlock3APostCapture - request to reset af and ae precapture metering failed, " +
"returning early."
@@ -506,7 +636,11 @@
// on the ae state, so we don't need to listen for a specific state. As long as the request
// successfully reaches the camera device and the capture request corresponding to that
// request arrives back, it should suffice.
- val listener = Result3AStateListenerImpl(unlock3APostCaptureAfUnlockedCondition)
+ val listener = if (cancelAf) {
+ Result3AStateListenerImpl(unlock3APostCaptureAfUnlockedCondition)
+ } else {
+ Result3AStateListenerImpl(emptyMap())
+ }
graphListener3A.addListener(listener)
graphProcessor.invalidate()
return listener.result
@@ -630,6 +764,47 @@
return exitConditionMapForLocked
}
+ private fun createLock3AForCaptureExitConditions(
+ isAfTriggered: Boolean,
+ waitForAwb: Boolean,
+ ): ((FrameMetadata) -> Boolean) = { frameMetadata ->
+ val afMode = AfMode(frameMetadata[CaptureResult.CONTROL_AF_MODE] ?: CONTROL_AF_MODE_OFF)
+ val meetsAfCondition = if (afMode.isOn()) {
+ if (isAfTriggered) {
+ afLockedStateList.contains(frameMetadata[CaptureResult.CONTROL_AF_STATE])
+ frameMetadata[CaptureResult.CONTROL_AF_STATE].isNullOrIn(afLockedStateList)
+ } else if (afMode.isContinuous()) {
+ // Even if AF is not triggered, we can still wait for PASSIVE_FOCUS in this case
+ afConvergedStateList.contains(frameMetadata[CaptureResult.CONTROL_AF_STATE])
+ } else {
+ true
+ }
+ } else {
+ true
+ }
+
+ // AE/AWB state may be null in some devices and thus should not be waited for in such case
+
+ val aeMode = AeMode(frameMetadata[CaptureResult.CONTROL_AE_MODE] ?: CONTROL_AE_MODE_OFF)
+ val meetsAeCondition = if (aeMode.isOn()) {
+ frameMetadata[CaptureResult.CONTROL_AE_STATE].isNullOrIn(aePostPrecaptureStateList)
+ } else {
+ true
+ }
+
+ val awbMode = AwbMode(frameMetadata[CaptureResult.CONTROL_AWB_MODE] ?: CONTROL_AWB_MODE_OFF)
+ val meetsAwbCondition = if (awbMode.isOn() && waitForAwb) {
+ frameMetadata[CaptureResult.CONTROL_AWB_STATE].isNullOrIn(awbPostPrecaptureStateList)
+ } else {
+ true
+ }
+
+ debug { "lock3AForCapture result: meetsAeCondition = $meetsAeCondition" +
+ ", meetsAfCondition = $meetsAfCondition, meetsAwbCondition = $meetsAwbCondition" }
+
+ meetsAeCondition && meetsAfCondition && meetsAwbCondition
+ }
+
private fun createUnLocked3AExitConditions(
ae: Boolean,
af: Boolean,
@@ -673,6 +848,10 @@
}
}
+/** Returns true if this is null or exists in the provided collection. */
+private fun <T> T?.isNullOrIn(collection: Collection<T>) =
+ this?.let { collection.contains(it) } ?: true
+
internal fun Lock3ABehavior?.shouldUnlockAe(): Boolean = this == Lock3ABehavior.AFTER_NEW_SCAN
internal fun Lock3ABehavior?.shouldUnlockAf(): Boolean = this == Lock3ABehavior.AFTER_NEW_SCAN
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
index 7e8ed2d..3b3a722 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
@@ -44,7 +44,7 @@
* Used to indicate that a previously initialized [GraphRequestProcessor] is no longer
* available.
*/
- fun onGraphStopped(requestProcessor: GraphRequestProcessor)
+ fun onGraphStopped(requestProcessor: GraphRequestProcessor?)
/**
* Used to indicate that the internal state of the [GraphRequestProcessor] has changed. This is
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index b5e3182..fc67120 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -226,9 +226,10 @@
_graphState.value = GraphStateStopping
}
- override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {
+ override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
debug { "$this onGraphStopped" }
_graphState.value = GraphStateStopped
+ if (requestProcessor == null) return
var old: GraphRequestProcessor? = null
synchronized(lock) {
if (closed) {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
index eafc0ae..9ccb72c 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
@@ -38,11 +38,11 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoInteractions
import org.robolectric.annotation.Config
@OptIn(ExperimentalCoroutinesApi::class)
@@ -107,7 +107,7 @@
// And a captureSession is never created
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
}
@Test
@@ -133,7 +133,7 @@
// Then fakeSurfaceListener marks surfaces as inactive.
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface1))
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface2))
}
@@ -167,7 +167,7 @@
// Then fakeSurfaceListener does not mark surfaces as inactive.
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
verify(fakeSurfaceListener, never()).onSurfaceInactive(eq(surface1))
verify(fakeSurfaceListener, never()).onSurfaceInactive(eq(surface2))
}
@@ -191,7 +191,7 @@
// Then fakeSurfaceListener marks surfaces as inactive.
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface1))
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface2))
}
@@ -215,7 +215,7 @@
// Then fakeSurfaceListener marks surfaces as inactive.
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface1))
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface2))
}
@@ -239,7 +239,7 @@
// Then fakeSurfaceListener marks surfaces as inactive.
advanceUntilIdle()
- verifyNoInteractions(fakeGraphListener)
+ verify(fakeGraphListener, times(1)).onGraphStopped(isNull())
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface1))
verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface2))
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 95006b2..de59848 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -15,12 +15,14 @@
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
package androidx.camera.camera2.pipe.graph
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.FrameMetadata
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestNumber
@@ -73,29 +75,24 @@
}
@Test
- fun testLock3AForCapture() = runTest {
+ fun testLock3AForCapture_when3aModesOn() = runTest {
val result = controller3A.lock3AForCapture()
assertThat(result.isCompleted).isFalse()
+ val on3aModesResultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+ )
+
// Since requirement is to trigger both AF and AE precapture metering. The result of
// lock3AForCapture call will complete once AE and AF have reached their desired states. In
// this response i.e cameraResponse1, AF is still scanning so the result won't be complete.
val cameraResponse = async {
- listener3A.onRequestSequenceCreated(
- FakeRequestMetadata(requestNumber = RequestNumber(1))
- )
- listener3A.onPartialCaptureResult(
- FakeRequestMetadata(requestNumber = RequestNumber(1)),
- FrameNumber(101L),
- FakeFrameMetadata(
- frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AF_STATE to
- CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
- CaptureResult.CONTROL_AE_STATE to
- CaptureResult.CONTROL_AE_STATE_SEARCHING
- )
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
)
)
}
@@ -106,21 +103,10 @@
// One we are notified that the AE and AF are in the desired states, the result of
// lock3AForCapture call will complete.
launch {
- listener3A.onRequestSequenceCreated(
- FakeRequestMetadata(requestNumber = RequestNumber(1))
- )
- listener3A.onPartialCaptureResult(
- FakeRequestMetadata(requestNumber = RequestNumber(1)),
- FrameNumber(101L),
- FakeFrameMetadata(
- frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AF_STATE to
- CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
- CaptureResult.CONTROL_AE_STATE to
- CaptureResult.CONTROL_AE_STATE_CONVERGED
- )
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
)
)
}
@@ -131,11 +117,175 @@
// We now check if the correct sequence of requests were submitted by lock3AForCapture call.
// There should be a request to trigger AF and AE precapture metering.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ assertCorrectCaptureSequenceInLock3AForCapture()
+ }
+
+ @Test
+ fun testLock3AForCapture_whenWaitingForAwb() = runTest {
+ val result = controller3A.lock3AForCapture(waitForAwb = true)
+ assertThat(result.isCompleted).isFalse()
+
+ val on3aModesResultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+ )
+
+ // AF/AE completed, but AWB still ongoing so result will be incomplete
+ val cameraResponse = async {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_SEARCHING,
+ )
+ )
+ }
+
+ cameraResponse.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // One we are notified that the AE and AF are in the desired states, the result of
+ // lock3AForCapture call will complete.
+ launch {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+ // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
+ // There should be a request to trigger AF and AE precapture metering.
+ assertCorrectCaptureSequenceInLock3AForCapture()
+ }
+
+ @Test
+ fun testLock3AForCapture_when3aModesAreOff() = runTest {
+ val result = controller3A.lock3AForCapture()
+ assertThat(result.isCompleted).isFalse()
+
+ val off3aModesResultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_OFF,
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_OFF,
+ )
+
+ // Since the 3A modes are off, the result of lock3AForCapture call will complete without
+ // waiting to be converged.
+ launch {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = off3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_SEARCHING
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result.isCompleted).isTrue()
+
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+ // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
+ // There should be a request to trigger AF and AE precapture metering.
+ assertCorrectCaptureSequenceInLock3AForCapture()
+ }
+
+ @Test
+ fun testLock3AForCapture_withoutAfTrigger_whenAfModeContinuousPicture() = runTest {
+ val result = controller3A.lock3AForCapture(triggerAf = false)
+ assertThat(result.isCompleted).isFalse()
+
+ val on3aModesResultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+ )
+
+ // In this response, AF is still scanning so the result won't be complete.
+ val cameraResponse = async {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN
+ )
+ )
+ }
+
+ cameraResponse.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // One we are notified that AF and AE are in the desired states, the result will complete.
+ launch {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+ // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
+ // There should be a request to trigger AE precapture metering, but not AF.
+ assertCorrectCaptureSequenceInLock3AForCapture(false)
+ }
+
+ @Test
+ fun testLock3AForCapture_withoutAfTrigger_whenAfModeAuto() = runTest {
+ val result = controller3A.lock3AForCapture(triggerAf = false)
+ assertThat(result.isCompleted).isFalse()
+
+ val on3aModesResultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+ )
+
+ // In this response, AF is still scanning so the result won't be complete.
+ val cameraResponse = async {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
+ )
+ )
+ }
+
+ cameraResponse.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // Since AF mode is AUTO and AF is not triggered, the result will complete without AF
+ // convergence.
+ launch {
+ listener3A.sendPartialCaptureResult(
+ resultMetadata = on3aModesResultMetadata + mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+ // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
+ // There should be a request to trigger AE precapture metering, but not AF.
+ assertCorrectCaptureSequenceInLock3AForCapture(false)
}
@Test
@@ -218,12 +368,21 @@
}
}
- private fun testUnlock3APostCaptureAndroidMAndAbove() = runTest {
- val result = controller3A.unlock3APostCapture()
+ @Test
+ fun testUnlock3APostCapture_whenAfNotTriggered() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ testUnlock3APostCaptureAndroidMAndAbove(false)
+ } else {
+ testUnlock3APostCaptureAndroidLAndBelow(false)
+ }
+ }
+
+ private fun testUnlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true) = runTest {
+ val result = controller3A.unlock3APostCapture(cancelAf)
assertThat(result.isCompleted).isFalse()
// In this response i.e cameraResponse1, AF is still scanning so the result won't be
- // complete.
+ // complete if AF cancellation is required.
val cameraResponse = async {
listener3A.onRequestSequenceCreated(
FakeRequestMetadata(requestNumber = RequestNumber(1))
@@ -245,7 +404,11 @@
}
cameraResponse.await()
- assertThat(result.isCompleted).isFalse()
+ if (cancelAf) {
+ assertThat(result.isCompleted).isFalse()
+ } else {
+ assertThat(result.isCompleted).isTrue()
+ }
// Once we are notified that the AF is in unlocked state, the result of unlock3APostCapture
// call will complete. For AE we don't need to to check for a specific state, receiving the
@@ -277,14 +440,16 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and AE precapture metering.
val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+ if (cancelAf) {
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ }
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
.isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
}
- private fun testUnlock3APostCaptureAndroidLAndBelow() = runTest {
- val result = controller3A.unlock3APostCapture()
+ private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
+ val result = controller3A.unlock3APostCapture(cancelAf)
assertThat(result.isCompleted).isFalse()
val cameraResponse = async {
@@ -306,17 +471,52 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and lock ae.
val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ if (cancelAf) {
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ }
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
// Then another request to unlock ae.
val request2 = captureSequenceProcessor.nextEvent().requestSequence
assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
}
+ private suspend fun assertCorrectCaptureSequenceInLock3AForCapture(
+ isAfTriggered: Boolean = true
+ ) {
+ val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
+ if (isAfTriggered) {
+ isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+ } else {
+ isNotEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
+ }
+ }
+ assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+ .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ }
+
companion object {
// The time duration in milliseconds between two frame results.
private const val FRAME_RATE_MS = 33L
}
}
+
+private fun Listener3A.sendPartialCaptureResult(
+ requestNumber: Long = 1L,
+ frameNumber: Long = 101L,
+ resultMetadata: Map<CaptureResult.Key<*>, Any?>
+) {
+ onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(requestNumber))
+ )
+ onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(requestNumber)),
+ FrameNumber(frameNumber),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = resultMetadata
+ )
+ )
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index b32c80d..be0e8eb 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -129,8 +129,9 @@
_graphState.value = GraphStateStopping
}
- override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {
+ override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
_graphState.value = GraphStateStopped
+ if (requestProcessor == null) return
val old = processor
if (requestProcessor === old) {
processor = null
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/DeleteMe.kt b/camera/camera-camera2/src/main/java/androidx/camera/camera2/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index a768062..2a567c8 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -520,7 +520,7 @@
@SuppressCompatibility @RequiresApi(21) @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
method public static long getDefaultRetryTimeoutInMillis();
method public default long getTimeoutInMillis();
- method public androidx.camera.core.RetryPolicy.RetryResponse shouldRetry(androidx.camera.core.RetryPolicy.ExecutionState);
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
field public static final androidx.camera.core.RetryPolicy DEFAULT;
field public static final androidx.camera.core.RetryPolicy NEVER;
field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
@@ -542,20 +542,20 @@
field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
}
- @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryResponse {
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
method public static long getDefaultRetryDelayInMillis();
method public long getRetryDelayInMillis();
method public boolean shouldRetry();
- field public static final androidx.camera.core.RetryPolicy.RetryResponse DEFAULT_DELAY_RETRY;
- field public static final androidx.camera.core.RetryPolicy.RetryResponse MINI_DELAY_RETRY;
- field public static final androidx.camera.core.RetryPolicy.RetryResponse NOT_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
}
- @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryResponse.Builder {
- ctor public RetryPolicy.RetryResponse.Builder();
- method public androidx.camera.core.RetryPolicy.RetryResponse build();
- method public androidx.camera.core.RetryPolicy.RetryResponse.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
- method public androidx.camera.core.RetryPolicy.RetryResponse.Builder setShouldRetry(boolean);
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
}
@RequiresApi(21) public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index a768062..2a567c8 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -520,7 +520,7 @@
@SuppressCompatibility @RequiresApi(21) @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
method public static long getDefaultRetryTimeoutInMillis();
method public default long getTimeoutInMillis();
- method public androidx.camera.core.RetryPolicy.RetryResponse shouldRetry(androidx.camera.core.RetryPolicy.ExecutionState);
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
field public static final androidx.camera.core.RetryPolicy DEFAULT;
field public static final androidx.camera.core.RetryPolicy NEVER;
field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
@@ -542,20 +542,20 @@
field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
}
- @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryResponse {
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
method public static long getDefaultRetryDelayInMillis();
method public long getRetryDelayInMillis();
method public boolean shouldRetry();
- field public static final androidx.camera.core.RetryPolicy.RetryResponse DEFAULT_DELAY_RETRY;
- field public static final androidx.camera.core.RetryPolicy.RetryResponse MINI_DELAY_RETRY;
- field public static final androidx.camera.core.RetryPolicy.RetryResponse NOT_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
}
- @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryResponse.Builder {
- ctor public RetryPolicy.RetryResponse.Builder();
- method public androidx.camera.core.RetryPolicy.RetryResponse build();
- method public androidx.camera.core.RetryPolicy.RetryResponse.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
- method public androidx.camera.core.RetryPolicy.RetryResponse.Builder setShouldRetry(boolean);
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
}
@RequiresApi(21) public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 9401862..c3c39aa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -350,20 +350,20 @@
completer.set(null);
} catch (CameraIdListIncorrectException | InitializationException
| RuntimeException e) {
- RetryPolicy.RetryResponse response = mRetryPolicy.shouldRetry(
+ RetryPolicy.RetryConfig retryConfig = mRetryPolicy.onRetryDecisionRequested(
new CameraProviderExecutionState(startMs, attemptCount, e));
- if (response.shouldRetry() && attemptCount < Integer.MAX_VALUE) {
+ if (retryConfig.shouldRetry() && attemptCount < Integer.MAX_VALUE) {
Logger.w(TAG, "Retry init. Start time " + startMs + " current time "
+ SystemClock.elapsedRealtime(), e);
HandlerCompat.postDelayed(mSchedulerHandler, () -> initAndRetryRecursively(
cameraExecutor, startMs, attemptCount + 1, mAppContext,
- completer), RETRY_TOKEN, response.getRetryDelayInMillis());
+ completer), RETRY_TOKEN, retryConfig.getRetryDelayInMillis());
} else {
synchronized (mInitializeLock) {
mInitState = InternalInitState.INITIALIZING_ERROR;
}
- if (response.shouldCompleteWithoutFailure()) {
+ if (retryConfig.shouldCompleteWithoutFailure()) {
// Ignoring camera failure for compatibility reasons. Initialization will
// be marked as complete, but some camera features might be unavailable.
setStateToInitialized();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/DeleteMe.kt b/camera/camera-core/src/main/java/androidx/camera/core/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/RetryPolicy.java b/camera/camera-core/src/main/java/androidx/camera/core/RetryPolicy.java
index 2fb6980..3756d74 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/RetryPolicy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/RetryPolicy.java
@@ -71,22 +71,22 @@
* if (executionState.getExecutedTimeInMillis() > 10000L
* || executionState.getNumOfAttempts() > 10
* || executionState.getStatus() == ExecutionState.STATUS_CONFIGURATION_FAIL) {
- * return RetryResponse.NOT_RETRY;
+ * return RetryConfig.NOT_RETRY;
* } else if (executionState.getStatus() == ExecutionState.STATUS_CAMERA_UNAVAILABLE) {
- * return RetryResponse.DEFAULT_DELAY_RETRY;
+ * return RetryConfig.DEFAULT_DELAY_RETRY;
* } else {
* Log.d("CameraX", "Unknown error occur: " + executionState.getCause());
- * return RetryResponse.MINI_DELAY_RETRY;
+ * return RetryConfig.MINI_DELAY_RETRY;
* }
* }).build());
* ...
* }</pre>
* In the second example, the custom retry policy retries the initialization up to 10 times or
* for a maximum of 10 seconds. If an unknown error occurs, the retry policy delays the next
- * retry after a delay defined by {@link RetryResponse#MINI_DELAY_RETRY}. The retry process
+ * retry after a delay defined by {@link RetryConfig#MINI_DELAY_RETRY}. The retry process
* stops if the status is {@link ExecutionState#STATUS_CONFIGURATION_FAIL}. For
* {@link ExecutionState#STATUS_CAMERA_UNAVAILABLE}, the retry policy applies
- * {@link RetryResponse#DEFAULT_DELAY_RETRY}.
+ * {@link RetryConfig#DEFAULT_DELAY_RETRY}.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@ExperimentalRetryPolicy
@@ -103,7 +103,7 @@
* immediately halts the initialization upon encountering an error.
*/
@NonNull
- RetryPolicy NEVER = executionState -> RetryResponse.NOT_RETRY;
+ RetryPolicy NEVER = executionState -> RetryConfig.NOT_RETRY;
/**
* This retry policy increases initialization success by automatically retrying upon
@@ -154,13 +154,14 @@
}
/**
- * Determines whether to retry the initialization.
+ * Called to request a decision on whether to retry the initialization process.
*
- * @param executionState The information about the execution state of the camera initialization.
- * @return A RetryResponse indicating whether to retry the initialization.
+ * @param executionState Information about the current execution state of the camera
+ * initialization.
+ * @return A RetryConfig indicating whether to retry, along with any associated delay.
*/
@NonNull
- RetryResponse shouldRetry(@NonNull ExecutionState executionState);
+ RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState);
/**
* Returns the maximum allowed retry duration in milliseconds. Initialization will
@@ -208,7 +209,7 @@
/**
* Sets a timeout in milliseconds. If retries exceed this duration, they will be
- * terminated with {@link RetryPolicy.RetryResponse#NOT_RETRY}.
+ * terminated with {@link RetryConfig#NOT_RETRY}.
*
* @param timeoutInMillis The maximum duration for retries in milliseconds. A value of 0
* indicates no timeout.
@@ -332,26 +333,26 @@
* Represents the outcome of a {@link RetryPolicy} decision.
*/
@ExperimentalRetryPolicy
- final class RetryResponse {
+ final class RetryConfig {
private static final long MINI_DELAY_MILLIS = 100L;
private static final long DEFAULT_DELAY_MILLIS = 500L;
- /** A RetryResponse indicating that no further retries should be attempted. */
+ /** A RetryConfig indicating that no further retries should be attempted. */
@NonNull
- public static final RetryResponse NOT_RETRY = new RetryResponse(false, 0L);
+ public static final RetryConfig NOT_RETRY = new RetryConfig(false, 0L);
/**
- * A RetryResponse indicating that the initialization should be retried after the default
+ * A RetryConfig indicating that the initialization should be retried after the default
* delay (determined by {@link #getDefaultRetryDelayInMillis()}). This delay provides
* sufficient time for typical device recovery processes, balancing retry efficiency
* and minimizing user wait time.
*/
@NonNull
- public static final RetryResponse DEFAULT_DELAY_RETRY = new RetryResponse(true);
+ public static final RetryConfig DEFAULT_DELAY_RETRY = new RetryConfig(true);
/**
- * A RetryResponse indicating that the initialization should be retried after a minimum
+ * A RetryConfig indicating that the initialization should be retried after a minimum
* delay of 100 milliseconds.
*
* This short delay serves two purposes:
@@ -365,18 +366,17 @@
* fastest possible camera restoration.
*/
@NonNull
- public static final RetryResponse MINI_DELAY_RETRY = new RetryResponse(true,
- MINI_DELAY_MILLIS);
+ public static final RetryConfig MINI_DELAY_RETRY = new RetryConfig(true, MINI_DELAY_MILLIS);
/**
- * A RetryResponse indicating that the initialization should be considered complete
- * without retrying. This response is intended for internal use and is not intended to
+ * A RetryConfig indicating that the initialization should be considered complete
+ * without retrying. This config is intended for internal use and is not intended to
* trigger further retries. It represents the legacy behavior of not failing the
* initialization task for minor issues.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@NonNull
- public static RetryResponse COMPLETE_WITHOUT_FAILURE = new RetryResponse(false, 0, true);
+ public static RetryConfig COMPLETE_WITHOUT_FAILURE = new RetryConfig(false, 0, true);
/**
* Returns the recommended default delay to optimize retry attempts and camera recovery.
@@ -400,11 +400,11 @@
private final boolean mShouldRetry;
private final boolean mCompleteWithoutFailure;
- private RetryResponse(boolean shouldRetry) {
- this(shouldRetry, RetryResponse.getDefaultRetryDelayInMillis());
+ private RetryConfig(boolean shouldRetry) {
+ this(shouldRetry, RetryConfig.getDefaultRetryDelayInMillis());
}
- private RetryResponse(boolean shouldRetry, long delayInMillis) {
+ private RetryConfig(boolean shouldRetry, long delayInMillis) {
this(shouldRetry, delayInMillis, false);
}
@@ -420,7 +420,7 @@
* When this flag is set to true, `shouldRetry` must be
* false.
*/
- private RetryResponse(boolean shouldRetry, long delayInMillis,
+ private RetryConfig(boolean shouldRetry, long delayInMillis,
boolean completeWithoutFailure) {
mShouldRetry = shouldRetry;
mDelayInMillis = delayInMillis;
@@ -452,7 +452,7 @@
/**
* Signals to treat initialization errors as successful for legacy behavior compatibility.
*
- * <p>This response is intended for internal use and is not intended to trigger further
+ * <p>This config is intended for internal use and is not intended to trigger further
* retries.
*
* @return true if initialization should be deemed complete without additional retries,
@@ -464,9 +464,9 @@
}
/**
- * A builder class for creating and customizing {@link RetryResponse} objects.
+ * A builder class for creating and customizing {@link RetryConfig} objects.
*
- * <p>While predefined responses like {@link RetryResponse#DEFAULT_DELAY_RETRY} are
+ * <p>While predefined configs like {@link RetryConfig#DEFAULT_DELAY_RETRY} are
* recommended for typical recovery scenarios, this builder allows for fine-tuned control
* when specific requirements necessitate a different approach.
*/
@@ -474,7 +474,7 @@
public static final class Builder {
private boolean mShouldRetry = true;
- private long mTimeoutInMillis = RetryResponse.getDefaultRetryDelayInMillis();
+ private long mTimeoutInMillis = RetryConfig.getDefaultRetryDelayInMillis();
/**
* Specifies whether a retry should be attempted.
@@ -508,13 +508,13 @@
}
/**
- * Builds the customized {@link RetryResponse} object.
+ * Builds the customized {@link RetryConfig} object.
*
- * @return The configured RetryResponse.
+ * @return The configured RetryConfig.
*/
@NonNull
- public RetryResponse build() {
- return new RetryResponse(mShouldRetry, mTimeoutInMillis);
+ public RetryConfig build() {
+ return new RetryConfig(mShouldRetry, mTimeoutInMillis);
}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraProviderInitRetryPolicy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraProviderInitRetryPolicy.java
index 1efbe47..2ef47e1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraProviderInitRetryPolicy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraProviderInitRetryPolicy.java
@@ -28,7 +28,7 @@
* Basic retry policy that automatically retries most failures with a standard delay.
*
* <p>This policy will initiate a retry with the
- * {@link RetryResponse#DEFAULT_DELAY_RETRY} delay for any failure status except
+ * {@link RetryConfig#DEFAULT_DELAY_RETRY} delay for any failure status except
* {@link ExecutionState#STATUS_CONFIGURATION_FAIL}.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -41,12 +41,12 @@
mDelegatePolicy = new TimeoutRetryPolicy(timeoutInMillis, new RetryPolicy() {
@NonNull
@Override
- public RetryResponse shouldRetry(@NonNull ExecutionState executionState) {
+ public RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState) {
if (executionState.getStatus() == ExecutionState.STATUS_CONFIGURATION_FAIL) {
- return RetryResponse.NOT_RETRY;
+ return RetryConfig.NOT_RETRY;
}
- return RetryResponse.DEFAULT_DELAY_RETRY;
+ return RetryConfig.DEFAULT_DELAY_RETRY;
}
@Override
@@ -58,8 +58,8 @@
@NonNull
@Override
- public RetryResponse shouldRetry(@NonNull ExecutionState executionState) {
- return mDelegatePolicy.shouldRetry(executionState);
+ public RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState) {
+ return mDelegatePolicy.onRetryDecisionRequested(executionState);
}
@Override
@@ -99,8 +99,8 @@
@NonNull
@Override
- public RetryResponse shouldRetry(@NonNull ExecutionState executionState) {
- if (!mBasePolicy.shouldRetry(executionState).shouldRetry()) {
+ public RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState) {
+ if (!mBasePolicy.onRetryDecisionRequested(executionState).shouldRetry()) {
Throwable cause = executionState.getCause();
if (cause instanceof CameraIdListIncorrectException) {
Logger.e("CameraX", "The device might underreport the amount of the "
@@ -110,12 +110,12 @@
// If the initialization task execution time exceeds the timeout
// threshold and the error type is CameraIdListIncorrectException,
// consider the initialization complete without retrying.
- return RetryResponse.COMPLETE_WITHOUT_FAILURE;
+ return RetryConfig.COMPLETE_WITHOUT_FAILURE;
}
}
- return RetryResponse.NOT_RETRY;
+ return RetryConfig.NOT_RETRY;
}
- return RetryResponse.DEFAULT_DELAY_RETRY;
+ return RetryConfig.DEFAULT_DELAY_RETRY;
}
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/TimeoutRetryPolicy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/TimeoutRetryPolicy.java
index 139ba82..5145b43 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/TimeoutRetryPolicy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/TimeoutRetryPolicy.java
@@ -27,7 +27,7 @@
*
* <p>This retry policy monitors the total execution time of a task. If the time surpasses the
* configured timeout threshold, it immediately stops any further retries by returning
- * {@link RetryPolicy.RetryResponse#NOT_RETRY}.
+ * {@link RetryConfig#NOT_RETRY}.
*
* <p>If the task total execution within the timeout, this policy delegates the retry decision to
* the underlying {@link RetryPolicy}, allowing for normal retry behavior based on other factors.
@@ -56,11 +56,11 @@
@NonNull
@Override
- public RetryResponse shouldRetry(@NonNull ExecutionState executionState) {
- RetryResponse response = mDelegatePolicy.shouldRetry(executionState);
+ public RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState) {
+ RetryConfig retryConfig = mDelegatePolicy.onRetryDecisionRequested(executionState);
return getTimeoutInMillis() > 0 && executionState.getExecutedTimeInMillis()
- >= getTimeoutInMillis() - response.getRetryDelayInMillis() ? RetryResponse.NOT_RETRY
- : response;
+ >= getTimeoutInMillis() - retryConfig.getRetryDelayInMillis()
+ ? RetryConfig.NOT_RETRY : retryConfig;
}
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
index a880482..9e4e6ba 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
@@ -242,6 +242,14 @@
* Sets the resolution selection strategy for the {@link UseCase}. The resolution selection
* strategy determines how the {@link UseCase} will choose the resolution of the captured
* image.
+ *
+ * <p>Note: The default {@link AspectRatioStrategy} is
+ * {@link AspectRatioStrategy#RATIO_4_3_FALLBACK_AUTO_STRATEGY}. Ensure you set a
+ * corresponding {@link AspectRatioStrategy} alongside your {@link ResolutionStrategy}.
+ * For example, if your {@link ResolutionStrategy} uses a bound size of {@code 1920x1080}
+ * and a 16:9 aspect ratio is preferred, set
+ * {@link AspectRatioStrategy#RATIO_16_9_FALLBACK_AUTO_STRATEGY} when building the
+ * {@link ResolutionSelector}.
*/
@NonNull
public Builder setResolutionStrategy(@NonNull ResolutionStrategy resolutionStrategy) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CameraXInitRetryTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/CameraXInitRetryTest.kt
index 15b652d..1103c00 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/CameraXInitRetryTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/CameraXInitRetryTest.kt
@@ -27,7 +27,7 @@
import androidx.camera.core.RetryPolicy.ExecutionState
import androidx.camera.core.RetryPolicy.NEVER
import androidx.camera.core.RetryPolicy.RETRY_UNAVAILABLE_CAMERA
-import androidx.camera.core.RetryPolicy.RetryResponse
+import androidx.camera.core.RetryPolicy.RetryConfig
import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraDeviceSurfaceManager
import androidx.camera.core.impl.CameraFactory
@@ -101,7 +101,7 @@
val executionStateMutableList = mutableListOf<ExecutionState>()
val policy = RetryPolicy { executionState: ExecutionState ->
executionStateMutableList.add(executionState)
- return@RetryPolicy DEFAULT.shouldRetry(executionState)
+ return@RetryPolicy DEFAULT.onRetryDecisionRequested(executionState)
}
val configBuilder: CameraXConfig.Builder = CameraXConfig.Builder.fromConfig(
createCameraXConfig(
@@ -196,7 +196,7 @@
setSchedulerHandler(handler)
setCameraProviderInitRetryPolicy { executionState ->
callCount++
- NEVER.shouldRetry(executionState)
+ NEVER.onRetryDecisionRequested(executionState)
}
}
@@ -215,7 +215,7 @@
}
@Test
- fun verifyImmediateFailureWithOptionResponseNotRetry() = runTest {
+ fun verifyImmediateFailureWithOptionRetryConfigNotRetry() = runTest {
// Arrange. Set up a simulated environment that no accessible cameras.
var callCount = 0
val configBuilder: CameraXConfig.Builder = CameraXConfig.Builder.fromConfig(
@@ -230,7 +230,7 @@
setSchedulerHandler(handler)
setCameraProviderInitRetryPolicy {
callCount++
- RetryResponse.Builder().setShouldRetry(false).build()
+ RetryConfig.Builder().setShouldRetry(false).build()
}
}
@@ -283,7 +283,7 @@
setCameraExecutor(handlerExecutor)
setSchedulerHandler(handler)
setCameraProviderInitRetryPolicy { executionState: ExecutionState ->
- RETRY_UNAVAILABLE_CAMERA.shouldRetry(executionState).also {
+ RETRY_UNAVAILABLE_CAMERA.onRetryDecisionRequested(executionState).also {
executedTime = executionState.executedTimeInMillis
}
}
@@ -302,7 +302,7 @@
throwableSubject.hasCauseThat().isInstanceOf(CameraUnavailableException::class.java)
assertThat(cameraX.isInitialized).isFalse()
assertThat(abs(DEFAULT_RETRY_TIMEOUT_IN_MILLIS - executedTime)).isLessThan(
- RetryResponse.DEFAULT_DELAY_RETRY.retryDelayInMillis + 100
+ RetryConfig.DEFAULT_DELAY_RETRY.retryDelayInMillis + 100
// Allow the tolerance for retry delay + 100ms potential processing time variations.
)
}
@@ -320,7 +320,7 @@
@Test
fun testTimeoutAdjustment_CustomRetryPolicyMode() = runTest {
// Arrange. Set up a RetryPolicy that persistently retries initialization attempts.
- val customAlwaysRetryPolicy = RetryPolicy { RetryResponse.MINI_DELAY_RETRY }
+ val customAlwaysRetryPolicy = RetryPolicy { RetryConfig.MINI_DELAY_RETRY }
// Act. & Assert. Confirm that retries cease if the total execution time surpasses the
// defined timeout, preventing indefinite loops.
@@ -339,7 +339,7 @@
setCameraExecutor(handlerExecutor)
setSchedulerHandler(handler)
setCameraProviderInitRetryPolicy { executionState ->
- customTimeoutPolicy.shouldRetry(executionState).also {
+ customTimeoutPolicy.onRetryDecisionRequested(executionState).also {
executedTime = executionState.executedTimeInMillis
}
}
@@ -357,7 +357,7 @@
// Assert. Verify that initialization persists with retries until the total execution
// time exhausts the allotted timeout.
assertThat(abs(testCustomTimeout - executedTime)).isLessThan(
- RetryResponse.DEFAULT_DELAY_RETRY.retryDelayInMillis + 100
+ RetryConfig.DEFAULT_DELAY_RETRY.retryDelayInMillis + 100
// Allow the tolerance for retry delay + 100ms potential processing time variations.
)
}
@@ -367,7 +367,7 @@
val desiredDelayTime = 900L
assertThat(
- RetryResponse.Builder().setRetryDelayInMillis(desiredDelayTime)
+ RetryConfig.Builder().setRetryDelayInMillis(desiredDelayTime)
.build().retryDelayInMillis
).isEqualTo(
desiredDelayTime
@@ -381,12 +381,12 @@
val timeoutInMs = 10000L
val executionStateMutableList = mutableListOf<ExecutionState>()
val policy = object : RetryPolicy {
- override fun shouldRetry(executionState: ExecutionState): RetryResponse {
+ override fun onRetryDecisionRequested(executionState: ExecutionState): RetryConfig {
if (executionState.getExecutedTimeInMillis() < timeoutInMillis) {
executionStateMutableList.add(executionState)
}
- return RetryResponse.DEFAULT_DELAY_RETRY
+ return RetryConfig.DEFAULT_DELAY_RETRY
}
override fun getTimeoutInMillis(): Long {
@@ -437,10 +437,10 @@
val policy = RetryPolicy { executionState ->
if (executionState.getExecutedTimeInMillis() < timeoutInMs) {
executionStateMutableList.add(executionState)
- return@RetryPolicy RetryResponse.DEFAULT_DELAY_RETRY;
+ return@RetryPolicy RetryConfig.DEFAULT_DELAY_RETRY;
}
- return@RetryPolicy RetryResponse.NOT_RETRY
+ return@RetryPolicy RetryConfig.NOT_RETRY
}
val configBuilder: CameraXConfig.Builder = CameraXConfig.Builder.fromConfig(
createCameraXConfig()
@@ -486,10 +486,10 @@
val policy = RetryPolicy { executionState: ExecutionState ->
executionStateMutableList.add(executionState)
if (executionState.numOfAttempts < maxAttempts) {
- return@RetryPolicy RetryResponse.DEFAULT_DELAY_RETRY;
+ return@RetryPolicy RetryConfig.DEFAULT_DELAY_RETRY;
}
- return@RetryPolicy RetryResponse.NOT_RETRY
+ return@RetryPolicy RetryConfig.NOT_RETRY
}
val configBuilder: CameraXConfig.Builder = CameraXConfig.Builder.fromConfig(
createCameraXConfig()
@@ -520,7 +520,7 @@
val resultList = mutableListOf<ExecutionState>()
val policy = RetryPolicy { executionState: ExecutionState ->
resultList.add(executionState)
- RETRY_UNAVAILABLE_CAMERA.shouldRetry(executionState)
+ RETRY_UNAVAILABLE_CAMERA.onRetryDecisionRequested(executionState)
}
val configBuilder: CameraXConfig.Builder = CameraXConfig.Builder.fromConfig(
createCameraXConfig(surfaceManager = null, useCaseConfigFactory = null)
@@ -573,7 +573,7 @@
setSchedulerHandler(handler)
setCameraProviderInitRetryPolicy { executionState: ExecutionState ->
executionStateMutableList.add(executionState)
- RetryResponse.NOT_RETRY
+ RetryConfig.NOT_RETRY
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/DeleteMe.kt b/camera/camera-extensions/src/main/java/androidx/camera/extensions/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/DeleteMe.kt b/camera/camera-video/src/main/java/androidx/camera/video/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/DeleteMe.kt b/camera/camera-view/src/main/java/androidx/camera/view/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/DeleteMe.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
index 3d89096..38c0edf 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
@@ -165,7 +165,7 @@
<string name="gas_station" msgid="1203313937444666161">"SPBU"</string>
<string name="short_route" msgid="4831864276538141265">"Rute pendek"</string>
<string name="less_busy" msgid="310625272281710983">"Tidak terlalu sibuk"</string>
- <string name="hov_friendly" msgid="6956152104754594971">"Cocok untuk Kendaraan Padat Penumpang"</string>
+ <string name="hov_friendly" msgid="6956152104754594971">"Sesuai untuk kendaraan multi-penumpang"</string>
<string name="long_route" msgid="4737969235741057506">"Rute panjang"</string>
<string name="continue_start_nav" msgid="6231797535084469163">"Lanjutkan untuk memulai navigasi"</string>
<string name="continue_route" msgid="5172258139245088080">"Lanjutkan ke rute"</string>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 01f91c7..20227ea 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -1088,6 +1088,40 @@
"Runtime(A)"
)
+ // b/327643787
+ @Test
+ fun testNestedExternalTypesAreStable() = assertStability(
+ externalSrc = "",
+ localSrc = """
+ data class B(val list: List<Int>)
+ data class A(val list: List<B>)
+ """.trimIndent(),
+ expression = "A(listOf())",
+ externalTypes = setOf("kotlin.collections.List"),
+ stability = "Stable"
+ )
+
+ @Test
+ fun testNestedGenericsAreRuntimeStable() = assertStability(
+ externalSrc = "",
+ localSrc = """
+ class A(val child: List<A>?)
+ """.trimIndent(),
+ expression = "A(null)",
+ externalTypes = setOf("kotlin.collections.List"),
+ stability = "Unstable"
+ )
+
+ @Test
+ fun testNestedEqualTypesAreUnstable() = assertStability(
+ externalSrc = "",
+ localSrc = """
+ class A(val child: A?)
+ """.trimIndent(),
+ expression = "A(A(null))",
+ stability = "Unstable"
+ )
+
@Test
fun testEmptyClass() = assertTransform(
"""
@@ -1593,7 +1627,9 @@
""".trimIndent()
val files = listOf(SourceFile("Test.kt", source))
- val irModule = compileToIr(files)
+ val irModule = compileToIr(files, registerExtensions = {
+ it.put(ComposeConfiguration.TEST_STABILITY_CONFIG_KEY, externalTypes)
+ })
val irClass = irModule.files.last().declarations.first() as IrClass
val externalTypeMatchers = externalTypes.map { FqNameMatcher(it) }.toSet()
val stabilityInferencer = StabilityInferencer(irModule.descriptor, externalTypeMatchers)
@@ -1615,7 +1651,13 @@
externalTypes: Set<String> = emptySet(),
packageName: String = "dependency"
) {
- val irModule = buildModule(externalSrc, classDefSrc, dumpClasses, packageName)
+ val irModule = buildModule(
+ externalSrc,
+ classDefSrc,
+ dumpClasses,
+ packageName,
+ externalTypes = externalTypes
+ )
val irClass = irModule.files.last().declarations.first() as IrClass
val externalTypeMatchers = externalTypes.map { FqNameMatcher(it) }.toSet()
val classStability =
@@ -1700,7 +1742,8 @@
fun TestFunction() = $expression
""".trimIndent(),
- dumpClasses
+ dumpClasses,
+ externalTypes = externalTypes
)
val irTestFn = irModule
.files
@@ -1731,7 +1774,8 @@
@Language("kotlin")
localSrc: String,
dumpClasses: Boolean = false,
- packageName: String = "dependency"
+ packageName: String = "dependency",
+ externalTypes: Set<String>
): IrModuleFragment {
val dependencyFileName = "Test_REPLACEME_${uniqueNumber++}"
val dependencySrc = """
@@ -1771,7 +1815,10 @@
""".trimIndent()
val files = listOf(SourceFile("Test.kt", source))
- return compileToIr(files, listOf(classesDirectory.root))
+ return compileToIr(files, listOf(classesDirectory.root), registerExtensions = {
+ it.put(ComposeConfiguration.TEST_STABILITY_CONFIG_KEY, externalTypes)
+ it.updateConfiguration()
+ })
}
private fun assertTransform(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index bbc589f..c5c7fac 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -80,6 +80,10 @@
CompilerConfigurationKey<List<String>>(
"Path to stability configuration file"
)
+ val TEST_STABILITY_CONFIG_KEY =
+ CompilerConfigurationKey<Set<String>>(
+ "Set of stable classes to be merged with configuration file, used for testing."
+ )
val TRACE_MARKERS_ENABLED_KEY =
CompilerConfigurationKey<Boolean>("Include composition trace markers in generated code")
}
@@ -453,6 +457,10 @@
}
stableTypeMatchers.addAll(matchers)
}
+ val testingMatchers = configuration.get(ComposeConfiguration.TEST_STABILITY_CONFIG_KEY)
+ ?.map { FqNameMatcher(it) }
+ ?: emptySet()
+ stableTypeMatchers.addAll(testingMatchers)
return ComposeIrGenerationExtension(
liveLiteralsEnabled = liveLiteralsEnabled,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
index eee5edc..ba9179e 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
@@ -227,6 +227,11 @@
?.getValueArgument(0) as? IrConst<*>
)?.value as? Int
+private data class SymbolForAnalysis(
+ val symbol: IrClassifierSymbol,
+ val typeParameters: List<IrTypeArgument?>
+)
+
class StabilityInferencer(
private val currentModule: ModuleDescriptor,
externalStableTypeMatchers: Set<FqNameMatcher>
@@ -239,10 +244,13 @@
private fun stabilityOf(
declaration: IrClass,
substitutions: Map<IrTypeParameterSymbol, IrTypeArgument>,
- currentlyAnalyzing: Set<IrClassifierSymbol>
+ currentlyAnalyzing: Set<SymbolForAnalysis>
): Stability {
val symbol = declaration.symbol
- if (currentlyAnalyzing.contains(symbol)) return Stability.Unstable
+ val typeArguments = declaration.typeParameters.map { substitutions[it.symbol] }
+ val fullSymbol = SymbolForAnalysis(symbol, typeArguments)
+
+ if (currentlyAnalyzing.contains(fullSymbol)) return Stability.Unstable
if (declaration.hasStableMarkedDescendant()) return Stability.Stable
if (declaration.isEnumClass || declaration.isEnumEntry) return Stability.Stable
if (declaration.defaultType.isPrimitiveType()) return Stability.Stable
@@ -252,7 +260,7 @@
error("Builtins Stub: ${declaration.name}")
}
- val analyzing = currentlyAnalyzing + symbol
+ val analyzing = currentlyAnalyzing + fullSymbol
if (canInferStability(declaration) || declaration.isExternalStableType()) {
val fqName = declaration.fqNameWhenAvailable?.toString() ?: ""
@@ -352,7 +360,7 @@
private fun stabilityOf(
classifier: IrClassifierSymbol,
substitutions: Map<IrTypeParameterSymbol, IrTypeArgument>,
- currentlyAnalyzing: Set<IrClassifierSymbol>
+ currentlyAnalyzing: Set<SymbolForAnalysis>
): Stability {
// if isEnum, return true
// class hasStableAnnotation()
@@ -366,7 +374,7 @@
private fun stabilityOf(
argument: IrTypeArgument,
substitutions: Map<IrTypeParameterSymbol, IrTypeArgument>,
- currentlyAnalyzing: Set<IrClassifierSymbol>
+ currentlyAnalyzing: Set<SymbolForAnalysis>
): Stability {
return when (argument) {
is IrStarProjection -> Stability.Unstable
@@ -378,7 +386,7 @@
private fun stabilityOf(
type: IrType,
substitutions: Map<IrTypeParameterSymbol, IrTypeArgument>,
- currentlyAnalyzing: Set<IrClassifierSymbol>
+ currentlyAnalyzing: Set<SymbolForAnalysis>
): Stability {
return when {
type is IrErrorType -> Stability.Unstable
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
index 2f40798..bec5e9b 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnModifierTest.kt
@@ -16,20 +16,33 @@
package androidx.compose.foundation.layout
+import androidx.compose.foundation.background
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Measured
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
+import kotlin.math.ceil
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -238,6 +251,194 @@
}
}
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun testRow_correctlyCalculatesIntrinsicCrossAxis() {
+ var totalFakeTextPlaced = 0
+
+ rule.setContent {
+ Row(
+ Modifier
+ .width(200.dp)
+ .background(Color.Green)
+ .height(IntrinsicSize.Max)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .background(Color.Blue)
+ )
+
+ Column(Modifier.wrapContentHeight()) {
+ FakeText(modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Text")
+ FlowRow(Modifier) {
+ FakeText(
+ modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Really long text 1")
+ FakeText(modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Really long text 2")
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .width(120.dp)
+ .height(60.dp)
+ .background(Color.Red)
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(totalFakeTextPlaced).isEqualTo(3)
+ }
+ }
+
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun testColumn_correctlyCalculatesIntrinsicCrossAxis() {
+ var totalFakeTextPlaced = 0
+ val forRow = false
+ rule.setContent {
+ Column(
+ Modifier
+ .height(176.dp)
+ .background(Color.Green)
+ .width(IntrinsicSize.Max)
+ ) {
+ Row(Modifier.wrapContentWidth()) {
+ FakeText(modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Text", forRow)
+ FlowColumn(Modifier) {
+ FakeText(
+ modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Really long text 1", forRow)
+ FakeText(modifier = Modifier.onPlaced {
+ totalFakeTextPlaced++
+ }, text = "Really long text 2", forRow)
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .width(60.dp)
+ .height(120.dp)
+ .background(Color.Red)
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(totalFakeTextPlaced).isEqualTo(3)
+ }
+ }
+
+ /**
+ * @param forRow creates the bug setting for row. Otherwise, make it work for Column
+ * by laying out the text top to bottom.
+ */
+ @Composable
+ fun FakeText(modifier: Modifier = Modifier, text: String, forRow: Boolean = true) {
+ val characterSizeMainAxis = 8.dp
+ val textCrossAxisSize = 30.dp
+
+ val maxIntrinsicMainAxisSize = (characterSizeMainAxis * text.length)
+ val orientation = if (forRow) LayoutOrientation.Horizontal else LayoutOrientation.Vertical
+ Layout(
+ content = {},
+ modifier = modifier,
+ measurePolicy = object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ val constraintsIndependent = OrientationIndependentConstraints(
+ constraints,
+ orientation
+ )
+ val maxMainAxis = constraintsIndependent.mainAxisMax
+ val lengthNeeded = text.length * characterSizeMainAxis.roundToPx()
+ val crossAxis = getCrossAxisNeeded(maxMainAxis)
+ val mainAxis = lengthNeeded.coerceAtMost(maxMainAxis)
+
+ var width: Int
+ var height: Int
+ if (forRow) {
+ width = mainAxis
+ height = crossAxis
+ } else {
+ width = crossAxis
+ height = mainAxis
+ }
+
+ return layout(width, height) {
+ measurables.forEach { measurable ->
+ val placeable = measurable.measure(constraints)
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int {
+ return if (forRow) {
+ getCrossAxisNeeded(width)
+ } else {
+ maxIntrinsicMainAxisSize.roundToPx()
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int {
+ return if (forRow) {
+ maxIntrinsicMainAxisSize.roundToPx()
+ } else {
+ getCrossAxisNeeded(height)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int {
+ return if (forRow) {
+ getCrossAxisNeeded(width)
+ } else {
+ characterSizeMainAxis.roundToPx()
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int {
+ return if (forRow) {
+ characterSizeMainAxis.roundToPx()
+ } else {
+ getCrossAxisNeeded(height)
+ }
+ }
+
+ private fun IntrinsicMeasureScope.getCrossAxisNeeded(mainAxisSize: Int): Int {
+ val lengthNeeded = text.length * characterSizeMainAxis.roundToPx()
+ val noOfLines = if (mainAxisSize == Constraints.Infinity) 1 else
+ ceil((lengthNeeded.toFloat() / mainAxisSize).toDouble()).toInt()
+ return (textCrossAxisSize.roundToPx() * noOfLines)
+ }
+ }
+ )
+ }
+
@Test
fun testColumn_updatesOnAlignmentChange() {
var positionInParentX = 0f
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index 953637a..a6c7d31 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -491,7 +491,7 @@
): Int {
return intrinsicCrossAxisSize(
measurables,
- { w -> maxIntrinsicHeight(w) },
+ { w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableHeight,
mainAxisSpacing,
@@ -504,7 +504,7 @@
): Int {
return intrinsicCrossAxisSize(
measurables,
- { h -> maxIntrinsicWidth(h) },
+ { h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableWidth,
mainAxisSpacing,
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index a59781b..962bffe 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -412,10 +412,12 @@
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
method public float getLastVelocity();
method public float getOffset();
- method @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method @Deprecated @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method public T getSettledValue();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getSnapAnimationSpec();
method public T getTargetValue();
method public boolean isAnimationRunning();
+ method @FloatRange(from=0.0, to=1.0) public float progress(T from, T to);
method public float requireOffset();
method public suspend Object? settle(float velocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
method public void updateAnchors(androidx.compose.foundation.gestures.DraggableAnchors<T> newAnchors, optional T newTarget);
@@ -425,7 +427,8 @@
property public final boolean isAnimationRunning;
property public final float lastVelocity;
property public final float offset;
- property @FloatRange(from=0.0, to=1.0) public final float progress;
+ property @Deprecated @FloatRange(from=0.0, to=1.0) public final float progress;
+ property public final T settledValue;
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec;
property public final T targetValue;
field public static final androidx.compose.foundation.gestures.AnchoredDraggableState.Companion Companion;
@@ -452,13 +455,13 @@
}
public final class DragGestureDetectorKt {
- method public static suspend Object? awaitDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitHorizontalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitHorizontalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitLongPressOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitVerticalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitVerticalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+ method public static suspend Object? awaitDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitHorizontalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitHorizontalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitLongPressOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitVerticalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitVerticalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend Object? detectDragGesturesAfterLongPress(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -581,8 +584,8 @@
method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
method @Deprecated public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onTap, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method @Deprecated public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+ method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method @Deprecated public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
}
@androidx.compose.runtime.Stable public interface TargetedFlingBehavior extends androidx.compose.foundation.gestures.FlingBehavior {
@@ -1067,9 +1070,9 @@
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static interface LazyLayoutIntervalContent.Interval {
method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? getKey();
- method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> getType();
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object?> getType();
property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? key;
- property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> type;
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object?> type;
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 6d06791..6990af6 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -414,10 +414,12 @@
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
method public float getLastVelocity();
method public float getOffset();
- method @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method @Deprecated @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method public T getSettledValue();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getSnapAnimationSpec();
method public T getTargetValue();
method public boolean isAnimationRunning();
+ method @FloatRange(from=0.0, to=1.0) public float progress(T from, T to);
method public float requireOffset();
method public suspend Object? settle(float velocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
method public void updateAnchors(androidx.compose.foundation.gestures.DraggableAnchors<T> newAnchors, optional T newTarget);
@@ -427,7 +429,8 @@
property public final boolean isAnimationRunning;
property public final float lastVelocity;
property public final float offset;
- property @FloatRange(from=0.0, to=1.0) public final float progress;
+ property @Deprecated @FloatRange(from=0.0, to=1.0) public final float progress;
+ property public final T settledValue;
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec;
property public final T targetValue;
field public static final androidx.compose.foundation.gestures.AnchoredDraggableState.Companion Companion;
@@ -454,13 +457,13 @@
}
public final class DragGestureDetectorKt {
- method public static suspend Object? awaitDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitHorizontalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitHorizontalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitLongPressOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitVerticalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method public static suspend Object? awaitVerticalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+ method public static suspend Object? awaitDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitHorizontalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitHorizontalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitLongPressOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitVerticalDragOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method public static suspend Object? awaitVerticalTouchSlopOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend Object? detectDragGesturesAfterLongPress(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStart, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -583,8 +586,8 @@
method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
method @Deprecated public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onTap, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
- method @Deprecated public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+ method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
+ method @Deprecated public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange?>);
}
@androidx.compose.runtime.Stable public interface TargetedFlingBehavior extends androidx.compose.foundation.gestures.FlingBehavior {
@@ -1069,9 +1072,9 @@
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static interface LazyLayoutIntervalContent.Interval {
method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? getKey();
- method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> getType();
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object?> getType();
property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? key;
- property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> type;
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object?> type;
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
index 7a2cb4e..ddc8782 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.samples.AnchoredDraggableCatchAnimatingWidgetSample
import androidx.compose.foundation.samples.AnchoredDraggableCustomAnchoredSample
import androidx.compose.foundation.samples.AnchoredDraggableLayoutDependentAnchorsSample
+import androidx.compose.foundation.samples.AnchoredDraggableProgressSample
import androidx.compose.foundation.samples.AnchoredDraggableWithOverscrollSample
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@@ -44,5 +45,7 @@
AnchoredDraggableCustomAnchoredSample()
Spacer(Modifier.height(50.dp))
AnchoredDraggableWithOverscrollSample()
+ Spacer(Modifier.height(50.dp))
+ AnchoredDraggableProgressSample()
}
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/build.gradle b/compose/foundation/foundation/integration-tests/lazy-tests/build.gradle
new file mode 100644
index 0000000..b217966
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/build.gradle
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 21
+ }
+ namespace "androidx.compose.foundation.lazytests"
+}
+
+dependencies {
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":internal-testutils-fonts"))
+ androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(project(":internal-testutils-runtime"))
+ androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
+ androidTestImplementation("androidx.savedstate:savedstate:1.2.1")
+
+ androidTestImplementation(libs.kotlinTest)
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.testUiautomator)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testMonitor)
+ androidTestImplementation(libs.espressoCore)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/lint-baseline.xml b/compose/foundation/foundation/integration-tests/lazy-tests/lint-baseline.xml
new file mode 100644
index 0000000..cf9f22c
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/lint-baseline.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" Thread.sleep(5)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt"/>
+ </issue>
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" Thread.sleep(5)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt"/>
+ </issue>
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" Thread.sleep(5)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt"/>
+ </issue>
+
+</issues>
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt
new file mode 100644
index 0000000..b889918
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/AutoTestFrameClock.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.compose.foundation
+
+import androidx.compose.runtime.MonotonicFrameClock
+import java.util.concurrent.atomic.AtomicLong
+
+class AutoTestFrameClock : MonotonicFrameClock {
+ private val time = AtomicLong(0)
+
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+ return onFrame(time.getAndAdd(16_000_000))
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
new file mode 100644
index 0000000..571c316
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2022 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.compose.foundation
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.scrollBy
+import androidx.compose.runtime.Stable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+
+open class BaseLazyLayoutTestWithOrientation(private val orientation: Orientation) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val vertical: Boolean
+ get() = orientation == Orientation.Vertical
+
+ @Stable
+ fun Modifier.crossAxisSize(size: Dp) =
+ if (vertical) {
+ this.width(size)
+ } else {
+ this.height(size)
+ }
+
+ @Stable
+ fun Modifier.mainAxisSize(size: Dp) =
+ if (vertical) {
+ this.height(size)
+ } else {
+ this.width(size)
+ }
+
+ @Stable
+ fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
+ if (vertical) {
+ this.size(crossAxis, mainAxis)
+ } else {
+ this.size(mainAxis, crossAxis)
+ }
+
+ fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
+ if (vertical) {
+ this.scrollBy(y = distance, density = rule.density)
+ } else {
+ this.scrollBy(x = distance, density = rule.density)
+ }
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertHeightIsEqualTo(expectedSize)
+ } else {
+ assertWidthIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertWidthIsEqualTo(expectedSize)
+ } else {
+ assertHeightIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+ val position = if (vertical) {
+ getUnclippedBoundsInRoot().top
+ } else {
+ getUnclippedBoundsInRoot().left
+ }
+ position.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertAxisBounds(
+ offset: DpOffset,
+ size: DpSize
+ ) =
+ assertMainAxisStartPositionInRootIsEqualTo(offset.y)
+ .assertCrossAxisStartPositionInRootIsEqualTo(offset.x)
+ .assertMainAxisSizeIsEqualTo(size.height)
+ .assertCrossAxisSizeIsEqualTo(size.width)
+
+ fun PaddingValues(
+ mainAxis: Dp = 0.dp,
+ crossAxis: Dp = 0.dp
+ ) = PaddingValues(
+ beforeContent = mainAxis,
+ afterContent = mainAxis,
+ beforeContentCrossAxis = crossAxis,
+ afterContentCrossAxis = crossAxis
+ )
+
+ fun PaddingValues(
+ beforeContent: Dp = 0.dp,
+ afterContent: Dp = 0.dp,
+ beforeContentCrossAxis: Dp = 0.dp,
+ afterContentCrossAxis: Dp = 0.dp,
+ ) = if (vertical) {
+ androidx.compose.foundation.layout.PaddingValues(
+ start = beforeContentCrossAxis,
+ top = beforeContent,
+ end = afterContentCrossAxis,
+ bottom = afterContent
+ )
+ } else {
+ androidx.compose.foundation.layout.PaddingValues(
+ start = beforeContent,
+ top = beforeContentCrossAxis,
+ end = afterContent,
+ bottom = afterContentCrossAxis
+ )
+ }
+
+ internal fun Modifier.debugBorder(color: Color = Color.Black) = border(1.dp, color)
+
+ companion object {
+ internal const val FrameDuration = 16L
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/ScrollableUtils.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/ScrollableUtils.kt
new file mode 100644
index 0000000..18eedb5
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/ScrollableUtils.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
+import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.util.fastForEach
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.CoordinatesProvider
+import androidx.test.espresso.action.GeneralLocation
+import androidx.test.espresso.action.GeneralSwipeAction
+import androidx.test.espresso.action.Press
+import androidx.test.espresso.action.Swipe
+import kotlinx.coroutines.coroutineScope
+import org.hamcrest.CoreMatchers
+
+// Very low tolerance on the difference
+internal val VelocityTrackerCalculationThreshold = 1
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEvents(
+ tracker: VelocityTracker,
+ pointerInputScope: PointerInputScope
+) {
+ if (VelocityTrackerAddPointsFix) {
+ savePointerInputEventsWithFix(tracker, pointerInputScope)
+ } else {
+ savePointerInputEventsLegacy(tracker, pointerInputScope)
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEventsWithFix(
+ tracker: VelocityTracker,
+ pointerInputScope: PointerInputScope
+) {
+ with(pointerInputScope) {
+ coroutineScope {
+ awaitPointerEventScope {
+ while (true) {
+ var event: PointerInputChange? = awaitFirstDown()
+ while (event != null && !event.changedToUpIgnoreConsumed()) {
+ val currentEvent = awaitPointerEvent().changes
+ .firstOrNull()
+
+ if (currentEvent != null && !currentEvent.changedToUpIgnoreConsumed()) {
+ currentEvent.historical.fastForEach {
+ tracker.addPosition(it.uptimeMillis, it.position)
+ }
+ tracker.addPosition(
+ currentEvent.uptimeMillis,
+ currentEvent.position
+ )
+ }
+
+ event = currentEvent
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal suspend fun savePointerInputEventsLegacy(
+ tracker: VelocityTracker,
+ pointerInputScope: PointerInputScope
+) {
+ with(pointerInputScope) {
+ coroutineScope {
+ awaitPointerEventScope {
+ while (true) {
+ var event = awaitFirstDown()
+ tracker.addPosition(event.uptimeMillis, event.position)
+ while (!event.changedToUpIgnoreConsumed()) {
+ val currentEvent = awaitPointerEvent().changes
+ .firstOrNull()
+
+ if (currentEvent != null) {
+ currentEvent.historical.fastForEach {
+ tracker.addPosition(it.uptimeMillis, it.position)
+ }
+ tracker.addPosition(
+ currentEvent.uptimeMillis,
+ currentEvent.position
+ )
+ event = currentEvent
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+internal fun composeViewSwipeUp() {
+ Espresso.onView(CoreMatchers.allOf(CoreMatchers.instanceOf(AbstractComposeView::class.java)))
+ .perform(
+ espressoSwipe(
+ GeneralLocation.CENTER,
+ GeneralLocation.TOP_CENTER
+ )
+ )
+}
+
+internal fun composeViewSwipeDown() {
+ Espresso.onView(CoreMatchers.allOf(CoreMatchers.instanceOf(AbstractComposeView::class.java)))
+ .perform(
+ espressoSwipe(
+ GeneralLocation.CENTER,
+ GeneralLocation.BOTTOM_CENTER
+ )
+ )
+}
+
+internal fun composeViewSwipeLeft() {
+ Espresso.onView(CoreMatchers.allOf(CoreMatchers.instanceOf(AbstractComposeView::class.java)))
+ .perform(
+ espressoSwipe(
+ GeneralLocation.CENTER,
+ GeneralLocation.CENTER_LEFT
+ )
+ )
+}
+
+internal fun composeViewSwipeRight() {
+ Espresso.onView(CoreMatchers.allOf(CoreMatchers.instanceOf(AbstractComposeView::class.java)))
+ .perform(
+ espressoSwipe(
+ GeneralLocation.CENTER,
+ GeneralLocation.CENTER_RIGHT
+ )
+ )
+}
+
+private fun espressoSwipe(
+ start: CoordinatesProvider,
+ end: CoordinatesProvider
+): GeneralSwipeAction {
+ return GeneralSwipeAction(
+ Swipe.FAST, start, end,
+ Press.FINGER
+ )
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
new file mode 100644
index 0000000..0bf7bf5
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+open class BaseLazyGridTestWithOrientation(
+ orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
+
+ fun LazyGridState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ fun LazyGridState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ scrollToItem(index)
+ }
+ }
+
+ fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollMainAxisBy(offset)
+
+ @Composable
+ fun LazyGrid(
+ cells: Int,
+ modifier: Modifier = Modifier,
+ state: LazyGridState = rememberLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ reverseArrangement: Boolean = false,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ crossAxisSpacedBy: Dp = 0.dp,
+ mainAxisSpacedBy: Dp = 0.dp,
+ content: LazyGridScope.() -> Unit
+ ) = LazyGrid(
+ GridCells.Fixed(cells),
+ modifier,
+ state,
+ contentPadding,
+ reverseLayout,
+ reverseArrangement,
+ flingBehavior,
+ userScrollEnabled,
+ crossAxisSpacedBy,
+ mainAxisSpacedBy,
+ content
+ )
+
+ @Composable
+ fun LazyGrid(
+ cells: GridCells,
+ modifier: Modifier = Modifier,
+ state: LazyGridState = rememberLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ reverseArrangement: Boolean = false,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ crossAxisSpacedBy: Dp = 0.dp,
+ mainAxisSpacedBy: Dp = 0.dp,
+ content: LazyGridScope.() -> Unit
+ ) {
+ if (vertical) {
+ val verticalArrangement = when {
+ mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.Bottom
+ else -> Arrangement.Top
+ }
+ val horizontalArrangement = when {
+ crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+ else -> Arrangement.Start
+ }
+ LazyVerticalGrid(
+ columns = cells,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ content = content
+ )
+ } else {
+ val horizontalArrangement = when {
+ mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.End
+ else -> Arrangement.Start
+ }
+ val verticalArrangement = when {
+ crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+ else -> Arrangement.Top
+ }
+ LazyHorizontalGrid(
+ rows = cells,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ horizontalArrangement = horizontalArrangement,
+ verticalArrangement = verticalArrangement,
+ content = content
+ )
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
index b26ab18..b94d36c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.grid
import android.os.Build
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazySemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
new file mode 100644
index 0000000..a1110337
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
+package androidx.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
+import androidx.compose.foundation.composeViewSwipeDown
+import androidx.compose.foundation.composeViewSwipeLeft
+import androidx.compose.foundation.composeViewSwipeRight
+import androidx.compose.foundation.composeViewSwipeUp
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyList
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+open class BaseLazyListTestWithOrientation(
+ private val orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
+
+ fun Modifier.fillMaxCrossAxis() =
+ if (vertical) {
+ this.fillMaxWidth()
+ } else {
+ this.fillMaxHeight()
+ }
+
+ fun LazyItemScope.fillParentMaxMainAxis() =
+ if (vertical) {
+ Modifier.fillParentMaxHeight()
+ } else {
+ Modifier.fillParentMaxWidth()
+ }
+
+ fun LazyItemScope.fillParentMaxCrossAxis() =
+ if (vertical) {
+ Modifier.fillParentMaxWidth()
+ } else {
+ Modifier.fillParentMaxHeight()
+ }
+
+ fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ fun LazyListState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ scrollToItem(index)
+ }
+ }
+
+ fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollBy(
+ x = if (vertical) 0.dp else offset,
+ y = if (!vertical) 0.dp else offset,
+ density = rule.density
+ )
+
+ fun composeViewSwipeForward() {
+ if (orientation == Orientation.Vertical) {
+ composeViewSwipeUp()
+ } else {
+ composeViewSwipeLeft()
+ }
+ }
+
+ fun composeViewSwipeBackward() {
+ if (orientation == Orientation.Vertical) {
+ composeViewSwipeDown()
+ } else {
+ composeViewSwipeRight()
+ }
+ }
+
+ fun Velocity.toFloat(): Float {
+ return if (orientation == Orientation.Vertical) y else x
+ }
+
+ @Composable
+ fun LazyColumnOrRow(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ reverseArrangement: Boolean = false,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ spacedBy: Dp = 0.dp,
+ content: LazyListScope.() -> Unit
+ ) {
+ if (vertical) {
+ val verticalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.Bottom
+ else -> Arrangement.Top
+ }
+ LazyColumn(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ verticalArrangement = verticalArrangement,
+ content = content
+ )
+ } else {
+ val horizontalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.End
+ else -> Arrangement.Start
+ }
+ LazyRow(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ horizontalArrangement = horizontalArrangement,
+ content = content
+ )
+ }
+ }
+
+ @Composable
+ fun LazyColumnOrRow(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ reverseArrangement: Boolean = false,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ spacedBy: Dp = 0.dp,
+ beyondBoundsItemCount: Int,
+ content: LazyListScope.() -> Unit
+ ) {
+ if (vertical) {
+ val verticalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.Bottom
+ else -> Arrangement.Top
+ }
+ LazyColumn(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ verticalArrangement = verticalArrangement,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content
+ )
+ } else {
+ val horizontalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ reverseLayout xor reverseArrangement -> Arrangement.End
+ else -> Arrangement.Start
+ }
+ LazyRow(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ horizontalArrangement = horizontalArrangement,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content
+ )
+ }
+ }
+}
+
+@Composable
+private fun LazyColumn(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ beyondBoundsItemCount: Int,
+ content: LazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ flingBehavior = flingBehavior,
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ isVertical = true,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content
+ )
+}
+
+@Composable
+private fun LazyRow(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ beyondBoundsItemCount: Int,
+ content: LazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ isVertical = false,
+ flingBehavior = flingBehavior,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content
+ )
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
index 3dadcfb..96d5b43 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import android.os.Build
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsItemCountTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
index 587e98b..b99984c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListHeadersTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
index d323292..9350a50 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import android.os.Build
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
index 94f0250..f64b86a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index da3f43f..0fda009 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import android.os.Build
@@ -49,7 +51,6 @@
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.savePointerInputEvents
import androidx.compose.foundation.text.BasicText
-import androidx.compose.foundation.text.matchers.isZero
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -2214,7 +2215,7 @@
.scrollMainAxisBy(250.dp) // 10 items, half a screen
rule.runOnIdle {
- assertThat(composedMoreThanOnce).isZero()
+ assertThat(composedMoreThanOnce).isEqualTo(0)
assertTrue(
"Items are expected to be composed only once.",
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
similarity index 98%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
index 0f07e16..d239d15 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsIndexedTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import androidx.compose.foundation.gestures.FlingBehavior
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyRowTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazySemanticsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateItemPlacementTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridArrangementsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridCustomKeysTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
similarity index 97%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
index 3c15d97..fa7fe4f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.staggeredgrid
import com.google.common.truth.Truth.assertThat
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLayoutInfoTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridReverseLayoutTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
similarity index 100%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
similarity index 99%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index a376481..fc376b4d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.AutoTestFrameClock
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
index 69c8223..81a89a8 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
@@ -30,25 +30,39 @@
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.overscroll
+import androidx.compose.foundation.samples.AnchoredDraggableSampleValue.Center
+import androidx.compose.foundation.samples.AnchoredDraggableSampleValue.End
+import androidx.compose.foundation.samples.AnchoredDraggableSampleValue.HalfEnd
+import androidx.compose.foundation.samples.AnchoredDraggableSampleValue.HalfStart
+import androidx.compose.foundation.samples.AnchoredDraggableSampleValue.Start
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import kotlin.math.max
import kotlin.math.roundToInt
private enum class AnchoredDraggableSampleValue {
- Start, Center, End
+ Start, HalfStart, Center, HalfEnd, End
}
@Composable
@@ -69,7 +83,7 @@
)
) {
AnchoredDraggableState(
- initialValue = AnchoredDraggableSampleValue.Center,
+ initialValue = Center,
positionalThreshold,
velocityThreshold,
snapAnimationSpec,
@@ -83,9 +97,9 @@
SideEffect {
state.updateAnchors(
DraggableAnchors {
- AnchoredDraggableSampleValue.Start at 0f
- AnchoredDraggableSampleValue.Center at containerWidthPx / 2f
- AnchoredDraggableSampleValue.End at containerWidthPx
+ Start at 0f
+ Center at containerWidthPx / 2f
+ End at containerWidthPx
}
)
}
@@ -124,14 +138,14 @@
)
) {
AnchoredDraggableState(
- initialValue = AnchoredDraggableSampleValue.Center,
+ initialValue = Center,
positionalThreshold,
velocityThreshold,
snapAnimationSpec,
decayAnimationSpec
)
}
- val draggableSize = 100.dp
+ val draggableSize = 60.dp
val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() }
Box(
Modifier
@@ -142,16 +156,19 @@
val dragEndPoint = layoutSize.width - draggableSizePx
state.updateAnchors(
DraggableAnchors {
- AnchoredDraggableSampleValue.Start at 0f
- AnchoredDraggableSampleValue.Center at dragEndPoint / 2f
- AnchoredDraggableSampleValue.End at dragEndPoint
+ Start at 0f
+ HalfStart at dragEndPoint * .25f
+ Center at dragEndPoint * .5f
+ HalfEnd at dragEndPoint * .75f
+ End at dragEndPoint
}
)
}
+ .visualizeDraggableAnchors(state, Orientation.Horizontal)
) {
Box(
Modifier
- .size(100.dp)
+ .size(draggableSize)
.offset {
IntOffset(
x = state
@@ -205,7 +222,7 @@
// or drag the settling box.
val snapAnimationSpec = tween<Float>(durationMillis = 3000)
val state = AnchoredDraggableState(
- initialValue = AnchoredDraggableSampleValue.Start,
+ initialValue = Start,
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { with(density) { 125.dp.toPx() } },
snapAnimationSpec = snapAnimationSpec,
@@ -221,8 +238,8 @@
val dragEndPoint = layoutSize.width - draggableSizePx
state.updateAnchors(
DraggableAnchors {
- AnchoredDraggableSampleValue.Start at 0f
- AnchoredDraggableSampleValue.End at dragEndPoint
+ Start at 0f
+ End at dragEndPoint
}
)
}
@@ -266,7 +283,7 @@
)
) {
AnchoredDraggableState(
- initialValue = AnchoredDraggableSampleValue.Center,
+ initialValue = Center,
positionalThreshold,
velocityThreshold,
animationSpec,
@@ -281,9 +298,9 @@
val dragEndPoint = layoutSize.width - draggableSizePx
state.updateAnchors(
DraggableAnchors {
- AnchoredDraggableSampleValue.Start at 0f
- AnchoredDraggableSampleValue.Center at dragEndPoint / 2f
- AnchoredDraggableSampleValue.End at dragEndPoint
+ Start at 0f
+ Center at dragEndPoint / 2f
+ End at dragEndPoint
}
)
}
@@ -309,3 +326,115 @@
)
}
}
+
+@Composable
+fun AnchoredDraggableProgressSample() {
+ val density = LocalDensity.current
+ val snapAnimationSpec = tween<Float>()
+ val decayAnimationSpec = rememberSplineBasedDecay<Float>()
+ val positionalThreshold = { distance: Float -> distance * 0.5f }
+ val velocityThreshold = { with(density) { 125.dp.toPx() } }
+ val state = rememberSaveable(
+ density,
+ saver = AnchoredDraggableState.Saver(
+ snapAnimationSpec,
+ decayAnimationSpec,
+ positionalThreshold,
+ velocityThreshold
+ )
+ ) {
+ AnchoredDraggableState(
+ initialValue = Center,
+ positionalThreshold,
+ velocityThreshold,
+ snapAnimationSpec,
+ decayAnimationSpec
+ )
+ }
+ val draggableSize = 60.dp
+ val draggableSizePx = with(LocalDensity.current) { draggableSize.toPx() }
+ Column(
+ Modifier
+ .fillMaxWidth()
+ // Our anchors depend on this box's size, so we obtain the size from onSizeChanged and
+ // use updateAnchors to let the state know about the new anchors
+ .onSizeChanged { layoutSize ->
+ val dragEndPoint = layoutSize.width - draggableSizePx
+ state.updateAnchors(
+ DraggableAnchors {
+ Start at 0f
+ Center at dragEndPoint * .5f
+ End at dragEndPoint
+ }
+ )
+ }
+ ) {
+ // Read progress in a snapshot-backed context to receive updates. This could be e.g. a
+ // derived state, snapshotFlow or other snapshot-aware context like the graphicsLayer
+ // block.
+ val centerToStartProgress by derivedStateOf { state.progress(from = Center, to = Start) }
+ val centerToEndProgress by derivedStateOf { state.progress(from = Center, to = End) }
+ Box {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(draggableSize)
+ .graphicsLayer { alpha = max(centerToStartProgress, centerToEndProgress) }
+ .background(Color.Black)
+ )
+ Box(
+ Modifier
+ .size(draggableSize)
+ .offset {
+ IntOffset(
+ x = state
+ .requireOffset()
+ .roundToInt(), y = 0
+ )
+ }
+ .anchoredDraggable(state, Orientation.Horizontal)
+ .background(Color.Red)
+ )
+ }
+ }
+}
+
+/**
+ * A [Modifier] that visualizes the anchors attached to an [AnchoredDraggableState] as lines along
+ * the cross axis of the layout (start to end for [Orientation.Vertical], top to end for
+ * [Orientation.Horizontal]).
+ * This is useful to debug components with a complex set of anchors, or for AnchoredDraggable
+ * development.
+ *
+ * @param state The state whose anchors to visualize
+ * @param orientation The orientation of the [anchoredDraggable]
+ * @param lineColor The color of the visualization lines
+ * @param lineStrokeWidth The stroke width of the visualization lines
+ * @param linePathEffect The path effect used to draw the visualization lines
+ */
+private fun Modifier.visualizeDraggableAnchors(
+ state: AnchoredDraggableState<*>,
+ orientation: Orientation,
+ lineColor: Color = Color.Black,
+ lineStrokeWidth: Float = 10f,
+ linePathEffect: PathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 30f))
+) = drawWithContent {
+ drawContent()
+ state.anchors.forEach { _, position ->
+ val startOffset = Offset(
+ x = if (orientation == Orientation.Horizontal) position else 0f,
+ y = if (orientation == Orientation.Vertical) position else 0f
+ )
+ val endOffset = Offset(
+ x = if (orientation == Orientation.Horizontal) startOffset.x else size.height,
+ y = if (orientation == Orientation.Vertical) startOffset.y else size.width
+ )
+ drawLine(
+ color = lineColor,
+ start = startOffset,
+ end = endOffset,
+ strokeWidth = lineStrokeWidth,
+ pathEffect = linePathEffect
+ )
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
index 571c316..0425e5a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
@@ -20,10 +20,10 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.grid.scrollBy
import androidx.compose.runtime.Stable
import androidx.compose.testutils.assertIsEqualTo
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsEqualTo
@@ -32,6 +32,9 @@
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
@@ -69,6 +72,22 @@
this.size(mainAxis, crossAxis)
}
+ fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
+ performTouchInput {
+ with(density) {
+ val touchSlop = TestTouchSlop.toInt()
+ val xPx = x.roundToPx()
+ val yPx = y.roundToPx()
+ val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
+ val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
+ swipeWithVelocity(
+ start = center,
+ end = Offset(center.x - offsetX, center.y - offsetY),
+ endVelocity = 0f
+ )
+ }
+ }
+
fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
if (vertical) {
this.scrollBy(y = distance, density = rule.density)
diff --git a/appcompat/appcompat/src/main/java/DeleteMe.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TouchUtils.kt
similarity index 81%
rename from appcompat/appcompat/src/main/java/DeleteMe.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TouchUtils.kt
index 38f8b7a..87d8319 100644
--- a/appcompat/appcompat/src/main/java/DeleteMe.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TouchUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,4 +14,6 @@
* limitations under the License.
*/
-// This file exists to trick AGP/lint to work around b/234865137
+package androidx.compose.foundation
+
+internal const val TestTouchSlop = 18f
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index 72464a7..a5c32eb 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -92,8 +92,8 @@
)
val anchors = DraggableAnchors {
A at 0f
- B at 250f
- C at 500f
+ B at AnchoredDraggableBoxSize.value / 2f
+ C at AnchoredDraggableBoxSize.value
}
state.updateAnchors(anchors)
@@ -164,8 +164,8 @@
)
val anchors = DraggableAnchors {
A at 0f
- B at 250f
- C at 500f
+ B at AnchoredDraggableBoxSize.value / 2f
+ C at AnchoredDraggableBoxSize.value
}
state.updateAnchors(anchors)
@@ -870,7 +870,7 @@
val velocityThreshold = 100.dp
val state = AnchoredDraggableState(
initialValue = A,
- velocityThreshold = { with(rule.density) { velocityThreshold.toPx() } },
+ velocityThreshold = { 0f },
positionalThreshold = { Float.POSITIVE_INFINITY },
snapAnimationSpec = tween(),
decayAnimationSpec = DefaultDecayAnimationSpec
@@ -909,13 +909,13 @@
.performTouchInput {
swipeWithVelocity(
start = Offset(left, 0f),
- end = Offset(right / 2, 0f),
+ end = Offset(right / 4, 0f),
endVelocity = with(rule.density) { velocityThreshold.toPx() } * 0.9f
)
}
rule.waitForIdle()
- assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.settledValue).isEqualTo(A)
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
index 58a4c3f..84885e5 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -53,7 +53,6 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.withFrameNanos
-import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
@@ -286,66 +285,83 @@
}
@Test
- fun anchoredDraggable_progress_matchesSwipePosition() {
+ fun anchoredDraggable_progress() {
+ rule.mainClock.autoAdvance = false
+ val animationDuration = 320
+ val frameLengthMillis = 16
+ val amountOfFramesForAnimation = animationDuration / frameLengthMillis
val state = AnchoredDraggableState(
initialValue = A,
- positionalThreshold = defaultPositionalThreshold,
+ snapAnimationSpec = tween(animationDuration, easing = LinearEasing),
+ positionalThreshold = { distance -> distance * 0.5f },
velocityThreshold = defaultVelocityThreshold,
- snapAnimationSpec = defaultAnimationSpec,
decayAnimationSpec = defaultDecayAnimationSpec
)
+ lateinit var scope: CoroutineScope
rule.setContent {
- WithTouchSlop(touchSlop = 0f) {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier
- .requiredSize(AnchoredDraggableBoxSize)
- .testTag(AnchoredDraggableTestTag)
- .anchoredDraggable(
- state = state,
- orientation = Orientation.Vertical
+ scope = rememberCoroutineScope()
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier
+ .requiredSize(AnchoredDraggableBoxSize)
+ .testTag(AnchoredDraggableTestTag)
+ .anchoredDraggable(
+ state = state,
+ orientation = Orientation.Vertical
+ )
+ .onSizeChanged { layoutSize ->
+ state.updateAnchors(
+ DraggableAnchors {
+ A at 0f
+ B at layoutSize.width / 2f
+ C at layoutSize.width.toFloat()
+ }
)
- .onSizeChanged { layoutSize ->
- state.updateAnchors(
- DraggableAnchors {
- A at 0f
- B at layoutSize.width / 2f
- C at layoutSize.width.toFloat()
- }
- )
- }
- .offset {
- IntOffset(
- state
- .requireOffset()
- .roundToInt(), 0
- )
- }
- .background(Color.Red)
- )
- }
+ }
+ .offset {
+ IntOffset(
+ state
+ .requireOffset()
+ .roundToInt(), 0
+ )
+ }
+ .background(Color.Red)
+ )
}
}
- val anchorA = state.anchors.positionOf(A)
- val anchorB = state.anchors.positionOf(B)
- val almostAnchorB = anchorB * 0.9f
- var expectedProgress = almostAnchorB / (anchorB - anchorA)
-
- rule.onNodeWithTag(AnchoredDraggableTestTag)
- .performTouchInput { swipeDown(endY = almostAnchorB) }
-
- assertThat(state.targetValue).isEqualTo(B)
- assertThat(state.progress).isEqualTo(expectedProgress)
-
- val almostAnchorA = anchorA + ((anchorB - anchorA) * 0.1f)
- expectedProgress = 1 - (almostAnchorA / (anchorB - anchorA))
-
- rule.onNodeWithTag(AnchoredDraggableTestTag)
- .performTouchInput { swipeUp(startY = anchorB, endY = almostAnchorA) }
-
+ assertThat(state.currentValue).isEqualTo(A)
assertThat(state.targetValue).isEqualTo(A)
- assertThat(state.progress).isEqualTo(expectedProgress)
+ assertThat(state.progress(from = A, to = B)).isEqualTo(0f)
+
+ scope.launch { state.animateTo(B) }
+ rule.mainClock.advanceTimeByFrame() // Start dispatching and running the animation
+
+ repeat(amountOfFramesForAnimation) { frame ->
+ val frameFraction = (frame / amountOfFramesForAnimation.toFloat())
+ val hiddenToHalfExpandedProgress = state.progress(from = A, to = B)
+ val hiddenToExpandedProgress = state.progress(from = A, to = C)
+ assertThat(hiddenToHalfExpandedProgress).isWithin(0.001f).of(frameFraction)
+ assertThat(hiddenToExpandedProgress).isWithin(0.001f).of(frameFraction / 2f)
+ rule.mainClock.advanceTimeByFrame()
+ }
+
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+
+ scope.launch { state.animateTo(A) }
+ rule.mainClock.advanceTimeByFrame() // Start dispatching and running the animation
+
+ repeat(amountOfFramesForAnimation) { frame ->
+ val frameFraction = (frame / amountOfFramesForAnimation.toFloat())
+ val aToBProgress = state.progress(from = A, to = B)
+ val aToCProgress = state.progress(from = A, to = C)
+
+ assertThat(aToBProgress).isWithin(0.001f).of(1 - frameFraction)
+ assertThat(aToCProgress).isWithin(0.001f).of(0.5f - (frameFraction / 2f))
+ rule.mainClock.advanceTimeByFrame()
+ }
}
@Test
@@ -498,8 +514,9 @@
decayAnimationSpec = defaultDecayAnimationSpec
)
}
- LaunchedEffect(state.progress) {
- progress = state.progress
+ val latestProgress = state.progress(from = A, to = B)
+ LaunchedEffect(latestProgress) {
+ progress = latestProgress
}
Box(Modifier.fillMaxSize()) {
Box(
@@ -880,31 +897,32 @@
}
@Test
- fun anchoredDraggable_customDrag_updatesOffset() = runBlocking {
+ fun anchoredDraggable_customDrag_snapsToClosestAnchor() = runBlocking {
val state = AnchoredDraggableState(
initialValue = A,
positionalThreshold = defaultPositionalThreshold,
velocityThreshold = defaultVelocityThreshold,
snapAnimationSpec = defaultAnimationSpec,
- decayAnimationSpec = defaultDecayAnimationSpec
+ decayAnimationSpec = defaultDecayAnimationSpec,
+ anchors = DraggableAnchors {
+ A at 0f
+ B at 200f
+ C at 300f
+ }
)
- val anchors = DraggableAnchors {
- A at 0f
- B at 200f
- C at 300f
- }
- state.updateAnchors(anchors)
state.anchoredDrag {
dragTo(150f)
}
- assertThat(state.requireOffset()).isEqualTo(150f)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.requireOffset()).isEqualTo(200f)
state.anchoredDrag {
- dragTo(250f)
+ dragTo(260f)
}
- assertThat(state.requireOffset()).isEqualTo(250f)
+ assertThat(state.currentValue).isEqualTo(C)
+ assertThat(state.requireOffset()).isEqualTo(300f)
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index 53a5d478..a1110337 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import androidx.compose.animation.core.snap
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/PlacedUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/PlacedUtils.kt
new file mode 100644
index 0000000..2f5acc3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/PlacedUtils.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.list
+
+import androidx.compose.ui.test.SemanticsNodeInteraction
+
+/**
+ * Asserts that the current semantics node is placed.
+ *
+ * Throws [AssertionError] if the node is not placed.
+ */
+internal fun SemanticsNodeInteraction.assertIsPlaced(): SemanticsNodeInteraction {
+ val errorMessageOnFail = "Assert failed: The component is not placed!"
+ if (!fetchSemanticsNode(errorMessageOnFail).layoutInfo.isPlaced) {
+ throw AssertionError(errorMessageOnFail)
+ }
+ return this
+}
+
+/**
+ * Asserts that the current semantics node is not placed.
+ *
+ * Throws [AssertionError] if the node is placed.
+ */
+internal fun SemanticsNodeInteraction.assertIsNotPlaced() {
+ // TODO(b/187188981): We don't have a non-throwing API to check whether an item exists.
+ // So until this bug is fixed, we are going to catch the assertion error and then check
+ // whether the node is placed or not.
+ try {
+ // If the node does not exist, it implies that it is also not placed.
+ assertDoesNotExist()
+ } catch (e: AssertionError) {
+ // If the node exists, we need to assert that it is not placed.
+ val errorMessageOnFail = "Assert failed: The component is placed!"
+ if (fetchSemanticsNode().layoutInfo.isPlaced) {
+ throw AssertionError(errorMessageOnFail)
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
index 94c9612..6c18f6e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -140,15 +140,16 @@
}
composeView = LocalView.current
focusManager = LocalFocusManager.current
- val resolvedFlingBehavior = flingBehavior ?: PagerDefaults.flingBehavior(
- state = state,
- pagerSnapDistance = snappingPage,
- snapPositionalThreshold = snapPositionalThreshold
- )
CompositionLocalProvider(
LocalLayoutDirection provides config.layoutDirection,
LocalOverscrollConfiguration provides null
) {
+ val resolvedFlingBehavior = flingBehavior ?: PagerDefaults.flingBehavior(
+ state = state,
+ pagerSnapDistance = snappingPage,
+ snapPositionalThreshold = snapPositionalThreshold
+ )
+
scope = rememberCoroutineScope()
Box(
modifier = Modifier
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
index b425d75..ed5fa25 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.pager
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -24,6 +25,8 @@
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -40,7 +43,7 @@
}
@Test
- fun swipeWithLowVelocity_positionalThresholdLessThanDefaultThreshold_shouldBounceBack() =
+ fun swipeWithLowVelocity_positionalThresholdLessThanDefaultThreshold_shouldBounceBack_ltr() =
with(rule) {
// Arrange
setContent {
@@ -90,6 +93,57 @@
}
@Test
+ fun swipeWithLowVelocity_positionalThresholdLessThanDefaultThreshold_shouldBounceBack_rtl() =
+ with(rule) {
+ // Arrange
+ setContent {
+ ParameterizedPager(
+ initialPage = 5,
+ modifier = Modifier.fillMaxSize(),
+ orientation = it.orientation,
+ pageSpacing = it.pageSpacing,
+ layoutDirection = it.layoutDirection
+ )
+ }
+ val ParamsWithRtl = ParamsToTest.map { it.copy(layoutDirection = LayoutDirection.Rtl) }
+ forEachParameter(ParamsWithRtl) { param ->
+ val swipeValue = 0.4f
+ val delta = pagerSize * swipeValue
+
+ // Act - forward
+ onPager().performTouchInput {
+ val (start, end) = if (param.orientation == Orientation.Vertical) {
+ topCenter to topCenter.copy(y = topCenter.y + delta)
+ } else {
+ centerRight to centerRight.copy(x = centerRight.x - delta)
+ }
+ swipeWithVelocity(start, end, 0.5f * MinFlingVelocityDp.toPx())
+ }
+ waitForIdle()
+
+ // Assert
+ onNodeWithTag("5").assertIsDisplayed()
+ param.confirmPageIsInCorrectPosition(5)
+
+ // Act - backward
+ onPager().performTouchInput {
+ val (start, end) = if (param.orientation == Orientation.Vertical) {
+ topCenter to topCenter.copy(y = topCenter.y - delta)
+ } else {
+ centerRight to centerRight.copy(x = centerRight.x + delta)
+ }
+ swipeWithVelocity(start, end, 0.5f * MinFlingVelocityDp.toPx())
+ }
+ waitForIdle()
+
+ // Assert
+ onNodeWithTag("5").assertIsDisplayed()
+ param.confirmPageIsInCorrectPosition(5)
+ resetTestCase(5)
+ }
+ }
+
+ @Test
fun swipeWithLowVelocity_positionalThresholdLessThanLowThreshold_shouldBounceBack() =
with(rule) {
// Arrange
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/SingleParamBasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/SingleParamBasePagerTest.kt
index 9c86107..2e565132 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/SingleParamBasePagerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/SingleParamBasePagerTest.kt
@@ -114,15 +114,17 @@
}
composeView = LocalView.current
focusManager = LocalFocusManager.current
- val resolvedFlingBehavior = flingBehavior ?: PagerDefaults.flingBehavior(
- state = state,
- pagerSnapDistance = snappingPage,
- snapPositionalThreshold = snapPositionalThreshold
- )
+
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection,
LocalOverscrollConfiguration provides null
) {
+ val resolvedFlingBehavior = flingBehavior ?: PagerDefaults.flingBehavior(
+ state = state,
+ pagerSnapDistance = snappingPage,
+ snapPositionalThreshold = snapPositionalThreshold
+ )
+
scope = rememberCoroutineScope()
Box(
modifier = Modifier
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextMinTouchBoundsSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextMinTouchBoundsSelectionGesturesTest.kt
index 4fecf01..73d0276 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextMinTouchBoundsSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextMinTouchBoundsSelectionGesturesTest.kt
@@ -36,6 +36,7 @@
import androidx.compose.foundation.text.selection.gestures.MultiTextMinTouchBoundsSelectionGesturesTest.TestVertical.OVERLAP_BELONGS_TO_FIRST
import androidx.compose.foundation.text.selection.gestures.MultiTextMinTouchBoundsSelectionGesturesTest.TestVertical.OVERLAP_BELONGS_TO_SECOND
import androidx.compose.foundation.text.selection.gestures.MultiTextMinTouchBoundsSelectionGesturesTest.TestVertical.OVERLAP_EQUIDISTANT
+import androidx.compose.foundation.text.selection.gestures.util.longPress
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
@@ -87,22 +88,30 @@
*/
private val touchTargetDpLen = dpLen + 12.dp * 2
- enum class TestHorizontal(val x: Float) {
- LEFT(-6f),
- CENTER(10f),
- RIGHT(26f)
+ enum class TestHorizontal(
+ val x: Float,
+ /** The x-value we would coerce to in order to get the x coordinate onto a box. */
+ val coercedX: Float,
+ ) {
+ LEFT(x = -6f, coercedX = 1f),
+ CENTER(x = 10f, coercedX = 10f),
+ RIGHT(x = 26f, coercedX = 19f)
}
- enum class TestVertical(val y: Float) {
- ABOVE(-6f),
- ON_FIRST(10f),
- NO_OVERLAP_BELONGS_TO_FIRST(25f),
- OVERLAP_BELONGS_TO_FIRST(29f),
- OVERLAP_EQUIDISTANT(30f),
- OVERLAP_BELONGS_TO_SECOND(31f),
- NO_OVERLAP_BELONGS_TO_SECOND(35f),
- ON_SECOND(50f),
- BELOW(66f),
+ enum class TestVertical(
+ val y: Float,
+ /** The y-value we would coerce to in order to get the y coordinate onto a box. */
+ val coercedY: Float,
+ ) {
+ ABOVE(y = -6f, coercedY = 1f),
+ ON_FIRST(y = 10f, coercedY = 10f),
+ NO_OVERLAP_BELONGS_TO_FIRST(y = 25f, coercedY = 19f),
+ OVERLAP_BELONGS_TO_FIRST(y = 29f, coercedY = 19f),
+ OVERLAP_EQUIDISTANT(y = 30f, coercedY = 19f),
+ OVERLAP_BELONGS_TO_SECOND(y = 31f, coercedY = 41f),
+ NO_OVERLAP_BELONGS_TO_SECOND(y = 35f, coercedY = 41f),
+ ON_SECOND(y = 50f, coercedY = 50f),
+ BELOW(y = 66f, coercedY = 59f);
}
enum class ExpectedText(val selectableId: Long?) {
@@ -170,8 +179,24 @@
}
@Test
- fun minTouchTargetSelectionGestureTest() {
+ fun minTouchTargetSelectionGestureTest() = runTest {
performTouchGesture { longClick(Offset(horizontal.x, vertical.y)) }
+ }
+
+ // Regression test for b/325307463
+ @Test
+ fun dragIntoMinTouchTargetSelectionGestureTest() = runTest {
+ performTouchGesture {
+ longPress(Offset(horizontal.coercedX, vertical.coercedY))
+ // The crash involved a quick drag from on the text to off the text
+ // causing a race of some state not being set before the drag is executed,
+ // so we want to force the moveTo immediately after the long press finishes.
+ moveTo(Offset(horizontal.x, vertical.y), delayMillis = 0L)
+ }
+ }
+
+ fun runTest(block: () -> Unit) {
+ block()
val expectedSelectableId = expectedText.selectableId
if (expectedSelectableId == null) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 9766e18..5ca3dbde 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -33,7 +33,6 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -47,14 +46,15 @@
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.getScrollViewportLength
import androidx.compose.ui.semantics.horizontalScrollAxisRange
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.scrollBy
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastRoundToInt
@@ -274,40 +274,16 @@
isVertical: Boolean
) = composed(
factory = {
- val coroutineScope = rememberCoroutineScope()
- semantics {
- isTraversalGroup = true
- val accessibilityScrollState = ScrollAxisRange(
- value = { state.value.toFloat() },
- maxValue = { state.maxValue.toFloat() },
- reverseScrolling = reverseScrolling
- )
- if (isVertical) {
- this.verticalScrollAxisRange = accessibilityScrollState
- } else {
- this.horizontalScrollAxisRange = accessibilityScrollState
- }
- if (isScrollable) {
- // when b/156389287 is fixed, this should be proper scrollTo with reverse handling
- scrollBy(
- action = { x: Float, y: Float ->
- coroutineScope.launch {
- if (isVertical) {
- (state as ScrollableState).animateScrollBy(y)
- } else {
- (state as ScrollableState).animateScrollBy(x)
- }
- }
- return@scrollBy true
- }
+ Modifier
+ .then(
+ ScrollSemanticsElement(
+ state = state,
+ reverseScrolling = reverseScrolling,
+ flingBehavior = flingBehavior,
+ isScrollable = isScrollable,
+ isVertical = isVertical,
)
-
- getScrollViewportLength {
- it.add(state.viewportSize.toFloat())
- true
- }
- }
- }
+ )
.scrollingContainer(
state = state,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
@@ -328,6 +304,76 @@
}
)
+private data class ScrollSemanticsElement(
+ val state: ScrollState,
+ val reverseScrolling: Boolean,
+ val flingBehavior: FlingBehavior?,
+ val isScrollable: Boolean,
+ val isVertical: Boolean
+) : ModifierNodeElement<ScrollSemanticsModifierNode>() {
+ override fun create(): ScrollSemanticsModifierNode = ScrollSemanticsModifierNode(
+ state = state,
+ reverseScrolling = reverseScrolling,
+ flingBehavior = flingBehavior,
+ isScrollable = isScrollable,
+ isVertical = isVertical,
+ )
+
+ override fun update(node: ScrollSemanticsModifierNode) {
+ node.state = state
+ node.reverseScrolling = reverseScrolling
+ node.flingBehavior = flingBehavior
+ node.isScrollable = isScrollable
+ node.isVertical = isVertical
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ // Not a public modifier.
+ }
+}
+
+private class ScrollSemanticsModifierNode(
+ var state: ScrollState,
+ var reverseScrolling: Boolean,
+ var flingBehavior: FlingBehavior?,
+ var isScrollable: Boolean,
+ var isVertical: Boolean
+) : Modifier.Node(), SemanticsModifierNode {
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ isTraversalGroup = true
+ val accessibilityScrollState = ScrollAxisRange(
+ value = { state.value.toFloat() },
+ maxValue = { state.maxValue.toFloat() },
+ reverseScrolling = reverseScrolling
+ )
+ if (isVertical) {
+ this.verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ this.horizontalScrollAxisRange = accessibilityScrollState
+ }
+ if (isScrollable) {
+ // when b/156389287 is fixed, this should be proper scrollTo with reverse handling
+ scrollBy(
+ action = { x: Float, y: Float ->
+ coroutineScope.launch {
+ if (isVertical) {
+ (state as ScrollableState).animateScrollBy(y)
+ } else {
+ (state as ScrollableState).animateScrollBy(x)
+ }
+ }
+ return@scrollBy true
+ }
+ )
+
+ getScrollViewportLength {
+ it.add(state.viewportSize.toFloat())
+ true
+ }
+ }
+ }
+}
+
internal class ScrollingLayoutElement(
val scrollState: ScrollState,
val isReversed: Boolean,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index b6abe95..0d055ac 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -49,6 +49,8 @@
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Velocity
import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
import kotlin.math.sign
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -303,26 +305,24 @@
private var state: AnchoredDraggableState<T>,
private var orientation: Orientation,
enabled: Boolean,
- reverseDirection: Boolean,
+ private var reverseDirection: Boolean,
interactionSource: MutableInteractionSource?,
private var overscrollEffect: OverscrollEffect?,
- startDragImmediately: () -> Boolean
-) : AbstractDraggableNode(
+ private var startDragImmediately: () -> Boolean
+) : DragGestureNode(
canDrag = AlwaysDrag,
enabled = enabled,
- interactionSource = interactionSource,
- startDragImmediately = startDragImmediately,
- reverseDirection = reverseDirection
+ interactionSource = interactionSource
) {
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
state.anchoredDrag(MutatePriority.Default) {
forEachDelta { dragDelta ->
if (overscrollEffect == null) {
- dragTo(state.newOffsetForDelta(dragDelta.delta.toFloat()))
+ dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().toFloat()))
} else {
overscrollEffect!!.applyToScroll(
- delta = dragDelta.delta,
+ delta = dragDelta.delta.reverseIfNeeded(),
source = NestedScrollSource.Drag
) { deltaForDrag ->
val dragOffset = state.newOffsetForDelta(deltaForDrag.toFloat())
@@ -342,10 +342,10 @@
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
if (overscrollEffect == null) {
- state.settle(velocity.toFloat()).toVelocity()
+ state.settle(velocity.reverseIfNeeded().toFloat()).toVelocity()
} else {
overscrollEffect!!.applyToFling(
- velocity = velocity
+ velocity = velocity.reverseIfNeeded()
) { availableVelocity ->
val consumed = state.settle(availableVelocity.toFloat()).toVelocity()
val currentOffset = state.requireOffset()
@@ -361,6 +361,8 @@
}
}
+ override fun startDragImmediately(): Boolean = startDragImmediately.invoke()
+
fun update(
state: AnchoredDraggableState<T>,
orientation: Orientation,
@@ -381,13 +383,17 @@
resetPointerInputHandling = true
}
+ if (this.reverseDirection != reverseDirection) {
+ this.reverseDirection = reverseDirection
+ resetPointerInputHandling = true
+ }
+
this.overscrollEffect = overscrollEffect
+ this.startDragImmediately = startDragImmediately
update(
enabled = enabled,
interactionSource = interactionSource,
- startDragImmediately = startDragImmediately,
- reverseDirection = reverseDirection,
isResetPointerInputHandling = resetPointerInputHandling,
)
}
@@ -407,6 +413,9 @@
private fun Offset.toFloat() =
if (orientation == Orientation.Vertical) this.y else this.x
+
+ private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
+ private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
}
private val AlwaysDrag: (PointerInputChange) -> Boolean = { true }
@@ -488,11 +497,22 @@
/**
* The current value of the [AnchoredDraggableState].
+ *
+ * That is the closest anchor point that the state has passed through.
*/
var currentValue: T by mutableStateOf(initialValue)
private set
/**
+ * The value the [AnchoredDraggableState] is currently settled at.
+ *
+ * When progressing through multiple anchors, e.g. `A -> B -> C`, [settledValue] will stay the
+ * same until settled at an anchor, while [currentValue] will update to the closest anchor.
+ */
+ var settledValue: T by mutableStateOf(initialValue)
+ private set
+
+ /**
* The target value. This is the closest value to the current offset, taking into account
* positional thresholds. If no interactions like animations or drags are in progress, this
* will be the current value.
@@ -507,20 +527,6 @@
}
/**
- * The closest value in the swipe direction from the current offset, not considering thresholds.
- * If an [anchoredDrag] is in progress, this will be the target of that anchoredDrag (if
- * specified).
- */
- internal val closestValue: T by derivedStateOf {
- dragTarget ?: run {
- val currentOffset = offset
- if (!currentOffset.isNaN()) {
- computeTargetWithoutThresholds(currentOffset, currentValue)
- } else currentValue
- }
- }
-
- /**
* The current offset, or [Float.NaN] if it has not been initialized yet.
*
* The offset will be initialized when the anchors are first set through [updateAnchors].
@@ -552,13 +558,39 @@
val isAnimationRunning: Boolean get() = dragTarget != null
/**
- * The fraction of the progress going from [currentValue] to [closestValue], within [0f..1f]
+ * The fraction of the offset between [from] and [to], as a fraction between [0f..1f], or 1f if
+ * [from] is equal to [to].
+ *
+ * @param from The starting value used to calculate the distance
+ * @param to The end value used to calculate the distance
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ fun progress(from: T, to: T): Float {
+ val fromOffset = anchors.positionOf(from)
+ val toOffset = anchors.positionOf(to)
+ val currentOffset = offset.coerceIn(
+ min(fromOffset, toOffset), // fromOffset might be > toOffset
+ max(fromOffset, toOffset)
+ )
+ val fraction = (currentOffset - fromOffset) / (toOffset - fromOffset)
+ return if (!fraction.isNaN()) {
+ // If we are very close to 0f or 1f, we round to the closest
+ if (fraction < 1e-6f) 0f else if (fraction > 1 - 1e-6f) 1f else abs(fraction)
+ } else 1f
+ }
+
+ /**
+ * The fraction of the progress going from [settledValue] to [targetValue], within [0f..1f]
* bounds, or 1f if the [AnchoredDraggableState] is in a settled state.
*/
+ @Deprecated(
+ message = "Use the progress function to query the progress between two specified " +
+ "anchors.",
+ replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
@get:FloatRange(from = 0.0, to = 1.0)
val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
- val a = anchors.positionOf(currentValue)
- val b = anchors.positionOf(closestValue)
+ val a = anchors.positionOf(settledValue)
+ val b = anchors.positionOf(targetValue)
val distance = abs(b - a)
if (!distance.isNaN() && distance > 1e-6f) {
val progress = (this.requireOffset() - a) / (b - a)
@@ -674,26 +706,55 @@
}
}
- private fun computeTargetWithoutThresholds(
- offset: Float,
- currentValue: T,
- ): T {
- val currentAnchors = anchors
- val currentAnchor = currentAnchors.positionOf(currentValue)
- return if (currentAnchor == offset || currentAnchor.isNaN()) {
- currentValue
- } else {
- currentAnchors.closestAnchor(
- offset,
- offset - currentAnchor > 0
- ) ?: currentValue
- }
- }
+ private var nextValue: T by mutableStateOf(initialValue)
private val anchoredDragScope: AnchoredDragScope = object : AnchoredDragScope {
+ var initialized = false
+ var absoluteThresholdToCross: Float = Float.NaN
+ var min = Float.NaN
+ var max = Float.NaN
+
override fun dragTo(newOffset: Float, lastKnownVelocity: Float) {
+ val previousOffset = offset
offset = newOffset
lastVelocity = lastKnownVelocity
+ val isMovingForward = newOffset >= previousOffset
+ if (initialized) {
+ val crossedThresholdTowardsNextAnchor = if (isMovingForward) {
+ newOffset >= absoluteThresholdToCross
+ } else {
+ newOffset <= absoluteThresholdToCross
+ }
+ if (crossedThresholdTowardsNextAnchor) {
+ update(isMovingForward)
+ }
+ } else if (!previousOffset.isNaN()) {
+ // In the first invocation, we do not have a direction. The previous offset will be
+ // NaN in the first invocation of dragTo; so we will only initialize in the second
+ // invocation when we have a direction to calculate the thresholds with
+ update(isMovingForward)
+ initialized = true
+ }
+ }
+
+ fun update(isMovingForward: Boolean) {
+ val currentAnchorPosition = anchors.positionOf(currentValue)
+ min = anchors.minAnchor()
+ max = anchors.maxAnchor()
+ val lookUpwards = when (currentAnchorPosition) {
+ min -> true
+ max -> false
+ else -> isMovingForward
+ }
+ val closestAnchor = anchors.closestAnchor(offset, lookUpwards)
+ val nextAnchor = closestAnchor ?: currentValue
+ val nextAnchorPosition = anchors.positionOf(nextAnchor!!)
+ if (confirmValueChange(nextAnchor)) {
+ currentValue = nextAnchor
+ }
+ val relativeThreshold = (nextAnchorPosition - currentAnchorPosition) / 2f
+ absoluteThresholdToCross = currentAnchorPosition + relativeThreshold
+ nextValue = nextAnchor
}
}
@@ -719,18 +780,15 @@
dragPriority: MutatePriority = MutatePriority.Default,
block: suspend AnchoredDragScope.(anchors: DraggableAnchors<T>) -> Unit
) {
- try {
- dragMutex.mutate(dragPriority) {
- restartable(inputs = { anchors }) { latestAnchors ->
- anchoredDragScope.block(latestAnchors)
- }
+ dragMutex.mutate(dragPriority) {
+ restartable(inputs = { anchors }) { latestAnchors ->
+ anchoredDragScope.block(latestAnchors)
}
- } finally {
val closest = anchors.closestAnchor(offset)
- if (closest != null &&
- abs(offset - anchors.positionOf(closest)) <= 0.5f &&
- confirmValueChange.invoke(closest)
- ) {
+ if (closest != null && confirmValueChange.invoke(closest)) {
+ val closestAnchorOffset = anchors.positionOf(closest)
+ anchoredDragScope.dragTo(closestAnchorOffset, lastVelocity)
+ settledValue = closest
currentValue = closest
}
}
@@ -762,7 +820,7 @@
suspend fun anchoredDrag(
targetValue: T,
dragPriority: MutatePriority = MutatePriority.Default,
- block: suspend AnchoredDragScope.(anchors: DraggableAnchors<T>, targetValue: T) -> Unit
+ block: suspend AnchoredDragScope.(anchor: DraggableAnchors<T>, targetValue: T) -> Unit
) {
if (anchors.hasAnchorFor(targetValue)) {
try {
@@ -770,23 +828,24 @@
dragTarget = targetValue
restartable(
inputs = { anchors to this@AnchoredDraggableState.targetValue }
- ) { (latestAnchors, latestTarget) ->
- anchoredDragScope.block(latestAnchors, latestTarget)
+ ) { (anchors, latestTarget) ->
+ anchoredDragScope.block(anchors, latestTarget)
+ }
+ if (confirmValueChange(targetValue)) {
+ val latestTargetOffset = anchors.positionOf(targetValue)
+ anchoredDragScope.dragTo(latestTargetOffset, lastVelocity)
+ settledValue = targetValue
+ currentValue = targetValue
}
}
} finally {
dragTarget = null
- val closest = anchors.closestAnchor(offset)
- if (closest != null &&
- abs(offset - anchors.positionOf(closest)) <= 0.5f &&
- confirmValueChange.invoke(closest)
- ) {
- currentValue = closest
- }
}
} else {
- // Todo: b/283467401, revisit this behavior
- currentValue = targetValue
+ if (confirmValueChange(targetValue)) {
+ settledValue = targetValue
+ currentValue = targetValue
+ }
}
}
@@ -821,6 +880,7 @@
dragTarget = null
}
currentValue = targetValue
+ settledValue = targetValue
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 59d6088..1b92b0e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -298,22 +298,20 @@
private var orientation: Orientation,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
- startDragImmediately: () -> Boolean,
+ private var startDragImmediately: () -> Boolean,
private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
- reverseDirection: Boolean
-) : AbstractDraggableNode(
+ private var reverseDirection: Boolean
+) : DragGestureNode(
canDrag,
enabled,
- interactionSource,
- startDragImmediately,
- reverseDirection
+ interactionSource
) {
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
state.drag(MutatePriority.UserInput) {
forEachDelta { dragDelta ->
- dragBy(dragDelta.delta.toFloat(orientation))
+ dragBy(dragDelta.delta.reverseIfNeeded().toFloat(orientation))
}
}
}
@@ -324,7 +322,9 @@
this@DraggableNode.onDragStarted(this, startedPosition)
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) =
- this@DraggableNode.onDragStopped(this, velocity.toFloat(orientation))
+ this@DraggableNode.onDragStopped(this, velocity.reverseIfNeeded().toFloat(orientation))
+
+ override fun startDragImmediately(): Boolean = startDragImmediately.invoke()
fun update(
state: DraggableState,
@@ -346,33 +346,40 @@
this.orientation = orientation
resetPointerInputHandling = true
}
+ if (this.reverseDirection != reverseDirection) {
+ this.reverseDirection = reverseDirection
+ resetPointerInputHandling = true
+ }
+
this.onDragStarted = onDragStarted
this.onDragStopped = onDragStopped
+ this.startDragImmediately = startDragImmediately
update(
canDrag,
enabled,
interactionSource,
- startDragImmediately,
- reverseDirection,
resetPointerInputHandling
)
}
+
+ private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
+ private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
}
-internal abstract class AbstractDraggableNode(
+/**
+ * A node that performs drag gesture recognition and event propagation.
+ */
+internal abstract class DragGestureNode(
private var canDrag: (PointerInputChange) -> Boolean,
private var enabled: Boolean,
private var interactionSource: MutableInteractionSource?,
- private var startDragImmediately: () -> Boolean,
- private var reverseDirection: Boolean
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
// Use wrapper lambdas here to make sure that if these properties are updated while we suspend,
// we point to the new reference when we invoke them. startDragImmediately is a lambda since we
// need the most recent value passed to it from Scrollable.
private val _canDrag: (PointerInputChange) -> Boolean = { canDrag(it) }
- private val _startDragImmediately: () -> Boolean = { startDragImmediately() }
private val velocityTracker = VelocityTracker()
private var isListeningForEvents = false
@@ -402,6 +409,12 @@
*/
abstract suspend fun CoroutineScope.onDragStopped(velocity: Velocity)
+ /**
+ * If touch slop recognition should be skipped. If this is true, this node will start
+ * recognizing drag events immediately without waiting for touch slop.
+ */
+ abstract fun startDragImmediately(): Boolean
+
private fun startListeningForEvents() {
isListeningForEvents = true
@@ -445,7 +458,7 @@
while (isActive) {
awaitDownAndSlop(
_canDrag,
- _startDragImmediately,
+ ::startDragImmediately,
velocityTracker,
pointerDirectionConfig
)?.let {
@@ -462,8 +475,7 @@
it.first,
it.second,
velocityTracker,
- channel,
- reverseDirection
+ channel
) { event ->
pointerDirectionConfig.calculateDeltaChange(
event.positionChangeIgnoreConsumed()
@@ -480,7 +492,7 @@
Velocity(maximumVelocity, maximumVelocity)
)
velocityTracker.resetTracking()
- DragStopped(velocity * if (reverseDirection) -1f else 1f)
+ DragStopped(velocity)
} else {
DragCancelled
}
@@ -554,8 +566,6 @@
canDrag: (PointerInputChange) -> Boolean = this.canDrag,
enabled: Boolean = this.enabled,
interactionSource: MutableInteractionSource? = this.interactionSource,
- startDragImmediately: () -> Boolean = this.startDragImmediately,
- reverseDirection: Boolean = this.reverseDirection,
isResetPointerInputHandling: Boolean = false
) {
var resetPointerInputHandling = isResetPointerInputHandling
@@ -571,11 +581,7 @@
disposeInteractionSource()
this.interactionSource = interactionSource
}
- this.startDragImmediately = startDragImmediately
- if (this.reverseDirection != reverseDirection) {
- this.reverseDirection = reverseDirection
- resetPointerInputHandling = true
- }
+
if (resetPointerInputHandling) {
pointerInputNode.resetPointerInputHandler()
}
@@ -623,7 +629,6 @@
initialDelta: Offset,
velocityTracker: VelocityTracker,
channel: SendChannel<DragEvent>,
- reverseDirection: Boolean,
hasDragged: (PointerInputChange) -> Boolean,
): Boolean {
@@ -634,7 +639,7 @@
Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
channel.trySend(DragStarted(adjustedStart))
- channel.trySend(DragDelta(if (reverseDirection) initialDelta * -1f else initialDelta))
+ channel.trySend(DragDelta(initialDelta))
return onDragOrUp(hasDragged, startEvent.id) { event ->
// Velocity tracker takes all events, even UP
@@ -644,7 +649,7 @@
if (!event.changedToUpIgnoreConsumed()) {
val delta = event.positionChange()
event.consume()
- channel.trySend(DragDelta(if (reverseDirection) delta * -1f else delta))
+ channel.trySend(DragDelta(delta))
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index e89f4ab..c8e26da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -192,7 +192,7 @@
CanDrag,
enabled,
interactionSource,
- if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
+ startDragImmediately,
onDragStarted,
onDragStopped,
reverseDirection
@@ -204,7 +204,7 @@
CanDrag,
enabled,
interactionSource,
- if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
+ startDragImmediately,
onDragStarted,
onDragStopped,
reverseDirection
@@ -252,8 +252,6 @@
}
companion object {
- val StartDragImmediately = { true }
- val DoNotStartDragImmediately = { false }
val CanDrag: (PointerInputChange) -> Boolean = { true }
}
}
@@ -264,16 +262,14 @@
canDrag: (PointerInputChange) -> Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
- startDragImmediately: () -> Boolean,
+ private var startDragImmediately: Boolean,
private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
- reverseDirection: Boolean
-) : AbstractDraggableNode(
+ private var reverseDirection: Boolean
+) : DragGestureNode(
canDrag,
enabled,
- interactionSource,
- startDragImmediately,
- reverseDirection
+ interactionSource
) {
override suspend fun drag(
@@ -281,7 +277,7 @@
) {
state.drag(MutatePriority.UserInput) {
forEachDelta { dragDelta ->
- dragBy(dragDelta.delta)
+ dragBy(dragDelta.delta.reverseIfNeeded())
}
}
}
@@ -292,14 +288,16 @@
this@Draggable2DNode.onDragStarted(this, startedPosition)
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) =
- this@Draggable2DNode.onDragStopped(this, velocity)
+ this@Draggable2DNode.onDragStopped(this, velocity.reverseIfNeeded())
+
+ override fun startDragImmediately(): Boolean = startDragImmediately
fun update(
state: Draggable2DState,
canDrag: (PointerInputChange) -> Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
- startDragImmediately: () -> Boolean,
+ startDragImmediately: Boolean,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
reverseDirection: Boolean
@@ -309,18 +307,25 @@
this.state = state
resetPointerInputHandling = true
}
+ if (this.reverseDirection != reverseDirection) {
+ this.reverseDirection = reverseDirection
+ resetPointerInputHandling = true
+ }
+
this.onDragStarted = onDragStarted
this.onDragStopped = onDragStopped
+ this.startDragImmediately = startDragImmediately
update(
canDrag,
enabled,
interactionSource,
- startDragImmediately,
- reverseDirection,
resetPointerInputHandling
)
}
+
+ private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
+ private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
}
@OptIn(ExperimentalFoundationApi::class)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
index b77ca7d..4fa1ead 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.mainAxisViewportSize
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
import kotlin.math.absoluteValue
@@ -186,8 +187,12 @@
}
}
-@OptIn(ExperimentalFoundationApi::class)
-private fun PagerState.isScrollingForward() = dragGestureDelta() < 0
+private fun PagerState.isLtrDragging() = dragGestureDelta() > 0
+private fun PagerState.isScrollingForward(): Boolean {
+ val reverseScrollDirection = layoutInfo.reverseLayout
+ return (isLtrDragging() && reverseScrollDirection ||
+ !isLtrDragging() && !reverseScrollDirection)
+}
@OptIn(ExperimentalFoundationApi::class)
private fun PagerState.dragGestureDelta() = if (layoutInfo.orientation == Orientation.Horizontal) {
@@ -209,16 +214,27 @@
@OptIn(ExperimentalFoundationApi::class)
internal fun calculateFinalSnappingBound(
pagerState: PagerState,
+ layoutDirection: LayoutDirection,
snapPositionalThreshold: Float,
flingVelocity: Float,
lowerBoundOffset: Float,
upperBoundOffset: Float
): Float {
- val isForward = pagerState.isScrollingForward()
-
- debugLog { "isForward=$isForward" }
-
+ val isForward = if (pagerState.layoutInfo.orientation == Orientation.Vertical) {
+ pagerState.isScrollingForward()
+ } else {
+ if (layoutDirection == LayoutDirection.Ltr) {
+ pagerState.isScrollingForward()
+ } else {
+ !pagerState.isScrollingForward()
+ }
+ }
+ debugLog {
+ "isLtrDragging=${pagerState.isLtrDragging()} " +
+ "isForward=$isForward " +
+ "layoutDirection=$layoutDirection"
+ }
// how many pages have I scrolled using a drag gesture.
val offsetFromSnappedPosition =
pagerState.dragGestureDelta() / pagerState.layoutInfo.pageSize.toFloat()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 5cc47d2..26dce70 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -40,6 +40,7 @@
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.pageDown
import androidx.compose.ui.semantics.pageLeft
import androidx.compose.ui.semantics.pageRight
@@ -300,12 +301,14 @@
"You've specified $snapPositionalThreshold"
}
val density = LocalDensity.current
+ val layoutDirection = LocalLayoutDirection.current
return remember(
state,
decayAnimationSpec,
snapAnimationSpec,
pagerSnapDistance,
- density
+ density,
+ layoutDirection
) {
val snapLayoutInfoProvider =
SnapLayoutInfoProvider(
@@ -315,6 +318,7 @@
) { flingVelocity, lowerBound, upperBound ->
calculateFinalSnappingBound(
pagerState = state,
+ layoutDirection = layoutDirection,
snapPositionalThreshold = snapPositionalThreshold,
flingVelocity = flingVelocity,
lowerBoundOffset = lowerBound,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 5808ef2..e18450f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -98,7 +98,19 @@
* change. The expectation is that this callback will end up causing `setSelection` to get
* called. This is what makes this a "controlled component".
*/
- var onSelectionChange: (Selection?) -> Unit = {}
+ var onSelectionChange: (Selection?) -> Unit = { selection = it }
+ set(newOnSelectionChange) {
+ // Wrap the given lambda with one that sets the selection immediately.
+ // The onSelectionChange loop requires a composition to happen for the selection
+ // to be updated, so we want to shorten that loop for gesture use cases where
+ // multiple selection changing events can be acted on within a single composition
+ // loop. Previous selection is used as part of that loop so keeping it up to date
+ // is important.
+ field = { newSelection ->
+ selection = newSelection
+ newOnSelectionChange(newSelection)
+ }
+ }
/**
* [HapticFeedback] handle to perform haptic feedback.
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/adding/AddingToProject.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/adding/AddingToProject.kt
deleted file mode 100644
index 4353eff..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/adding/AddingToProject.kt
+++ /dev/null
@@ -1,160 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "ClassName"
-)
-
-package androidx.compose.integration.docs.adding
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import org.junit.Rule
-import org.junit.Test
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/interop/adding
- *
- * No action required if it's modified.
- */
-
-private object AddingToProjectSnippet1 {
-
- class MyActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- // ...
-
- val greeting = findViewById<ComposeView>(R.id.greeting)
- greeting.setContent {
- MdcTheme { // or AppCompatTheme
- Greeting()
- }
- }
- }
- }
-
- @Composable
- private fun Greeting() {
- Text(
- text = stringResource(R.string.greeting),
- style = MaterialTheme.typography.h5,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = dimensionResource(R.dimen.margin_small))
- .wrapContentWidth(Alignment.CenterHorizontally)
- )
- }
-}
-
-private object AddingToProjectSnippet2 {
-
- class MyActivityTest {
- @Rule
- @JvmField
- val composeTestRule = createAndroidComposeRule<MyActivity>()
-
- @Test
- fun testGreeting() {
- val greeting = InstrumentationRegistry.getInstrumentation()
- .targetContext.resources.getString(R.string.greeting)
-
- composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
- }
- }
-}
-
-private object AddingToProjectSnippet3 {
-
- class MyActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MyScreen()
- }
- }
- }
-
- @Composable
- private fun MyScreen(
- viewModel: MyViewModel = viewModel()
- ) {
- val uiState by viewModel.uiState.collectAsState()
- when {
- uiState.isLoading -> { /* ... */ }
- uiState.isSuccess -> { /* ... */ }
- uiState.isError -> { /* ... */ }
- }
- }
-
- class MyViewModel : ViewModel() {
- private val _uiState = MutableStateFlow(MyScreenState.Loading)
- val uiState: StateFlow<MyScreenState> = _uiState
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private object R {
- object dimen {
- const val margin_small = 1
- }
- object id {
- const val greeting = 2
- }
- object string {
- const val greeting = 3
- }
-}
-
-@Composable
-private fun MdcTheme(content: @Composable () -> Unit) {
-}
-private class MyActivity : AppCompatActivity()
-private class MyScreenState {
- val isLoading = true
- val isSuccess = true
- val isError = true
- companion object {
- val Loading = MyScreenState()
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/input/HandlingInteraction.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/input/HandlingInteraction.kt
deleted file mode 100644
index b26842a..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/input/HandlingInteraction.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER")
-
-package androidx.compose.integration.docs.input
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.Interaction
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.interaction.collectIsPressedAsState
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.size
-import androidx.compose.integration.docs.input.DynamicButton.PressIconButton
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ShoppingCart
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/handling-interaction
- *
- * No action required if it's modified.
- */
-
-@Composable
-private fun UseInteractionSoure() {
- val interactionSource = remember { MutableInteractionSource() }
- val isPressed by interactionSource.collectIsPressedAsState()
-
- Button(
- onClick = { /* do something */ },
- interactionSource = interactionSource) {
- Text(if (isPressed) "Pressed!" else "Not pressed")
- }
-}
-
-@Composable
-private fun InteractionSourceBuildList() {
- val interactionSource = remember { MutableInteractionSource() }
- val interactions = remember { mutableStateListOf<Interaction>() }
-
- LaunchedEffect(interactionSource) {
- interactionSource.interactions.collect { interaction ->
- when (interaction) {
- is PressInteraction.Press -> {
- interactions.add(interaction)
- }
- is DragInteraction.Start -> {
- interactions.add(interaction)
- }
- }
- }
- }
-}
-
-@Composable
-private fun InteractionSourcePruneList() {
- val interactionSource = remember { MutableInteractionSource() }
-
- // snippet 1:
-
- val interactions = remember { mutableStateListOf<Interaction>() }
-
- LaunchedEffect(interactionSource) {
- interactionSource.interactions.collect { interaction ->
- when (interaction) {
- is PressInteraction.Press -> {
- interactions.add(interaction)
- }
- is PressInteraction.Release -> {
- interactions.remove(interaction.press)
- }
- is PressInteraction.Cancel -> {
- interactions.remove(interaction.press)
- }
- is DragInteraction.Start -> {
- interactions.add(interaction)
- }
- is DragInteraction.Stop -> {
- interactions.remove(interaction.start)
- }
- is DragInteraction.Cancel -> {
- interactions.add(interaction.start)
- }
- }
- }
- }
-
- // snippet 2:
- val isPressedOrDragged = interactions.isNotEmpty()
-
- // snippet 3:
- val lastInteraction = when (interactions.lastOrNull()) {
- is DragInteraction.Start -> "Dragged"
- is PressInteraction.Press -> "Pressed"
- else -> "No state"
- }
-}
-
-private object DynamicButton {
- @Composable
- fun PressIconButton(
- onClick: () -> Unit,
- icon: @Composable () -> Unit,
- text: @Composable () -> Unit,
- modifier: Modifier = Modifier,
- interactionSource: MutableInteractionSource =
- remember { MutableInteractionSource() },
- ) {
- val isPressed by interactionSource.collectIsPressedAsState()
- Button(onClick = onClick, modifier = modifier,
- interactionSource = interactionSource) {
- AnimatedVisibility(visible = isPressed) {
- if (isPressed) {
- Row {
- icon()
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- }
- }
- }
- text()
- }
- }
-}
-
-@Composable
-private fun UseDynamicButton() {
- PressIconButton(
- onClick = {},
- icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
- text = { Text("Add to cart") }
- )
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-// none yet
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropArchitecture.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropArchitecture.kt
deleted file mode 100644
index 3451066..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropArchitecture.kt
+++ /dev/null
@@ -1,200 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER",
- "RedundantSuspendModifier", "CascadeIf", "ClassName", "RemoveExplicitTypeArguments",
- "ControlFlowWithEmptyBody", "PropertyName", "CanBeParameter"
-)
-
-package androidx.compose.integration.docs.interoperability
-
-import android.content.Context
-import android.os.Bundle
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.material.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.ComposeView
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/interop/compose-in-existing-arch
- *
- * No action required if it's modified.
- */
-
-private object InteropArchitectureSnippet1 {
- class GreetingActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- MaterialTheme {
- Column {
- GreetingScreen("user1")
- GreetingScreen("user2")
- }
- }
- }
- }
- }
-
- @Composable
- fun GreetingScreen(
- userId: String,
- viewModel: GreetingViewModel = viewModel(
- factory = GreetingViewModelFactory(userId)
- )
- ) {
- val messageUser by viewModel.message.observeAsState("")
-
- Text(messageUser)
- }
-
- class GreetingViewModel(private val userId: String) : ViewModel() {
- private val _message = MutableLiveData("Hi $userId")
- val message: LiveData<String> = _message
- }
-
- class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun <T : ViewModel> create(modelClass: Class<T>): T {
- return GreetingViewModel(userId) as T
- }
- }
-}
-
-private object InteropArchitectureSnippet2 {
- @Composable
- fun MyApp() {
- NavHost(rememberNavController(), startDestination = "profile/{userId}") {
- /* ... */
- composable("profile/{userId}") { backStackEntry ->
- GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
- }
- }
- }
-
- @Composable
- fun GreetingScreen(
- userId: String,
- viewModel: GreetingViewModel = viewModel(
- factory = GreetingViewModelFactory(userId)
- )
- ) {
- val messageUser by viewModel.message.observeAsState("")
-
- Text(messageUser)
- }
-}
-
-private object InteropArchitectureSnippet3 {
- @Composable
- fun rememberAnalytics(user: User): FirebaseAnalytics {
- val analytics: FirebaseAnalytics = remember {
- // START - DO NOT COPY IN CODE SNIPPET
- FirebaseAnalytics()
- // END - DO NOT COPY IN CODE SNIPPET, just use /* ... */
- }
-
- // On every successful composition, update FirebaseAnalytics with
- // the userType from the current User, ensuring that future analytics
- // events have this metadata attached
- SideEffect {
- analytics.setUserProperty("userType", user.userType)
- }
- return analytics
- }
-}
-
-private object InteropArchitectureSnippet4 {
- class CustomViewGroup @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0
- ) : LinearLayout(context, attrs, defStyle) {
-
- // Source of truth in the View system as mutableStateOf
- // to make it thread-safe for Compose
- private var text by mutableStateOf("")
-
- private val textView: TextView
-
- init {
- orientation = VERTICAL
-
- textView = TextView(context)
- val composeView = ComposeView(context).apply {
- setContent {
- MaterialTheme {
- TextField(value = text, onValueChange = { updateState(it) })
- }
- }
- }
-
- addView(textView)
- addView(composeView)
- }
-
- // Update both the source of truth and the TextView
- private fun updateState(newValue: String) {
- text = newValue
- textView.text = newValue
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private class GreetingViewModel(userId: String) : ViewModel() {
- val _message = MutableLiveData("")
- val message: LiveData<String> = _message
-}
-private class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun <T : ViewModel> create(modelClass: Class<T>): T {
- return GreetingViewModel(userId) as T
- }
-}
-
-private class User(val userType: String = "user")
-private class FirebaseAnalytics {
- fun setUserProperty(name: String, value: String) {}
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt
deleted file mode 100644
index c4241f0..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt
+++ /dev/null
@@ -1,357 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER",
- "RedundantSuspendModifier", "CascadeIf", "ClassName", "RemoveExplicitTypeArguments",
- "ControlFlowWithEmptyBody", "PropertyName", "CanBeParameter", "PackageDirectoryMismatch"
-)
-
-package androidx.compose.integration.docs.interoperabilityui
-
-import android.app.Activity
-import android.content.Context
-import android.os.Bundle
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.FloatingActionButton
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.AbstractComposeView
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.unit.dp
-import androidx.recyclerview.widget.RecyclerView
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/interop/compose-in-existing-ui
- *
- * No action required if it's modified.
- */
-
-private object InteropUiSnippet1 {
- @Composable
- fun CallToActionButton(
- text: String,
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- ) {
- Button(
- colors = ButtonDefaults.buttonColors(
- backgroundColor = MaterialTheme.colors.secondary
- ),
- onClick = onClick,
- modifier = modifier,
- ) {
- Text(text)
- }
- }
-
- class CallToActionViewButton @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0
- ) : AbstractComposeView(context, attrs, defStyle) {
-
- var text by mutableStateOf<String>("")
- var onClick by mutableStateOf<() -> Unit>({})
-
- @Composable
- override fun Content() {
- YourAppTheme {
- CallToActionButton(text, onClick)
- }
- }
- }
-}
-
-private object InteropUiSnippet2 {
- class ExampleActivity : Activity() {
-
- private lateinit var binding: ActivityExampleBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityExampleBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- binding.callToAction.apply {
- text = getString(R.string.something)
- onClick = { /* Do something */ }
- }
- }
- }
-}
-
-private object InteropUiSnippet3 {
- // import com.google.android.material.composethemeadapter.MdcTheme
-
- class ExampleActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- // Use MdcTheme instead of MaterialTheme
- // Colors, typography, and shape have been read from the
- // View-based theme used in this Activity
- MdcTheme {
- ExampleComposable(/*...*/)
- }
- }
- }
- }
-}
-
-private object InteropUiSnippet4 {
- class ExampleActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- AppCompatTheme {
- // Colors, typography, and shape have been read from the
- // View-based theme used in this Activity
- ExampleComposable(/*...*/)
- }
- }
- }
- }
-}
-
-private object InteropUiSnippet5 {
- class ExampleActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
- setContent {
- MaterialTheme {
- MyScreen()
- }
- }
- }
- }
-
- @Composable
- fun MyScreen() {
- Box {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize() // fill the entire window
- .imePadding() // padding for the bottom for the IME
- .imeNestedScroll(), // scroll IME at the bottom
- content = { }
- )
- FloatingActionButton(
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp) // normal 16dp of padding for FABs
- .navigationBarsPadding() // Move it out from under the nav bar
- .imePadding(), // padding for when IME appears
- onClick = { }
- ) {
- Icon( /* ... */)
- }
- }
- }
-}
-
-@Composable
-fun InteropUiSnippet6(showCautionIcon: Boolean) {
- if (showCautionIcon) {
- CautionIcon(/* ... */)
- }
-}
-
-@Composable
-fun InteropUiSnippet7() {
- var isEnabled by rememberSaveable { mutableStateOf(false) }
-
- Column {
- ImageWithEnabledOverlay(isEnabled)
- ControlPanelWithToggle(
- isEnabled = isEnabled,
- onEnabledChanged = { isEnabled = it }
- )
- }
-}
-
-private object InteropUiSnippet8 {
- @Composable
- fun MyComposable() {
- BoxWithConstraints {
- if (minWidth < 480.dp) {
- /* Show grid with 4 columns */
- } else if (minWidth < 720.dp) {
- /* Show grid with 8 columns */
- } else {
- /* Show grid with 12 columns */
- }
- }
- }
-}
-
-private object InteropUiSnippet9 {
- // import androidx.compose.ui.platform.ComposeView
-
- class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): MyComposeViewHolder {
- return MyComposeViewHolder(ComposeView(parent.context))
- }
-
- override fun onViewRecycled(holder: MyComposeViewHolder) {
- // Dispose the underlying Composition of the ComposeView
- // when RecyclerView has recycled this ViewHolder
- holder.composeView.disposeComposition()
- }
-
- /* Other methods */
-
- // NOTE: DO NOT COPY THE METHODS BELOW IN THE CODE SNIPPETS
- override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
- TODO("Not yet implemented")
- }
-
- override fun getItemCount(): Int {
- TODO("Not yet implemented")
- }
- }
-
- class MyComposeViewHolder(
- val composeView: ComposeView
- ) : RecyclerView.ViewHolder(composeView) {
- /* ... */
- }
-}
-
-private object InteropUiSnippet10 {
- // import androidx.compose.ui.platform.ViewCompositionStrategy
-
- class MyComposeViewHolder(
- val composeView: ComposeView
- ) : RecyclerView.ViewHolder(composeView) {
-
- init {
- composeView.setViewCompositionStrategy(
- ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
- )
- }
-
- fun bind(input: String) {
- composeView.setContent {
- MdcTheme {
- Text(input)
- }
- }
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private object R {
- object string {
- const val something = 1
- }
-}
-
-private fun ExampleComposable() {}
-@Composable
-private fun MdcTheme(content: @Composable () -> Unit) {
-}
-
-@Composable
-private fun AppCompatTheme(content: @Composable () -> Unit) {
-}
-
-@Composable
-private fun BlueTheme(content: @Composable () -> Unit) {
-}
-
-@Composable
-private fun PinkTheme(content: @Composable () -> Unit) {
-}
-
-@Composable
-private fun YourAppTheme(content: @Composable () -> Unit) {
-}
-
-@Composable
-private fun Icon() {
-}
-
-@Composable
-private fun CautionIcon() {
-}
-
-@Composable
-private fun ImageWithEnabledOverlay(isEnabled: Boolean) {
-}
-
-@Composable
-private fun ControlPanelWithToggle(
- isEnabled: Boolean,
- onEnabledChanged: (Boolean) -> Unit
-) {
-}
-
-private class WindowCompat {
- companion object {
- fun setDecorFitsSystemWindows(window: Any, bool: Boolean) {}
- }
-}
-
-private fun Modifier.navigationBarsPadding(): Modifier = this
-
-private fun Modifier.fillMaxSize(): Modifier = this
-
-private fun Modifier.imePadding(): Modifier = this
-
-private fun Modifier.imeNestedScroll(): Modifier = this
-
-private class ActivityExampleBinding {
- val root: Int = 0
- lateinit var callToAction: InteropUiSnippet1.CallToActionViewButton
- companion object {
- fun inflate(li: LayoutInflater): ActivityExampleBinding { TODO() }
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt
deleted file mode 100644
index 369ff5f..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER",
- "RedundantSuspendModifier", "CascadeIf", "ClassName", "RemoveExplicitTypeArguments",
- "ControlFlowWithEmptyBody", "PropertyName", "CanBeParameter"
-)
-
-package androidx.compose.integration.docs.interoperability
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.integration.docs.databinding.ExampleLayoutBinding
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.compose.ui.viewinterop.AndroidViewBinding
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/interop/interop-apis
- *
- * No action required if it's modified.
- */
-
-private object InteropSnippet1 {
- class ExampleActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent { // In here, we can call composables!
- MaterialTheme {
- Greeting(name = "compose")
- }
- }
- }
- }
-
- @Composable
- fun Greeting(name: String) {
- Text(text = "Hello $name!")
- }
-}
-
-private object InteropSnippet2 {
- class ExampleFragment : Fragment() {
-
- private var _binding: FragmentExampleBinding? = null
- // This property is only valid between onCreateView and onDestroyView.
- private val binding get() = _binding!!
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- _binding = FragmentExampleBinding.inflate(inflater, container, false)
- val view = binding.root
- binding.composeView.apply {
- // Dispose of the Composition when the view's LifecycleOwner
- // is destroyed
- setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
- setContent {
- // In Compose world
- MaterialTheme {
- Text("Hello Compose!")
- }
- }
- }
- return view
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
-}
-
-private object InteropSnippet3 {
- class ExampleFragment : Fragment() {
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return ComposeView(requireContext()).apply {
- // Dispose of the Composition when the view's LifecycleOwner
- // is destroyed
- setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
- setContent {
- MaterialTheme {
- // In Compose world
- Text("Hello Compose!")
- }
- }
- }
- }
- }
-}
-
-/* ktlint-disable indent */
-private object InteropSnippet4 {
- // TW: Do not use this snippet.
- // This snippet simplifies too much code from Fragment and View so check out the following
- // snippet for changes:
-
- class ExampleFragment : Fragment() {
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return LinearLayout(context).apply {
- addView(ComposeView(context).apply {
- id = R.id.compose_view_x
- })
- }
- }
- }
-}
-/* ktlint-enable indent */
-
-private object InteropSnippet5 {
- @Composable
- fun CustomView() {
- val selectedItem = remember { mutableIntStateOf(0) }
-
- // Adds view to Compose
- AndroidView(
- modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
- factory = { context ->
- // Creates custom view
- CustomView(context).apply {
- // Sets up listeners for View -> Compose communication
- myView.setOnClickListener {
- selectedItem.intValue = 1
- }
- }
- },
- update = { view ->
- // View's been inflated or state read in this block has been updated
- // Add logic here if necessary
-
- // As selectedItem is read here, AndroidView will recompose
- // whenever the state changes
- // Example of Compose -> View communication
- view.coordinator.selectedItem = selectedItem.intValue
- }
- )
- }
-
- @Composable
- fun ContentExample() {
- Column(Modifier.fillMaxSize()) {
- Text("Look at this CustomView!")
- CustomView()
- }
- }
-}
-
-private object InteropSnippet6 {
- @Composable
- fun AndroidViewBindingExample() {
- AndroidViewBinding(ExampleLayoutBinding::inflate) {
- exampleView.setBackgroundColor(Color.GRAY)
- }
- }
-}
-
-private object InteropSnippet7 {
- @Composable
- fun ToastGreetingButton(greeting: String) {
- val context = LocalContext.current
- Button(onClick = {
- Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
- }) {
- Text("Greet")
- }
- }
-}
-
-/* ktlint-disable indent */
-private object InteropSnippet8 {
- class ExampleActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- // get data from savedInstanceState
- setContent {
- MaterialTheme {
- ExampleComposable(data, onButtonClick = {
- startActivity(/*...*/)
- })
- }
- }
- }
- }
-
- @Composable
- fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
- Button(onClick = onButtonClick) {
- Text(data.title)
- }
- }
-}
-
-private object InteropSnippet9 {
- @Composable
- fun SystemBroadcastReceiver(
- systemAction: String,
- onSystemEvent: (intent: Intent?) -> Unit
- ) {
- // Grab the current context in this part of the UI tree
- val context = LocalContext.current
-
- // Safely use the latest onSystemEvent lambda passed to the function
- val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
-
- // If either context or systemAction changes, unregister and register again
- DisposableEffect(context, systemAction) {
- val intentFilter = IntentFilter(systemAction)
- val broadcast = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- currentOnSystemEvent(intent)
- }
- }
-
- context.registerReceiver(broadcast, intentFilter)
-
- // When the effect leaves the Composition, remove the callback
- onDispose {
- context.unregisterReceiver(broadcast)
- }
- }
- }
-
- @Composable
- fun HomeScreen() {
-
- SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
- val isCharging = /* Get from batteryStatus ... */ true
- /* Do something if the device is charging */
- }
-
- /* Rest of the HomeScreen */
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private object R {
- object layout {
- const val fragment_example = 1
- }
-
- object id {
- const val compose_view = 2
- const val compose_view_x = 3
- }
-
- object string {
- const val ok = 4
- const val plane_description = 5
- }
-
- object dimen {
- const val padding_small = 6
- }
-
- object drawable {
- const val ic_plane = 7
- }
-
- object color {
- const val Blue700 = 8
- }
-}
-
-private class CustomView(context: Context) : View(context) {
- class Coord(var selectedItem: Int = 0)
-
- val coordinator = Coord()
- lateinit var myView: View
-}
-
-private class DataExample(val title: String = "")
-
-private val data = DataExample()
-private fun startActivity(): Nothing = TODO()
-private class ExampleViewModel : ViewModel() {
- val exampleLiveData = MutableLiveData(" ")
-}
-
-private fun ShowData(dataExample: State<String?>): Nothing = TODO()
-private class ExampleImageLoader {
- fun load(url: String): DummyInto = TODO()
- fun cancel(listener: Listener): Any = TODO()
-
- open class Listener {
- open fun onSuccess(bitmap: Bitmap): Unit = TODO()
- }
-
- companion object {
- fun get() = ExampleImageLoader()
- }
-}
-
-private class DummyInto {
- fun into(listener: ExampleImageLoader.Listener) {}
-}
-
-private open class Fragment {
-
- lateinit var context: Context
- open fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- TODO("not implemented")
- }
-
- fun requireContext(): Context = TODO()
-
- open fun onDestroyView() { }
-}
-
-private class FragmentExampleBinding {
- val root: View = TODO()
- var composeView: ComposeView
- companion object {
- fun inflate(
- li: LayoutInflater,
- container: ViewGroup?,
- boolean: Boolean
- ): FragmentExampleBinding { TODO() }
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/LayoutBasics.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/LayoutBasics.kt
deleted file mode 100644
index 5e3c966..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/LayoutBasics.kt
+++ /dev/null
@@ -1,287 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "RemoveEmptyParenthesesFromLambdaCall"
-)
-
-package androidx.compose.integration.docs.layout
-
-import android.annotation.SuppressLint
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.Card
-import androidx.compose.material.Scaffold
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/layouts/basics
- *
- * No action required if it's modified.
- */
-
-private object LayoutBasicsSnippet1 {
- @Composable
- fun ArtistCard() {
- Text("Alfred Sisley")
- Text("3 minutes ago")
- }
-}
-
-private object LayoutBasicsSnippet2 {
- @Composable
- fun ArtistCard() {
- Column {
- Text("Alfred Sisley")
- Text("3 minutes ago")
- }
- }
-}
-
-private object LayoutBasicsSnippet3 {
- @Composable
- fun ArtistCard(artist: Artist) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Image(/*...*/)
- Column {
- Text(artist.name)
- Text(artist.lastSeenOnline)
- }
- }
- }
-}
-
-private object LayoutBasicsSnippet4 {
- @Composable
- fun ArtistCard(artist: Artist) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.End
- ) {
- Image(/*...*/)
- Column { /*...*/ }
- }
- }
-}
-
-private object LayoutBasicsSnippet5 {
- @Composable
- fun ArtistCard(
- artist: Artist,
- onClick: () -> Unit
- ) {
- val padding = 16.dp
- Column(
- Modifier
- .clickable(onClick = onClick)
- .padding(padding)
- .fillMaxWidth()
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
- Spacer(Modifier.size(padding))
- Card(elevation = 4.dp) { /*...*/ }
- }
- }
-}
-
-private object LayoutBasicsSnippet6 {
- @Composable
- fun ArtistCard(/*...*/) {
- val padding = 16.dp
- Column(
- Modifier
- .clickable(onClick = onClick)
- .padding(padding)
- .fillMaxWidth()
- ) {
- // rest of the implementation
- }
- }
-}
-
-private object LayoutBasicsSnippet7 {
- @Composable
- fun ArtistCard(/*...*/) {
- val padding = 16.dp
- Column(
- Modifier
- .padding(padding)
- .clickable(onClick = onClick)
- .fillMaxWidth()
- ) {
- // rest of the implementation
- }
- }
-}
-
-private object LayoutBasicsSnippet8 {
- @Composable
- fun ArtistCard(/*...*/) {
- Row(
- modifier = Modifier.size(width = 400.dp, height = 100.dp)
- ) {
- Image(/*...*/)
- Column { /*...*/ }
- }
- }
-}
-
-private object LayoutBasicsSnippet9 {
- @Composable
- fun ArtistCard(/*...*/) {
- Row(
- modifier = Modifier.size(width = 400.dp, height = 100.dp)
- ) {
- Image(
- /*...*/
- modifier = Modifier.requiredSize(150.dp)
- )
- Column { /*...*/ }
- }
- }
-}
-
-private object LayoutBasicsSnippet10 {
- @Composable
- fun ArtistCard(/*...*/) {
- Row(
- modifier = Modifier.size(width = 400.dp, height = 100.dp)
- ) {
- Image(
- /*...*/
- modifier = Modifier.fillMaxHeight()
- )
- Column { /*...*/ }
- }
- }
-}
-
-private object LayoutBasicsSnippet11 {
- @Composable
- fun MatchParentSizeComposable() {
- Box {
- Spacer(Modifier.matchParentSize().background(Color.LightGray))
- ArtistCard()
- }
- }
-}
-
-private object LayoutBasicsSnippet12 {
- @Composable
- fun ArtistCard(artist: Artist) {
- Row(/*...*/) {
- Column {
- Text(
- text = artist.name,
- modifier = Modifier.paddingFromBaseline(top = 50.dp)
- )
- Text(artist.lastSeenOnline)
- }
- }
- }
-}
-
-private object LayoutBasicsSnippet13 {
- @Composable
- fun ArtistCard(artist: Artist) {
- Row(/*...*/) {
- Column {
- Text(artist.name)
- Text(
- text = artist.lastSeenOnline,
- modifier = Modifier.offset(x = 4.dp)
- )
- }
- }
- }
-}
-
-private object LayoutBasicsSnippet14 {
- @Composable
- fun ArtistCard(/*...*/) {
- Row(
- modifier = Modifier.fillMaxWidth()
- ) {
- Image(
- /*...*/
- modifier = Modifier.weight(2f)
- )
- Column(
- modifier = Modifier.weight(1f)
- ) {
- /*...*/
- }
- }
- }
-}
-
-private object LayoutBasicsSnippet15 {
- @Composable
- fun WithConstraintsComposable() {
- BoxWithConstraints {
- Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object LayoutBasicsSnippet16 {
- @Composable
- fun HomeScreen(/*...*/) {
- Scaffold(
- drawerContent = { /*...*/ },
- topBar = { /*...*/ },
- content = { /*...*/ }
- )
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private data class Artist(val name: String, val lastSeenOnline: String)
-private class Image
-
-@Composable
-private fun Image(modifier: Modifier = Modifier) {
-}
-
-@Composable
-private fun ArtistCard(modifier: Modifier = Modifier) {
-}
-
-private val onClick = {}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Material.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Material.kt
deleted file mode 100644
index 8756730..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Material.kt
+++ /dev/null
@@ -1,577 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "RemoveEmptyParenthesesFromLambdaCall"
-)
-
-package androidx.compose.integration.docs.layout
-
-import android.annotation.SuppressLint
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CornerSize
-import androidx.compose.material.BackdropScaffold
-import androidx.compose.material.BackdropValue
-import androidx.compose.material.BottomAppBar
-import androidx.compose.material.BottomDrawer
-import androidx.compose.material.BottomDrawerValue
-import androidx.compose.material.BottomSheetScaffold
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Divider
-import androidx.compose.material.DrawerValue
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.ExtendedFloatingActionButton
-import androidx.compose.material.FabPosition
-import androidx.compose.material.FloatingActionButton
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.ModalBottomSheetLayout
-import androidx.compose.material.ModalBottomSheetValue
-import androidx.compose.material.ModalDrawer
-import androidx.compose.material.Scaffold
-import androidx.compose.material.SnackbarDuration
-import androidx.compose.material.SnackbarResult
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.rememberBackdropScaffoldState
-import androidx.compose.material.rememberBottomDrawerState
-import androidx.compose.material.rememberBottomSheetScaffoldState
-import androidx.compose.material.rememberDrawerState
-import androidx.compose.material.rememberModalBottomSheetState
-import androidx.compose.material.rememberScaffoldState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/layouts/material
- *
- * No action required if it's modified.
- */
-
-private object MaterialSnippet1 {
- @Composable
- fun MyApp() {
- MaterialTheme {
- // Material Components like Button, Card, Switch, etc.
- }
- }
-}
-
-private object MaterialSnippet2 {
- @Composable
- fun MyButton() {
- Button(
- onClick = { /* ... */ },
- // Uses ButtonDefaults.ContentPadding by default
- contentPadding = PaddingValues(
- start = 20.dp,
- top = 12.dp,
- end = 20.dp,
- bottom = 12.dp
- )
- ) {
- // Inner content including an icon and a text label
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Like")
- }
- }
-}
-
-private object MaterialSnippet3 {
- @Composable
- fun MyExtendedFloatingActionButton() {
- ExtendedFloatingActionButton(
- onClick = { /* ... */ },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite"
- )
- },
- text = { Text("Like") }
- )
- }
-}
-
-private object MaterialSnippet4 {
- @Composable
- fun MyScaffold() {
- Scaffold(/* ... */) { contentPadding ->
- // Screen content
- Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet5 {
- @Composable
- fun MyTopAppBar() {
- Scaffold(
- topBar = {
- TopAppBar { /* Top app bar content */ }
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet6 {
- @Composable
- fun MyBottomAppBar() {
- Scaffold(
- bottomBar = {
- BottomAppBar { /* Bottom app bar content */ }
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet7 {
- @Composable
- fun MyFAB() {
- Scaffold(
- floatingActionButton = {
- FloatingActionButton(onClick = { /* ... */ }) {
- /* FAB content */
- }
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet8 {
- @Composable
- fun MyFAB() {
- Scaffold(
- floatingActionButton = {
- FloatingActionButton(onClick = { /* ... */ }) {
- /* FAB content */
- }
- },
- // Defaults to FabPosition.End
- floatingActionButtonPosition = FabPosition.Center
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet9 {
- @Composable
- fun MyFAB() {
- Scaffold(
- floatingActionButton = {
- FloatingActionButton(onClick = { /* ... */ }) {
- /* FAB content */
- }
- },
- // Defaults to false
- isFloatingActionButtonDocked = true,
- bottomBar = {
- BottomAppBar { /* Bottom app bar content */ }
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet10 {
- @Composable
- fun MyFAB() {
- Scaffold(
- floatingActionButton = {
- FloatingActionButton(onClick = { /* ... */ }) {
- /* FAB content */
- }
- },
- isFloatingActionButtonDocked = true,
- bottomBar = {
- BottomAppBar(
- // Defaults to null, that is, no cutout
- cutoutShape = MaterialTheme.shapes.small.copy(
- CornerSize(percent = 50)
- )
- ) {
- /* Bottom app bar content */
- }
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet11 {
- @Composable
- fun MySnackbar() {
- val scaffoldState = rememberScaffoldState()
- val scope = rememberCoroutineScope()
- Scaffold(
- scaffoldState = scaffoldState,
- floatingActionButton = {
- ExtendedFloatingActionButton(
- text = { Text("Show snackbar") },
- onClick = {
- scope.launch {
- scaffoldState.snackbarHostState
- .showSnackbar("Snackbar")
- }
- }
- )
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet12 {
- @Composable
- fun MySnackbar() {
- val scaffoldState = rememberScaffoldState()
- val scope = rememberCoroutineScope()
- Scaffold(
- scaffoldState = scaffoldState,
- floatingActionButton = {
- ExtendedFloatingActionButton(
- text = { Text("Show snackbar") },
- onClick = {
- scope.launch {
- val result = scaffoldState.snackbarHostState
- .showSnackbar(
- message = "Snackbar",
- actionLabel = "Action",
- // Defaults to SnackbarDuration.Short
- duration = SnackbarDuration.Indefinite
- )
- when (result) {
- SnackbarResult.ActionPerformed -> {
- /* Handle snackbar action performed */
- }
- SnackbarResult.Dismissed -> {
- /* Handle snackbar dismissed */
- }
- }
- }
- }
- )
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet13 {
- @Composable
- fun MyDrawer() {
- Scaffold(
- drawerContent = {
- Text("Drawer title", modifier = Modifier.padding(16.dp))
- Divider()
- // Drawer items
- }
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet14 {
- @Composable
- fun MyDrawer() {
- Scaffold(
- drawerContent = {
- // Drawer content
- },
- // Defaults to true
- drawerGesturesEnabled = false
- ) {
- // Screen content
- }
- }
-}
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-private object MaterialSnippet15 {
- @Composable
- fun MyDrawer() {
- val scaffoldState = rememberScaffoldState()
- val scope = rememberCoroutineScope()
- Scaffold(
- scaffoldState = scaffoldState,
- drawerContent = {
- // Drawer content
- },
- floatingActionButton = {
- ExtendedFloatingActionButton(
- text = { Text("Open or close drawer") },
- onClick = {
- scope.launch {
- scaffoldState.drawerState.apply {
- if (isClosed) open() else close()
- }
- }
- }
- )
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet16 {
- @Composable
- fun MyModalDrawer() {
- val drawerState = rememberDrawerState(DrawerValue.Closed)
- ModalDrawer(
- drawerState = drawerState,
- drawerContent = {
- // Drawer content
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet17 {
- @ExperimentalMaterialApi
- @Composable
- fun MyModalDrawer() {
- val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- // Drawer content
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet18 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBottomSheet() {
- BottomSheetScaffold(
- sheetContent = {
- // Sheet content
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet19 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBottomSheet() {
- BottomSheetScaffold(
- sheetContent = {
- // Sheet content
- },
- // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
- sheetPeekHeight = 128.dp,
- // Defaults to true
- sheetGesturesEnabled = false
-
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet20 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBottomSheet() {
- val scaffoldState = rememberBottomSheetScaffoldState()
- val scope = rememberCoroutineScope()
- BottomSheetScaffold(
- scaffoldState = scaffoldState,
- sheetContent = {
- // Sheet content
- },
- floatingActionButton = {
- ExtendedFloatingActionButton(
- text = { Text("Expand or collapse sheet") },
- onClick = {
- scope.launch {
- scaffoldState.bottomSheetState.apply {
- if (isCollapsed) expand() else collapse()
- }
- }
- }
- )
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet21 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBottomSheet() {
- val sheetState = rememberModalBottomSheetState(
- ModalBottomSheetValue.Hidden
- )
- ModalBottomSheetLayout(
- sheetState = sheetState,
- sheetContent = {
- // Sheet content
- }
- ) {
- // Screen content
- }
- }
-}
-
-private object MaterialSnippet22 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBackdrop() {
- BackdropScaffold(
- appBar = {
- // Top app bar
- },
- backLayerContent = {
- // Back layer content
- },
- frontLayerContent = {
- // Front layer content
- }
- )
- }
-}
-
-private object MaterialSnippet23 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBackdrop() {
- BackdropScaffold(
- appBar = {
- // Top app bar
- },
- backLayerContent = {
- // Back layer content
- },
- frontLayerContent = {
- // Front layer content
- },
- // Defaults to BackdropScaffoldDefaults.PeekHeight
- peekHeight = 40.dp,
- // Defaults to BackdropScaffoldDefaults.HeaderHeight
- headerHeight = 60.dp,
- // Defaults to true
- gesturesEnabled = false
- )
- }
-}
-
-private object MaterialSnippet24 {
- @ExperimentalMaterialApi
- @Composable
- fun MyBackdrop() {
- val scaffoldState = rememberBackdropScaffoldState(
- BackdropValue.Concealed
- )
- val scope = rememberCoroutineScope()
- BackdropScaffold(
- scaffoldState = scaffoldState,
- appBar = {
- TopAppBar(
- title = { Text("Backdrop") },
- navigationIcon = {
- if (scaffoldState.isConcealed) {
- IconButton(
- onClick = {
- scope.launch { scaffoldState.reveal() }
- }
- ) {
- Icon(
- Icons.Default.Menu,
- contentDescription = "Menu"
- )
- }
- } else {
- IconButton(
- onClick = {
- scope.launch { scaffoldState.conceal() }
- }
- ) {
- Icon(
- Icons.Default.Close,
- contentDescription = "Close"
- )
- }
- }
- },
- elevation = 0.dp,
- backgroundColor = Color.Transparent
- )
- },
- backLayerContent = {
- // Back layer content
- },
- frontLayerContent = {
- // Front layer content
- }
- )
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/libraries/Libraries.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/libraries/Libraries.kt
deleted file mode 100644
index 1418881..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/libraries/Libraries.kt
+++ /dev/null
@@ -1,314 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER",
- "RedundantSuspendModifier", "CascadeIf", "ClassName", "SameParameterValue"
-)
-
-package androidx.compose.integration.docs.libraries
-
-import android.graphics.Bitmap
-import android.net.Uri
-import androidx.activity.compose.BackHandler
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts.GetContent
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Button
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.IntSize
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.navigation
-import kotlinx.coroutines.flow.Flow
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/libraries
- *
- * No action required if it's modified.
- */
-
-private object LibrariesSnippetActivityResult {
- @Composable
- fun GetContentExample() {
- var imageUri by remember { mutableStateOf<Uri?>(null) }
- val launcher = rememberLauncherForActivityResult(GetContent()) { uri: Uri? ->
- imageUri = uri
- }
- Column {
- Button(onClick = { launcher.launch("image/*") }) {
- Text(text = "Load Image")
- }
- Image(
- painter = rememberImagePainter(imageUri),
- contentDescription = "My Image"
- )
- }
- }
-}
-
-@Composable
-private fun LibrariesSnippetBackHandler() {
- var backHandlingEnabled by remember { mutableStateOf(true) }
- BackHandler(backHandlingEnabled) {
- // Handle back press
- }
-}
-
-private object LibrariesSnippetAddingViewModel {
- class MyViewModel : ViewModel() { /*...*/ }
-
- @Composable
- fun MyScreen(
- viewModel: MyViewModel = viewModel()
- ) {
- // use viewModel here
- }
-}
-
-private object LibrariesSnippetSameViewModelTwice {
- @Composable
- fun MyScreen(
- // Returns the same instance as long as the activity is alive,
- // just as if you grabbed the instance from an Activity or Fragment
- viewModel: MyViewModel = viewModel()
- ) { /* ... */ }
-
- @Composable
- fun MyScreen2(
- viewModel: MyViewModel = viewModel() // Same instance as in MyExample
- ) { /* ... */ }
-}
-
-private object LibrariesSnippetRecomposesWhenStateChanges {
- @Composable
- fun MyScreen(
- viewModel: MyViewModel = viewModel()
- ) {
- val dataExample = viewModel.exampleLiveData.observeAsState()
-
- // Because the state is read here,
- // MyExample recomposes whenever dataExample changes.
- dataExample.value?.let {
- ShowData(dataExample)
- }
- }
-}
-
-private object LibrariesSnippetHilt {
- @HiltViewModel
- class MyViewModel @Inject constructor(
- private val savedStateHandle: SavedStateHandle,
- private val repository: ExampleRepository
- ) : ViewModel() { /* ... */ }
-
- @Composable
- fun MyScreen(
- viewModel: MyViewModel = viewModel()
- ) { /* ... */ }
-}
-
-private object LibrariesSnippetHiltViewModel {
- // import androidx.hilt.navigation.compose.hiltViewModel
-
- @Composable
- fun MyApp() {
- NavHost(navController, startDestination = startRoute) {
- composable("example") { backStackEntry ->
- // Creates a ViewModel from the current BackStackEntry
- // Available in the androidx.hilt:hilt-navigation-compose artifact
- val viewModel = hiltViewModel<MyViewModel>()
- MyScreen(viewModel)
- }
- /* ... */
- }
- }
-}
-
-private object LibrariesSnippetBackStackEntry {
- // import androidx.hilt.navigation.compose.hiltViewModel
- // import androidx.navigation.compose.getBackStackEntry
-
- @Composable
- fun MyApp() {
- NavHost(navController, startDestination = startRoute) {
- navigation(startDestination = innerStartRoute, route = "Parent") {
- // ...
- composable("exampleWithRoute") { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- navController.getBackStackEntry("Parent")
- }
- val parentViewModel = hiltViewModel<ParentViewModel>(
- parentEntry
- )
- ExampleWithRouteScreen(parentViewModel)
- }
- }
- }
- }
-}
-
-private object LibrariesSnippetPaging {
- @Composable
- fun MyScreen(flow: Flow<PagingData<String>>) {
- val lazyPagingItems = flow.collectAsLazyPagingItems()
- LazyColumn {
- items(lazyPagingItems) {
- Text("Item is $it")
- }
- }
- }
-}
-
-private object LibrariesSnippetRemoteImages {
- @Composable
- fun MyScreen() {
- val painter = rememberImagePainter(
- data = "https://picsum.photos/300/300",
- builder = {
- crossfade(true)
- }
- )
-
- Box {
- Image(
- painter = painter,
- contentDescription = stringResource(R.string.image_content_desc),
- )
-
- when (painter.state) {
- is ImagePainter.State.Loading -> {
- // Display a circular progress indicator whilst loading
- CircularProgressIndicator(Modifier.align(Alignment.Center))
- }
- is ImagePainter.State.Error -> {
- // If you wish to display some content if the request fails
- }
- }
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private object R {
- object drawable {
- const val ic_error = 1
- }
- object string {
- const val image_content_desc = 2
- }
-}
-
-private fun ShowData(dataExample: State<String?>): Nothing = TODO()
-private class ExampleImageLoader {
- fun load(url: String): DummyInto = TODO()
- fun cancel(listener: Listener): Any = TODO()
-
- open class Listener {
- open fun onSuccess(bitmap: Bitmap): Unit = TODO()
- }
-
- companion object {
- fun get() = ExampleImageLoader()
- }
-}
-
-private class DummyInto {
- fun into(listener: ExampleImageLoader.Listener) {}
-}
-
-private class SavedStateHandle
-private class ExampleRepository
-private annotation class HiltViewModel
-private annotation class Inject
-
-private class ParentViewModel : ViewModel()
-private class MyViewModel : ViewModel() {
- val exampleLiveData = MutableLiveData(" ")
-}
-
-private inline fun <reified VM : ViewModel> hiltViewModel(): VM { TODO() }
-private inline fun <reified VM : ViewModel> hiltViewModel(backStackEntry: NavBackStackEntry): VM {
- TODO()
-}
-
-@Composable
-private fun MyScreen(vm: MyViewModel) {
- TODO()
-}
-
-@Composable
-private fun ExampleWithRouteScreen(vm: ParentViewModel) {
- TODO()
-}
-
-private val navController: NavHostController = TODO()
-private val innerStartRoute: String = TODO()
-private val startRoute: String = TODO()
-
-private class PagingData<T>
-
-private fun Flow<PagingData<String>>.collectAsLazyPagingItems() = listOf("")
-
-// Coil
-interface ImageRequest { interface Builder }
-
-@Composable
-fun rememberImagePainter(
- data: Any?,
- builder: ImageRequest.Builder.() -> Unit = {},
-): LoadPainter { TODO() }
-fun ImageRequest.Builder.crossfade(enable: Boolean): Nothing = TODO()
-
-fun interface Loader<R> {
- fun load(request: R, size: IntSize): Flow<ImagePainter.State>
-}
-abstract class LoadPainter : Painter() {
- var state: ImagePainter.State by mutableStateOf(ImagePainter.State.Loading)
- private set
-}
-interface ImagePainter {
- sealed class State {
- object Loading : State()
- object Error : State()
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lifecycle/Lifecycle.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lifecycle/Lifecycle.kt
deleted file mode 100644
index f85e23d..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lifecycle/Lifecycle.kt
+++ /dev/null
@@ -1,138 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "SimplifyBooleanWithConstants"
-)
-
-package androidx.compose.integration.docs.lifecycle
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.key
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/lifecycle
- *
- * No action required if it's modified.
- */
-
-private object LifecycleSnippet1 {
- @Composable
- fun MyComposable() {
- Column {
- Text("Hello")
- Text("World")
- }
- }
-}
-private object LifecycleSnippet2 {
- @Composable
- fun LoginScreen(showError: Boolean) {
- if (showError) {
- LoginError()
- }
- LoginInput() // This call site affects where LoginInput is placed in Composition
- }
-
- @Composable
- fun LoginInput() { /* ... */ }
-}
-
-private object LifecycleSnippet3 {
- @Composable
- fun MoviesScreen(movies: List<Movie>) {
- Column {
- for (movie in movies) {
- // MovieOverview composables are placed in Composition given its
- // index position in the for loop
- MovieOverview(movie)
- }
- }
- }
-}
-
-private object LifecycleSnippet4 {
- @Composable
- fun MovieOverview(movie: Movie) {
- Column {
- // Side effect explained later in the docs. If MovieOverview
- // recomposes, while fetching the image is in progress,
- // it is cancelled and restarted.
- val image = loadNetworkImage(movie.url)
- MovieHeader(image)
-
- /* ... */
- }
- }
-}
-
-private object LifecycleSnippet5 {
- @Composable
- fun MoviesScreen(movies: List<Movie>) {
- Column {
- for (movie in movies) {
- key(movie.id) { // Unique ID for this movie
- MovieOverview(movie)
- }
- }
- }
- }
-}
-
-private object LifecycleSnippet6 {
- @Composable
- fun MoviesScreen(movies: List<Movie>) {
- LazyColumn {
- items(movies, key = { movie -> movie.id }) { movie ->
- MovieOverview(movie)
- }
- }
- }
-}
-
-private object LifecycleSnippet7 {
- // Marking the type as stable to favor skipping and smart recompositions.
- @Stable
- interface UiState<T : Result<T>> {
- val value: T?
- val exception: Throwable?
-
- val hasError: Boolean
- get() = exception != null
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-@Composable
-private fun LoginError() { }
-
-@Composable
-private fun MovieOverview(movie: Movie) { }
-@Composable
-private fun MovieHeader(movie: String) { }
-private data class Movie(val id: Long, val url: String = "")
-
-private fun loadNetworkImage(url: String): String = ""
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
deleted file mode 100644
index 3ad5fb2..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE")
-
-package androidx.compose.integration.docs.preview
-
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults.buttonColors
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/preview
- *
- * No action required if it's modified.
- */
-
-private object PreviewSnippet1 {
- @Composable
- fun Greeting(name: String) {
- Text(text = "Hello $name!")
- }
-}
-
-private object PreviewSnippet2 {
- @Preview
- @Composable
- fun PreviewGreeting() {
- Greeting("Android")
- }
-}
-
-private object PreviewSnippet3 {
- @Preview(name = "Android greeting")
- @Composable
- fun PreviewGreeting() {
- Greeting("Android")
- }
-}
-
-private object PreviewSnippet4 {
- @Preview(name = "Long greeting")
- @Composable
- fun PreviewLongGreeting() {
- Greeting("my valued friend, whom I am incapable of " +
- "greeting without using a great many words")
- }
- @Preview(name = "Newline greeting")
- @Composable
- fun PreviewNewlineGreeting() {
- Greeting("world\nwith a line break")
- }
-}
-
-private object PreviewSnippet5 {
- @Composable
- fun Counter(count: Int, updateCount: (Int) -> Unit) {
- Button(
- onClick = { updateCount(count + 1) },
- colors = buttonColors(
- backgroundColor = if (count > 5) Color.Green else Color.White
- )
- ) {
- Text("I've been clicked $count times")
- }
- }
- @Preview
- @Composable
- fun PreviewCounter() {
- val counterState = remember { mutableIntStateOf(0) }
-
- Counter(
- count = counterState.intValue,
- updateCount = { newCount ->
- counterState.intValue = newCount
- }
- )
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-@Composable private fun Greeting(name: String) {}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/sideeffects/SideEffects.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/sideeffects/SideEffects.kt
deleted file mode 100644
index 928fce9..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/sideeffects/SideEffects.kt
+++ /dev/null
@@ -1,349 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "SimplifyBooleanWithConstants"
-)
-
-package androidx.compose.integration.docs.sideeffects
-
-import android.annotation.SuppressLint
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.Button
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Scaffold
-import androidx.compose.material.ScaffoldState
-import androidx.compose.material.Text
-import androidx.compose.material.rememberScaffoldState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.produceState
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import kotlin.Boolean
-import kotlin.Exception
-import kotlin.Long
-import kotlin.Nothing
-import kotlin.String
-import kotlin.Suppress
-import kotlin.Unit
-import kotlin.random.Random
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/side-effects
- *
- * No action required if it's modified.
- */
-
-@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-@ExperimentalMaterialApi
-private object SideEffectsSnippet1 {
- @Composable
- fun MyScreen(
- state: UiState<List<Movie>>,
- scaffoldState: ScaffoldState = rememberScaffoldState()
- ) {
-
- // If the UI state contains an error, show snackbar
- if (state.hasError) {
-
- // `LaunchedEffect` will cancel and re-launch if
- // `scaffoldState.snackbarHostState` changes
- LaunchedEffect(scaffoldState.snackbarHostState) {
- // Show snackbar using a coroutine, when the coroutine is cancelled the
- // snackbar will automatically dismiss. This coroutine will cancel whenever
- // `state.hasError` is false, and only start when `state.hasError` is true
- // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
- scaffoldState.snackbarHostState.showSnackbar(
- message = "Error message",
- actionLabel = "Retry message"
- )
- }
- }
-
- Scaffold(scaffoldState = scaffoldState) {
- /* ... */
- }
- }
-}
-
-@ExperimentalMaterialApi
-private object SideEffectsSnippet2 {
- @Composable
- fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
-
- // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
- val scope = rememberCoroutineScope()
-
- Scaffold(scaffoldState = scaffoldState) { innerPadding ->
- Column(Modifier.padding(innerPadding)) {
- /* ... */
- Button(
- onClick = {
- // Create a new coroutine in the event handler to show a snackbar
- scope.launch {
- scaffoldState.snackbarHostState.showSnackbar("Something happened!")
- }
- }
- ) {
- Text("Press me")
- }
- }
- }
- }
-}
-
-private object SideEffectsSnippet3 {
- @Composable
- fun LandingScreen(onTimeout: () -> Unit) {
-
- // This will always refer to the latest onTimeout function that
- // LandingScreen was recomposed with
- val currentOnTimeout by rememberUpdatedState(onTimeout)
-
- // Create an effect that matches the lifecycle of LandingScreen.
- // If LandingScreen recomposes, the delay shouldn't start again.
- LaunchedEffect(true) {
- delay(SplashWaitTimeMillis)
- currentOnTimeout()
- }
-
- /* Landing screen content */
- }
-}
-
-private object SideEffectsSnippet4 {
- @Composable
- fun HomeScreen(
- lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
- onStart: () -> Unit, // Send the 'started' analytics event
- onStop: () -> Unit // Send the 'stopped' analytics event
- ) {
- // Safely update the current lambdas when a new one is provided
- val currentOnStart by rememberUpdatedState(onStart)
- val currentOnStop by rememberUpdatedState(onStop)
-
- // If `lifecycleOwner` changes, dispose and reset the effect
- DisposableEffect(lifecycleOwner) {
- // Create an observer that triggers our remembered callbacks
- // for sending analytics events
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_START) {
- currentOnStart()
- } else if (event == Lifecycle.Event.ON_STOP) {
- currentOnStop()
- }
- }
-
- // Add the observer to the lifecycle
- lifecycleOwner.lifecycle.addObserver(observer)
-
- // When the effect leaves the Composition, remove the observer
- onDispose {
- lifecycleOwner.lifecycle.removeObserver(observer)
- }
- }
-
- /* Home screen content */
- }
-}
-
-private object SideEffectsSnippet5 {
- @Composable
- fun rememberAnalytics(user: User): FirebaseAnalytics {
- val analytics: FirebaseAnalytics = remember {
- // START - DO NOT COPY IN CODE SNIPPET
- FirebaseAnalytics()
- // END - DO NOT COPY IN CODE SNIPPET, just use /* ... */
- }
-
- // On every successful composition, update FirebaseAnalytics with
- // the userType from the current User, ensuring that future analytics
- // events have this metadata attached
- SideEffect {
- analytics.setUserProperty("userType", user.userType)
- }
- return analytics
- }
-}
-
-private object SideEffectsSnippet6 {
- @Composable
- fun loadNetworkImage(
- url: String,
- imageRepository: ImageRepository
- ): State<Result<Image>> {
-
- // Creates a State<T> with Result.Loading as initial value
- // If either `url` or `imageRepository` changes, the running producer
- // will cancel and will be re-launched with the new inputs.
- return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
-
- // In a coroutine, can make suspend calls
- val image = imageRepository.load(url)
-
- // Update State with either an Error or Success result.
- // This will trigger a recomposition where this State is read
- value = if (image == null) {
- Result.Error
- } else {
- Result.Success(image)
- }
- }
- }
-}
-
-private object SideEffectsSnippet7 {
- @Composable
- fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
-
- val todoTasks = remember { mutableStateListOf<String>() }
-
- // Calculate high priority tasks only when the todoTasks or highPriorityKeywords
- // change, not on every recomposition
- val highPriorityTasks by remember(highPriorityKeywords) {
- derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
- }
-
- Box(Modifier.fillMaxSize()) {
- LazyColumn {
- items(highPriorityTasks) { /* ... */ }
- items(todoTasks) { /* ... */ }
- }
- /* Rest of the UI where users can add elements to the list */
- }
- }
-}
-
-@Composable
-private fun SideEffectsSnippet8(messages: List<Message>) {
- val listState = rememberLazyListState()
-
- LazyColumn(state = listState) {
- // ...
- }
-
- LaunchedEffect(listState) {
- snapshotFlow { listState.firstVisibleItemIndex }
- .map { index -> index > 0 }
- .distinctUntilChanged()
- .filter { it == true }
- .collect {
- MyAnalyticsService.sendScrolledPastFirstItemEvent()
- }
- }
-}
-
-private object SideEffectsSnippet9 {
- @Composable
- fun HomeScreen(
- lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
- onStart: () -> Unit, // Send the 'started' analytics event
- onStop: () -> Unit // Send the 'stopped' analytics event
- ) {
- // These values never change in Composition
- val currentOnStart by rememberUpdatedState(onStart)
- val currentOnStop by rememberUpdatedState(onStop)
-
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- // START - DO NOT COPY IN CODE SNIPPET
- if (event == Lifecycle.Event.ON_START) {
- currentOnStart()
- } else if (event == Lifecycle.Event.ON_STOP) {
- currentOnStop()
- }
- // END - DO NOT COPY IN CODE SNIPPET, just use /* ... */
- }
-
- lifecycleOwner.lifecycle.addObserver(observer)
- onDispose {
- lifecycleOwner.lifecycle.removeObserver(observer)
- }
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private const val SplashWaitTimeMillis = 1000L
-
-private data class Movie(val id: Long, val url: String = "")
-
-private data class UiState<T>(
- val loading: Boolean = false,
- val exception: Exception? = null,
- val data: T? = null
-) {
- val hasError: Boolean
- get() = exception != null
-}
-
-private class Message(val id: Long)
-private class Image
-private class ImageRepository {
- fun load(url: String): Image? = if (Random.nextInt() == 0) Image() else null // Avoid warnings
-}
-
-private class FirebaseAnalytics {
- fun setUserProperty(name: String, value: String) {}
-}
-
-private sealed class Result<out R> {
- data class Success<out T>(val data: T) : Result<T>()
- object Loading : Result<Nothing>()
- object Error : Result<Nothing>()
-}
-
-private class User(val userType: String = "user")
-private class Weather
-private class Greeting(val name: String)
-private fun prepareGreeting(user: User, weather: Weather) = Greeting("haha")
-
-private fun String.containsWord(input: List<String>): Boolean = false
-
-private object MyAnalyticsService {
- fun sendScrolledPastFirstItemEvent() = Unit
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
deleted file mode 100644
index bda62b5..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
+++ /dev/null
@@ -1,410 +0,0 @@
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE")
-
-package androidx.compose.integration.docs.testing
-
-import android.os.Build
-import android.view.KeyEvent as AndroidKeyEvent
-import android.view.KeyEvent.ACTION_DOWN as ActionDown
-import android.view.KeyEvent.KEYCODE_A as KeyCodeA
-import androidx.activity.ComponentActivity
-import androidx.annotation.RequiresApi
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.layout.FirstBaseline
-import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertAll
-import androidx.compose.ui.test.assertAny
-import androidx.compose.ui.test.assertContentDescriptionContains
-import androidx.compose.ui.test.assertContentDescriptionEquals
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.assertHasNoClickAction
-import androidx.compose.ui.test.assertHeightIsAtLeast
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEnabled
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertIsNotEnabled
-import androidx.compose.ui.test.assertIsNotFocused
-import androidx.compose.ui.test.assertIsNotSelected
-import androidx.compose.ui.test.assertIsOff
-import androidx.compose.ui.test.assertIsOn
-import androidx.compose.ui.test.assertIsSelectable
-import androidx.compose.ui.test.assertIsSelected
-import androidx.compose.ui.test.assertIsToggleable
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertRangeInfoEquals
-import androidx.compose.ui.test.assertTextContains
-import androidx.compose.ui.test.assertTextEquals
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertValueEquals
-import androidx.compose.ui.test.assertWidthIsAtLeast
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.doubleClick
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.filterToOne
-import androidx.compose.ui.test.getAlignmentLinePosition
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.hasAnyAncestor
-import androidx.compose.ui.test.hasAnyChild
-import androidx.compose.ui.test.hasAnyDescendant
-import androidx.compose.ui.test.hasAnySibling
-import androidx.compose.ui.test.hasClickAction
-import androidx.compose.ui.test.hasContentDescription
-import androidx.compose.ui.test.hasImeAction
-import androidx.compose.ui.test.hasNoClickAction
-import androidx.compose.ui.test.hasNoScrollAction
-import androidx.compose.ui.test.hasParent
-import androidx.compose.ui.test.hasProgressBarRangeInfo
-import androidx.compose.ui.test.hasScrollAction
-import androidx.compose.ui.test.hasSetTextAction
-import androidx.compose.ui.test.hasStateDescription
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.isDialog
-import androidx.compose.ui.test.isEnabled
-import androidx.compose.ui.test.isFocusable
-import androidx.compose.ui.test.isFocused
-import androidx.compose.ui.test.isHeading
-import androidx.compose.ui.test.isNotEnabled
-import androidx.compose.ui.test.isNotFocusable
-import androidx.compose.ui.test.isNotFocused
-import androidx.compose.ui.test.isNotSelected
-import androidx.compose.ui.test.isOff
-import androidx.compose.ui.test.isOn
-import androidx.compose.ui.test.isPopup
-import androidx.compose.ui.test.isRoot
-import androidx.compose.ui.test.isSelectable
-import androidx.compose.ui.test.isSelected
-import androidx.compose.ui.test.isToggleable
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.longClick
-import androidx.compose.ui.test.multiTouchSwipe
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onAllNodesWithTag
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onAncestors
-import androidx.compose.ui.test.onChild
-import androidx.compose.ui.test.onChildAt
-import androidx.compose.ui.test.onChildren
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onLast
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.onParent
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.onSibling
-import androidx.compose.ui.test.onSiblings
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performImeAction
-import androidx.compose.ui.test.performKeyPress
-import androidx.compose.ui.test.performScrollTo
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTextClearance
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.performTextReplacement
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.pinch
-import androidx.compose.ui.test.printToLog
-import androidx.compose.ui.test.printToString
-import androidx.compose.ui.test.swipe
-import androidx.compose.ui.test.swipeDown
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/testing-cheatsheet.html
- *
- * No action required if it's modified.
- */
-
-@Composable
-private fun TestingCheatSheetFinders() {
- // FINDERS
- composeTestRule.onNode(matcher)
- composeTestRule.onAllNodes(matcher)
- composeTestRule.onNodeWithContentDescription("label")
- composeTestRule.onAllNodesWithContentDescription("label")
- composeTestRule.onNodeWithTag("tag")
- composeTestRule.onAllNodesWithTag("tag")
- composeTestRule.onNodeWithText("text")
- composeTestRule.onAllNodesWithText("text")
- composeTestRule.onRoot()
-
- // OPTIONS
- composeTestRule.onNode(matcher, useUnmergedTree = true)
-
- // SELECTORS
- composeTestRule.onAllNodes(matcher)
- .filter(matcher)
- .filterToOne(matcher)
- composeTestRule.onNode(matcher)
- .onAncestors()
- composeTestRule.onNode(matcher)
- .onChild()
- .onChildAt(0)
- .onChildren()
- .onFirst()
- composeTestRule.onAllNodes(matcher)
- .onLast()
- .onParent()
- .onSibling()
- .onSiblings()
-
- // HIERARCHICAL
- composeTestRule.onNode(
- hasAnyAncestor(matcher) and
- hasAnyChild(matcher) and
- hasAnyDescendant(matcher) and
- hasAnySibling(matcher) and
- hasParent(matcher)
- )
-
- // MATCHERS
- composeTestRule.onNode(
- hasClickAction() and
- hasNoClickAction() and
- hasContentDescription("label") and
- hasImeAction(ImeAction.Default) and
- hasProgressBarRangeInfo(rangeInfo) and
- hasScrollAction() and
- hasNoScrollAction() and
- hasSetTextAction() and
- hasStateDescription("label") and
- hasTestTag("tag") and
- hasText("text") and
- isDialog() and
- isEnabled() and
- isFocusable() and
- isFocused() and
- isHeading() and
- isNotEnabled() and
- isNotFocusable() and
- isNotFocused() and
- isNotSelected() and
- isOff() and
- isOn() and
- isPopup() and
- isRoot() and
- isSelectable() and
- isSelected() and
- isToggleable()
- )
-}
-
-@Composable
-private fun TestingCheatSheetActions() {
- composeTestRule.onRoot()
- .performClick()
- .performTouchInput { longClick() }
- .performScrollTo()
- .performSemanticsAction(SemanticsActions.OnLongClick)
- composeTestRule.onRoot()
- .performKeyPress(keyEvent2)
- composeTestRule.onRoot()
- .performImeAction()
- composeTestRule.onRoot()
- .performTextClearance()
- composeTestRule.onRoot()
- .performTextInput("text")
- composeTestRule.onRoot()
- .performTextReplacement("text")
-
- // GESTURES
-
- composeTestRule.onRoot().performTouchInput {
- click()
- longClick()
- doubleClick()
- swipe(this.center, offset)
- swipe({ Offset(it.toFloat(), it.toFloat()) }, 1L)
- @OptIn(ExperimentalTestApi::class)
- multiTouchSwipe(listOf { Offset(it.toFloat(), it.toFloat()) }, 1L)
- pinch(offset, offset, offset, offset)
- swipeWithVelocity(offset, offset, 1f)
- swipeUp()
- swipeDown()
- swipeLeft()
- swipeRight()
-
- // PARTIAL GESTURES
- down(offset)
- moveTo(offset)
- updatePointerTo(0, offset)
- moveBy(offset)
- updatePointerBy(0, offset)
- move()
- percentOffset()
- up()
- cancel()
-
- currentPosition(0)
- advanceEventTime(1L)
-
- eventPeriodMillis
- visibleSize
-
- bottom
- bottomCenter
- bottomLeft
- bottomRight
- center
- centerLeft
- centerRight
- centerX
- centerY
- height
- left
- right
- top
- topCenter
- topLeft
- topRight
- width
- }
-}
-
-@Composable
-private fun TestingCheatSheetAssertions() {
- composeTestRule.onRoot().apply {
- assert(matcher)
- assertContentDescriptionContains("label")
- assertContentDescriptionEquals("label")
- assertHasClickAction()
- assertHasNoClickAction()
- assertIsDisplayed()
- assertIsEnabled()
- assertIsFocused()
- assertIsNotDisplayed()
- assertIsNotEnabled()
- assertIsNotFocused()
- assertIsNotSelected()
- assertIsOff()
- assertIsOn()
- assertIsSelectable()
- assertIsSelected()
- assertIsToggleable()
- assertRangeInfoEquals(rangeInfo)
- assertTextContains("text")
- assertTextEquals("text")
- assertValueEquals("value")
- }
-
- composeTestRule.onRoot().apply {
- assertDoesNotExist()
- assertExists()
- }
-
- // COLLECTIONS
- composeTestRule.onAllNodes(matcher)
- .assertAll(matcher)
- .assertAny(matcher)
- .assertCountEquals(1)
-
- // BOUNDS
- composeTestRule.onRoot()
- .assertWidthIsEqualTo(1.dp)
- .assertHeightIsEqualTo(1.dp)
- .assertWidthIsAtLeast(1.dp)
- .assertHeightIsAtLeast(1.dp)
- .assertPositionInRootIsEqualTo(1.dp, 1.dp)
- .assertTopPositionInRootIsEqualTo(1.dp)
- .assertLeftPositionInRootIsEqualTo(1.dp)
-
- composeTestRule.onNodeWithTag("button")
- .getAlignmentLinePosition(FirstBaseline)
-
- composeTestRule.onRoot()
- .getUnclippedBoundsInRoot()
-}
-
-@OptIn(DelicateCoroutinesApi::class)
-@RequiresApi(Build.VERSION_CODES.O)
-private fun TestingCheatSheetOther() {
-
- // COMPOSE TEST RULE
- nonAndroidComposeTestRule.apply {
- setContent { }
- density
- runOnIdle { }
- runOnUiThread { }
- waitForIdle()
- waitUntil { true }
- mainClock.apply {
- autoAdvance
- currentTime
- advanceTimeBy(1L)
- advanceTimeByFrame()
- advanceTimeUntil { true }
- }
- registerIdlingResource(idlingResource)
- unregisterIdlingResource(idlingResource)
- }
- GlobalScope.launch {
- nonAndroidComposeTestRule.awaitIdle()
- }
-
- // ANDROID COMPOSE TEST RULE
- composeTestRule.activity
- composeTestRule.activityRule
-
- // Capture and debug
- composeTestRule.onRoot().apply {
- printToLog("TAG")
- printToString()
- captureToImage()
- }
- // MATCHERS
- matcher.matches(composeTestRule.onRoot().fetchSemanticsNode())
-}
-
-/*
-Fakes needed for snippets to build:
- */
-private val matcher = isDialog()
-private class FakeActivity : ComponentActivity()
-private val composeTestRule = createAndroidComposeRule<FakeActivity>()
-private val nonAndroidComposeTestRule = createComposeRule()
-private val keyEvent2 = KeyEvent(AndroidKeyEvent(ActionDown, KeyCodeA))
-private val offset = Offset(0f, 0f)
-private val rangeInfo = ProgressBarRangeInfo(0f, 0f..1f)
-private val idlingResource = object : IdlingResource {
- override val isIdleNow: Boolean
- get() = TODO("Stub!")
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
deleted file mode 100644
index ab880fe..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE")
-
-package androidx.compose.integration.docs.testing
-
-import android.view.KeyEvent as AndroidKeyEvent
-import android.view.KeyEvent.ACTION_DOWN as ActionDown
-import android.view.KeyEvent.KEYCODE_A as KeyCodeA
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.integration.docs.testing.CreateSemanticsPropertySnippet.PickedDateKey
-import androidx.compose.material.Scaffold
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.AccessibilityAction
-import androidx.compose.ui.semantics.SemanticsPropertyKey
-import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.testTagsAsResourceId
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertAll
-import androidx.compose.ui.test.assertAny
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasAnyAncestor
-import androidx.compose.ui.test.hasAnyDescendant
-import androidx.compose.ui.test.hasAnySibling
-import androidx.compose.ui.test.hasClickAction
-import androidx.compose.ui.test.hasParent
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onChildren
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performKeyPress
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.printToLog
-import androidx.compose.ui.test.swipeLeft
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/testing
- *
- * No action required if it's modified.
- */
-
-@Composable
-private fun ButtonSnippet() {
- MyButton(modifier = Modifier.semantics { contentDescription = "Like button" })
-}
-
-private object ComposeTestRuleSnippet {
- // file: app/src/androidTest/kotlin/com/package/MyComposeTest.kt
-
- class MyComposeTest {
-
- @get:Rule
- val composeTestRule = createAndroidComposeRule<MyActivity>()
- // createComposeRule() if you don't need access to the activityTestRule
-
- @Test fun myTest() {
- // Start the app
- composeTestRule.setContent {
- MyAppTheme {
- MainScreen(uiState = exampleUiState, /*...*/)
- }
- }
-
- composeTestRule.onNodeWithText("Continue").performClick()
-
- composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
- }
- }
-}
-
-@Composable
-private fun SelectNodesSnippets() {
- // single node
- // It's API, see line below.
- // onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
- composeTestRule
- .onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
-
- // multiple nodes
- // It's API, see line below.
- // onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
-
- // Example
- composeTestRule
- .onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
-}
-
-@Composable
-private fun MergeTextSnippet() {
- MyButton {
- Text("Hello")
- Text("World")
- }
-
- composeTestRule.onRoot().printToLog("TAG")
-
- composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
-}
-
-@Composable
-private fun UseUnmergedTreeSnippet() {
- composeTestRule.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
-}
-
-// assertions
-
-@Composable
-private fun CheckAssertionsOneNodeSnippet() {
- // Single matcher:
- composeTestRule.onNode(matcher).assert(hasText("Button")) // hasText is a SemanticsMatcher
-
- // Multiple matchers can use and / or
- composeTestRule.onNode(matcher)
- .assert(hasText("Button") or hasText("Button2"))
-}
-
-@Composable
-private fun CheckAssertionsMultipleNodesSnippet() {
- // Check number of matched nodes
- composeTestRule
- .onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
- // At least one matches
- composeTestRule
- .onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
- // All of them match
- composeTestRule
- .onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
-}
-
-@Composable
-private fun SemanticsNodeInteraction.PerformClickEtc() {
- val listOfActions = listOf(
- // start snippet
- performClick(),
- performSemanticsAction(key),
- performKeyPress(keyEvent),
- performTouchInput { swipeLeft() }
- // end snippet
- )
-}
-
-@Composable
-private fun HierarchicalApiSnippets() {
- // It's API, look for changes below.
- val matcher = SemanticsMatcher("test", { true })
- hasParent(matcher)
- hasAnySibling(matcher)
- hasAnyAncestor(matcher)
- hasAnyDescendant(matcher)
-}
-
-@Composable
-private fun AssertIsDisplayedSnippet() {
- composeTestRule.onNode(hasParent(hasText("Button")))
- .assertIsDisplayed()
-}
-
-@Composable
-private fun SelectorsSnippet() {
- composeTestRule.onNode(hasTestTag("Players"))
- .onChildren()
- .filter(hasClickAction())
- .assertCountEquals(4)
- .onFirst()
- .assert(hasText("John"))
-}
-
-private object SyncSnippet {
- @Test fun counterTest() {
- var myCounter by mutableIntStateOf(0) // State that can cause recompositions
- var lastSeenValue = 0 // Used to track recompositions
- composeTestRule.setContent {
- Text(myCounter.toString())
- lastSeenValue = myCounter
- }
- myCounter = 1 // The state changes, but there is no recomposition
-
- // Fails because nothing triggered a recomposition
- assertTrue(lastSeenValue == 1)
-
- // Passes because the assertion triggers recomposition
- composeTestRule.onNodeWithText("1").assertExists()
- }
-}
-
-private fun TestClockAdvanceSnippets() {
- composeTestRule.mainClock.autoAdvance = false
-
- composeTestRule.mainClock.advanceTimeByFrame()
- composeTestRule.mainClock.advanceTimeBy(milliseconds)
-}
-
-private fun IdlingResourceSnippet() {
- composeTestRule.registerIdlingResource(idlingResource)
- composeTestRule.unregisterIdlingResource(idlingResource)
-}
-
-private fun ManualSyncSnippet() {
- composeTestRule.mainClock.autoAdvance = true // default
- composeTestRule.waitForIdle() // Advances the clock until Compose is idle
-
- composeTestRule.mainClock.autoAdvance = false
- composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
-}
-
-private fun AdvanceWaitSnippets() {
- composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
-
- composeTestRule.waitUntil(timeoutMs) { condition }
-}
-
-private object ComponentActivitySnippet {
- class MyComposeTest {
-
- @get:Rule
- val composeTestRule = createAndroidComposeRule<ComponentActivity>()
-
- @Test
- fun myTest() {
- // Start the app
- composeTestRule.setContent {
- MyAppTheme {
- MainScreen(uiState = exampleUiState, /*...*/)
- }
- }
- val continueLabel = composeTestRule.activity.resources.getString(R.string.next)
- composeTestRule.onNodeWithText(continueLabel).performClick()
- }
- }
-}
-
-private object CreateSemanticsPropertySnippet {
- // Creates a Semantics property of type boolean
- val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
- var SemanticsPropertyReceiver.pickedDate by PickedDateKey
-}
-
-private fun UseSemanticsPropertySnippet() {
- composeTestRule
- .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
- .assertExists()
-}
-
-private object StateRestorationSnippet {
- @OptIn(ExperimentalTestApi::class)
- class MyStateRestorationTests {
-
- @get:Rule
- val composeTestRule = createComposeRule()
-
- @Test
- fun onRecreation_stateIsRestored() {
- val restorationTester = StateRestorationTester(composeTestRule)
-
- restorationTester.setContent { MainScreen() }
-
- // TODO: Run actions that modify the state
-
- // Trigger a recreation
- restorationTester.emulateSavedInstanceStateRestore()
-
- // TODO: Verify that state has been correctly restored.
- }
- }
-}
-
-private object InteropTestSnippet {
- @Test fun androidViewInteropTest() {
- // Check the initial state of a TextView that depends on a Compose state:
- Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
- // Click on the Compose button that changes the state
- composeTestRule.onNodeWithText("Click here").performClick()
- // Check the new value
- Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
- }
-}
-
-@ExperimentalComposeUiApi
-@Composable
-fun UiAutomatorInteropTestSnippet() {
- Scaffold(
- // Enables for all composables in the hierarchy.
- modifier = Modifier.semantics {
- testTagsAsResourceId = true
- }
- ) { padding ->
- // Modifier.testTag is accessible from UiAutomator for composables nested here.
- LazyColumn(
- modifier = Modifier
- .testTag("myLazyColumn")
- .padding(padding),
- ) {
- // content
- }
- }
-
- val device = UiDevice.getInstance(getInstrumentation())
- val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
- // some interaction with the lazyColumn
-}
-
-private object TestingSnippets13 {
- class MyTest() {
-
- private val themeIsDark = MutableStateFlow(false)
-
- @Before
- fun setUp() {
- composeTestRule.setContent {
- JetchatTheme(
- isDarkTheme = themeIsDark.collectAsState(false).value
- ) {
- MainScreen()
- }
- }
- }
-
- @Test fun changeTheme_scrollIsPersisted() {
- composeTestRule.onNodeWithContentDescription("Continue").performClick()
-
- // Set theme to dark
- themeIsDark.value = true
-
- // Check that we're still on the same page
- composeTestRule.onNodeWithContentDescription("Welcome").assertIsDisplayed()
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private val matcher = hasText("Button")
-private val text = ""
-private val composeTestRule = createAndroidComposeRule<MyActivity>()
-@Composable private fun MyButton(modifier: Modifier) {}
-@Composable private fun MyAppTheme(content: @Composable () -> Unit) {}
-@Composable private fun JetchatTheme(isDarkTheme: Boolean, content: @Composable () -> Unit) {}
-private val exampleUiState = Unit
-@Composable private fun MainScreen(uiState: Any = Unit) {}
-private class MyActivity : ComponentActivity()
-@Composable private fun MyButton(content: @Composable RowScope.() -> Unit) { }
-private lateinit var key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>
-private var keyEvent = KeyEvent(AndroidKeyEvent(ActionDown, KeyCodeA))
-private const val milliseconds = 10L
-private const val timeoutMs = 10L
-private val idlingResource = object : IdlingResource {
- override val isIdleNow: Boolean
- get() = TODO("Stub!")
-}
-private val condition = true
-private object R {
- object string {
- const val next = 1
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tooling/Tooling.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tooling/Tooling.kt
deleted file mode 100644
index c415400..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tooling/Tooling.kt
+++ /dev/null
@@ -1,200 +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.
-*/
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "LocalVariableName")
-
-package androidx.compose.integration.docs.tooling
-
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/tooling
- *
- * No action required if it's modified.
- */
-
-private object ToolingSnippet1 {
- @Composable
- fun SimpleComposable() {
- Text("Hello World")
- }
-}
-
-private class ToolingSnippet2 {
- @Preview
- @Composable
- fun ComposablePreview() {
- SimpleComposable()
- }
-}
-
-@Composable
-private fun ToolingSnippetLocalInspectionMode() {
- if (LocalInspectionMode.current) {
- // Show this text in a preview window:
- Text("Hello preview user!")
- } else {
- // Show this text in the app:
- Text("Hello $name!")
- }
-}
-
-private class ToolingSnippetMultipreviewDefinition {
- @Preview(
- name = "small font",
- group = "font scales",
- fontScale = 0.5f
- )
- @Preview(
- name = "large font",
- group = "font scales",
- fontScale = 1.5f
- )
- annotation class FontScalePreviews
-}
-
-private class ToolingSnippetMultipreviewUsage {
- @FontScalePreviews
- @Composable
- fun HelloWorldPreview() {
- Text("Hello World")
- }
-}
-
-private class ToolingSnippetMultipreviewCombine {
- @Preview(
- name = "dark theme",
- group = "themes",
- uiMode = UI_MODE_NIGHT_YES
- )
- @FontScalePreviews
- @DevicePreviews
- annotation class CompletePreviews
-
- @CompletePreviews
- @Composable
- fun HelloWorldPreview() {
- MyTheme { Surface { Text("Hello world") } }
- }
-}
-
-private class ToolingSnippet3 {
- @Preview(showBackground = true, backgroundColor = 0xFF00FF00)
- @Composable
- fun WithGreenBackground() {
- Text("Hello World")
- }
-}
-
-private class ToolingSnippet4 {
- @Preview(widthDp = 50, heightDp = 50)
- @Composable
- fun SquareComposablePreview() {
- Box(Modifier.background(Color.Yellow)) {
- Text("Hello World")
- }
- }
-}
-
-private class ToolingSnippet5 {
- @Preview(locale = "fr-rFR")
- @Composable
- fun DifferentLocaleComposablePreview() {
- Text(text = stringResource(R.string.greetings))
- }
-}
-
-private class ToolingSnippet6 {
- @Preview(showSystemUi = true)
- @Composable
- fun DecoratedComposablePreview() {
- Text("Hello World")
- }
-}
-
-private class ToolingSnippet7 {
- @Preview
- @Composable
- fun UserProfilePreview(
- @PreviewParameter(UserPreviewParameterProvider::class) user: User
- ) {
- UserProfile(user)
- }
-
- class UserPreviewParameterProvider : PreviewParameterProvider<User> {
- override val values = sequenceOf(
- User("Elise"),
- User("Frank"),
- User("Julia")
- )
- }
-}
-
-private class ToolingSnippet8 {
- @Preview
- @Composable
- fun UserProfilePreview(
- @PreviewParameter(UserPreviewParameterProvider::class, limit = 2) user: User
- ) {
- UserProfile(user)
- }
-}
-
-private fun SimpleComposable() {}
-private data class User(val name: String)
-
-@Composable
-private fun UserProfile(user: User) {
- Text(user.name)
-}
-
-private class UserPreviewParameterProvider : PreviewParameterProvider<User> {
- override val values = emptySequence<User>()
-}
-
-private enum class SurfaceState { Released, Pressed }
-
-private object R {
- object string {
- const val greetings = 1
- }
-}
-
-private annotation class FontScalePreviews
-private annotation class DevicePreviews
-
-@Composable
-private fun MyTheme(content: @Composable () -> Unit) {}
-
-/*
- * Fakes needed for snippets to build:
- */
-
-private val name = "friend"
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt
deleted file mode 100644
index 636f2d1..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * 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.compose.integration.docs.tutorial
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/tutorial
- *
- * No action required if it's modified.
- *
- * Tech writers: on DAC, these snippets contain html formatting that is omitted here.
- */
-
-private object TutorialSnippet1 {
- class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- Text("Hello world!")
- }
- }
- }
-}
-
-/*
-Page 2
- */
-
-private object TutorialSnippet2 {
- class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- Greeting("Android")
- }
- }
- }
-
- @Composable
- fun Greeting(name: String) {
- Text(text = "Hello $name!")
- }
-}
-
-/*
-Page 3
- */
-
-private object TutorialSnippet3 {
- @Composable
- fun Greeting(name: String) {
- Text(text = "Hello $name!")
- }
-
- @Preview
- @Composable
- fun PreviewGreeting() {
- Greeting("Android")
- }
-}
-
-/*
-Lesson 2
- */
-
-private object TutorialSnippet4 {
- class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- NewsStory()
- }
- }
- }
-
- @Composable
- fun NewsStory() {
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
-
- @Preview
- @Composable
- fun DefaultPreview() {
- NewsStory()
- }
-}
-
-/*
-Page 2
- */
-
-private object TutorialSnippet5 {
- @Composable
- fun NewsStory() {
- Column {
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
-}
-
-private object TutorialSnippet6 {
- @Composable
- fun NewsStory() {
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
-}
-
-private object TutorialSnippet7 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
-
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- Image(image, contentDescription = null)
-
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
-}
-
-private object TutorialSnippet8 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
-
- Image(
- image,
- contentDescription = null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
-
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
-}
-
-/*
-Lesson 3
- */
-private object TutorialSnippet9 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
- .clip(shape = RoundedCornerShape(4.dp))
-
- Image(
- image,
- contentDescription = null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
- Spacer(Modifier.height(16.dp))
-
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
-}
-
-private object TutorialSnippet10 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- MaterialTheme {
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
- .clip(shape = RoundedCornerShape(4.dp))
-
- Image(
- image,
- contentDescription = null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
- Spacer(Modifier.height(16.dp))
-
- Text("A day in Shark Fin Cove")
- Text("Davenport, California")
- Text("December 2018")
- }
- }
- }
-}
-
-private object TutorialSnippet11 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- MaterialTheme {
- val typography = MaterialTheme.typography
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
- .clip(shape = RoundedCornerShape(4.dp))
-
- Image(
- image,
- contentDescription = null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
- Spacer(Modifier.height(16.dp))
-
- Text("A day in Shark Fin Cove",
- style = typography.h6)
- Text("Davenport, California",
- style = typography.body2)
- Text("December 2018",
- style = typography.body2)
- }
- }
- }
-}
-
-private object TutorialSnippet12 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- MaterialTheme {
- val typography = MaterialTheme.typography
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
- .clip(shape = RoundedCornerShape(4.dp))
-
- Image(
- image,
- contentDescription = null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
- Spacer(Modifier.height(16.dp))
-
- Text(
- "A day wandering through the sandhills " +
- "in Shark Fin Cove, and a few of the " +
- "sights I saw",
- style = typography.h6)
- Text("Davenport, California",
- style = typography.body2)
- Text("December 2018",
- style = typography.body2)
- }
- }
- }
-}
-
-/* ktlint-disable indent */
-private object TutorialSnippet13 {
- @Composable
- fun NewsStory() {
- val image = painterResource(R.drawable.header)
- MaterialTheme {
- val typography = MaterialTheme.typography
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- val imageModifier = Modifier
- .height(180.dp)
- .fillMaxWidth()
- .clip(shape = RoundedCornerShape(4.dp))
-
- Image(
- image, null,
- modifier = imageModifier,
- contentScale = ContentScale.Crop
- )
- Spacer(Modifier.height(16.dp))
-
- Text(
- "A day wandering through the sandhills " +
- "in Shark Fin Cove, and a few of the " +
- "sights I saw",
- style = typography.h6,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis)
- Text("Davenport, California",
- style = typography.body2)
- Text("December 2018",
- style = typography.body2)
- }
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private object R {
- object drawable {
- const val header = 1
- }
-}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/tutorial/Tutorial.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/tutorial/Tutorial.kt
deleted file mode 100644
index b17b824..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/tutorial/Tutorial.kt
+++ /dev/null
@@ -1,495 +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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "UNUSED_PARAMETER")
-
-package androidx.compose.integration.tutorial
-
-import android.content.res.Configuration
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.animateContentSize
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.integration.tutorial.Lesson2_Layouts.Snippet1.Message
-import androidx.compose.integration.tutorial.Lesson2_Layouts.Snippet4.MessageCard
-import androidx.compose.integration.tutorial.Lesson4_ListsAnimations.Snippet1.Conversation
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/tutorial
- *
- * No action required if it's modified.
- */
-
-private object Lesson1_ComposableFunctions {
- object Snippet1 {
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- Text("Hello world!")
- }
- }
- }
- }
- object Snippet2 {
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MessageCard("Android")
- }
- }
- }
-
- @Composable
- fun MessageCard(name: String) {
- Text(text = "Hello $name!")
- }
- }
-
- object Snippet3 {
- @Composable
- fun MessageCard(name: String) {
- Text(text = "Hello $name!")
- }
-
- @Preview
- @Composable
- fun PreviewMessageCard() {
- MessageCard("Android")
- }
- }
-}
-
-private object Lesson2_Layouts {
- object Snippet1 {
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MessageCard(Message("Android", "Jetpack Compose"))
- }
- }
- }
-
- data class Message(val author: String, val body: String)
-
- @Composable
- fun MessageCard(msg: Message) {
- Text(text = msg.author)
- Text(text = msg.body)
- }
-
- @Preview
- @Composable
- fun PreviewMessageCard() {
- MessageCard(
- msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
- )
- }
- }
-
- object Snippet2 {
- @Composable
- fun MessageCard(msg: Message) {
- Column {
- Text(text = msg.author)
- Text(text = msg.body)
- }
- }
- }
-
- object Snippet3 {
- @Composable
- fun MessageCard(msg: Message) {
- Row {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = "Contact profile picture",
- )
-
- Column {
- Text(text = msg.author)
- Text(text = msg.body)
- }
- }
- }
- }
-
- object Snippet4 {
- @Composable
- fun MessageCard(msg: Message) {
- // Add padding around our message
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = "Contact profile picture",
- modifier = Modifier
- // Set image size to 40 dp
- .size(40.dp)
- // Clip image to be shaped as a circle
- .clip(CircleShape)
- )
-
- // Add a horizontal space between the image and the column
- Spacer(modifier = Modifier.width(8.dp))
-
- Column {
- Text(text = msg.author)
- // Add a vertical space between the author and message texts
- Spacer(modifier = Modifier.height(4.dp))
- Text(text = msg.body)
- }
- }
- }
- }
-}
-
-private object Lesson3_MaterialDesign {
- object Snippet1 {
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- ComposeTutorialTheme {
- MessageCard(Message("Android", "Jetpack Compose"))
- }
- }
- }
- }
-
- @Preview
- @Composable
- fun PreviewMessageCard() {
- ComposeTutorialTheme {
- MessageCard(
- msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
- )
- }
- }
- }
-
- object Snippet2 {
- @Composable
- fun MessageCard(msg: Message) {
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = null,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Column {
- Text(
- text = msg.author,
- color = MaterialTheme.colors.secondaryVariant
- )
-
- Spacer(modifier = Modifier.height(4.dp))
- Text(text = msg.body)
- }
- }
- }
- }
-
- object Snippet3 {
- @Composable
- fun MessageCard(msg: Message) {
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = null,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
- )
- Spacer(modifier = Modifier.width(8.dp))
-
- Column {
- Text(
- text = msg.author,
- color = MaterialTheme.colors.secondaryVariant,
- style = MaterialTheme.typography.subtitle2
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- Text(
- text = msg.body,
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
- }
-
- object Snippet4 {
- @Composable
- fun MessageCard(msg: Message) {
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = null,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
- )
- Spacer(modifier = Modifier.width(8.dp))
-
- Column {
- Text(
- text = msg.author,
- color = MaterialTheme.colors.secondaryVariant,
- style = MaterialTheme.typography.subtitle2
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
- Text(
- text = msg.body,
- modifier = Modifier.padding(all = 4.dp),
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
- }
-
- object Snippet5 {
- @Preview(name = "Light Mode")
- @Preview(
- uiMode = Configuration.UI_MODE_NIGHT_YES,
- showBackground = true,
- name = "Dark Mode"
- )
- @Composable
- fun PreviewMessageCard() {
- ComposeTutorialTheme {
- MessageCard(
- msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!")
- )
- }
- }
- }
- }
-}
-
-private object Lesson4_ListsAnimations {
- object Snippet1 {
- // import androidx.compose.foundation.lazy.items
-
- @Composable
- fun Conversation(messages: List<Message>) {
- LazyColumn {
- items(messages) { message ->
- MessageCard(message)
- }
- }
- }
-
- @Preview
- @Composable
- fun PreviewConversation() {
- ComposeTutorialTheme {
- val messages = List(15) {
- Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
- }
-
- Conversation(messages)
- }
- }
- }
-
- object Snippet2 {
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- ComposeTutorialTheme {
- val messages = List(15) {
- Message(
- "Colleague",
- "Hey, take a look at Jetpack Compose, it's great!\n" +
- "It's the Android's modern toolkit for building native UI." +
- "It simplifies and accelerates UI development on Android." +
- "Quickly bring your app to life with less code, powerful " +
- "tools, and intuitive Kotlin APIs"
- )
- }
-
- Conversation(messages)
- }
- }
- }
- }
-
- @Composable
- fun MessageCard(msg: Message) {
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = null,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
- )
- Spacer(modifier = Modifier.width(8.dp))
-
- // We keep track if the message is expanded or not in this
- // variable
- var isExpanded by remember { mutableStateOf(false) }
-
- // We toggle the isExpanded variable when we click on this Column
- Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
- Text(
- text = msg.author,
- color = MaterialTheme.colors.secondaryVariant,
- style = MaterialTheme.typography.subtitle2
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- Surface(
- shape = MaterialTheme.shapes.medium,
- elevation = 1.dp,
- ) {
- Text(
- text = msg.body,
- modifier = Modifier.padding(all = 4.dp),
- // If the message is expanded, we display all its content
- // otherwise we only display the first line
- maxLines = if (isExpanded) Int.MAX_VALUE else 1,
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
- }
- }
-
- object Snippet4 {
- @Composable
- fun MessageCard(msg: Message) {
- Row(modifier = Modifier.padding(all = 8.dp)) {
- Image(
- painter = painterResource(R.drawable.profile_picture),
- contentDescription = null,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
- )
- Spacer(modifier = Modifier.width(8.dp))
-
- // We keep track if the message is expanded or not in this
- // variable
- var isExpanded by remember { mutableStateOf(false) }
- // surfaceColor will be updated gradually from one color to the other
- val surfaceColor: Color by animateColorAsState(
- if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
- )
-
- // We toggle the isExpanded variable when we click on this Column
- Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
- Text(
- text = msg.author,
- color = MaterialTheme.colors.secondaryVariant,
- style = MaterialTheme.typography.subtitle2
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- Surface(
- shape = MaterialTheme.shapes.medium,
- elevation = 1.dp,
- // surfaceColor color will be changing gradually from primary to surface
- color = surfaceColor,
- // animateContentSize will change the Surface size gradually
- modifier = Modifier.animateContentSize().padding(1.dp)
- ) {
- Text(
- text = msg.body,
- modifier = Modifier.padding(all = 4.dp),
- // If the message is expanded, we display all its content
- // otherwise we only display the first line
- maxLines = if (isExpanded) Int.MAX_VALUE else 1,
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
- }
- }
-}
-
-// ========================
-// Fakes below
-// ========================
-
-@Composable
-private fun ComposeTutorialTheme(content: @Composable () -> Unit) = MaterialTheme(content = content)
-
-private object R {
- object drawable {
- const val profile_picture = 1
- }
-}
-
-@Repeatable
-@Retention(AnnotationRetention.SOURCE)
-private annotation class Preview(
- val name: String = "",
- val uiMode: Int = 0,
- val showBackground: Boolean = true
-)
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/PrimitiveInCollectionDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/PrimitiveInCollectionDetector.kt
index ec98210..945f354 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/PrimitiveInCollectionDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/PrimitiveInCollectionDetector.kt
@@ -34,6 +34,7 @@
import com.intellij.psi.PsiWildcardType
import com.intellij.psi.impl.source.PsiClassReferenceType
import java.util.EnumSet
+import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UField
@@ -79,6 +80,22 @@
}
override fun visitVariable(node: UVariable) {
+ // Kotlin destructuring expression is desugared. E.g.,
+ //
+ // val (x, y) = pair
+ //
+ // is mapped to
+ //
+ // val varHash = pair // temp variable
+ // val x = varHash.component1()
+ // val y = varHash.component2()
+ //
+ // and thus we don't need to analyze the temporary variable.
+ // Their `sourcePsi`s are different:
+ // KtDestructuringDeclaration (for overall expression) v.s.
+ // KtDestructuringDeclarationEntry (for individual local variables)
+ if (node.sourcePsi is KtDestructuringDeclaration) return
+
val primitiveCollection = node.type.primitiveCollectionReplacement(context) ?: return
if (node.isLambdaParameter()) {
// Don't notify for lambda parameters. We'll be notifying for the method
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/PrimitiveInCollectionDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/PrimitiveInCollectionDetectorTest.kt
index 9dc2cee..b14f040 100644
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/PrimitiveInCollectionDetectorTest.kt
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/PrimitiveInCollectionDetectorTest.kt
@@ -414,6 +414,26 @@
)
}
+ @Test
+ fun hiddenVariableInDeconstruction() {
+ // Regression test from b/328122546
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.lint
+
+ fun foo(value: Any) {
+ val list = value as List<Any>
+ val (first, second, third) = (list as List<$type>)
+ println(first)
+ println(second)
+ println(third)
+ }
+ """
+ )
+ ).run().expectClean()
+ }
+
data class Parameters(
val type: String,
val value: String,
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index ee16cb3..e06e787 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -69,6 +69,7 @@
androidMain {
dependsOn(jvmMain)
dependencies {
+ implementation(project(":compose:ui:ui-graphics"))
}
}
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
index 30c28cb..7bd6362 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
@@ -18,7 +18,7 @@
import android.content.Context
import android.view.ViewGroup
-import androidx.compose.ui.R
+import androidx.compose.ui.graphics.R
internal interface RippleHostKey {
/**
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
index 685ef98..04b2af4 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
@@ -51,10 +51,10 @@
Text("Surfaces", style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
SurfaceColorSwatch(
- surface = colorScheme.surface,
- surfaceText = "Surface",
- onSurface = colorScheme.onSurface,
- onSurfaceText = "On Surface"
+ color1 = colorScheme.surface,
+ color1Text = "Surface",
+ color2 = colorScheme.onSurface,
+ color2Text = "On Surface"
)
Spacer(modifier = Modifier.height(16.dp))
DoubleTile(
@@ -85,42 +85,26 @@
)
},
)
- Text("Surface Container Variants", style = MaterialTheme.typography.bodyLarge)
+ Text("Surface Containers", style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
- DoubleTile(
- leftTile = {
- ColorTile(
- text = "High Emphasis",
- color = colorScheme.surfaceContainerHigh,
- )
- },
- rightTile = {
- ColorTile(
- text = "Highest Emphasis",
- color = colorScheme.surfaceContainerHighest,
- )
- },
+ SurfaceColorSwatch(
+ color1 = colorScheme.surfaceContainerHigh,
+ color1Text = "Surface Container High",
+ color2 = colorScheme.surfaceContainerHighest,
+ color2Text = "Surface Container Highest"
)
- DoubleTile(
- leftTile = {
- ColorTile(
- text = "Low Emphasis",
- color = colorScheme.surfaceContainerLow,
- )
- },
- rightTile = {
- ColorTile(
- text = "Lowest Emphasis",
- color = colorScheme.surfaceContainerLowest,
- )
- },
+ SurfaceColorSwatch(
+ color1 = colorScheme.surfaceContainerLow,
+ color1Text = "Surface Container Low",
+ color2 = colorScheme.surfaceContainerLowest,
+ color2Text = "Surface Container Lowest"
)
Spacer(modifier = Modifier.height(16.dp))
SurfaceColorSwatch(
- surface = colorScheme.surfaceVariant,
- surfaceText = "Surface Variant",
- onSurface = colorScheme.onSurfaceVariant,
- onSurfaceText = "On Surface Variant"
+ color1 = colorScheme.surfaceVariant,
+ color1Text = "Surface Variant",
+ color2 = colorScheme.onSurfaceVariant,
+ color2Text = "On Surface Variant"
)
Spacer(modifier = Modifier.height(16.dp))
DoubleTile(
@@ -220,18 +204,18 @@
@Composable
private fun SurfaceColorSwatch(
- surface: Color,
- surfaceText: String,
- onSurface: Color,
- onSurfaceText: String
+ color1: Color,
+ color1Text: String,
+ color2: Color,
+ color2Text: String
) {
ColorTile(
- text = surfaceText,
- color = surface,
+ text = color1Text,
+ color = color1,
)
ColorTile(
- text = onSurfaceText,
- color = onSurface,
+ text = color2Text,
+ color = color2,
)
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index 9875e1b..03d0b44 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -31,8 +31,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
-import androidx.compose.material3.tokens.CircularProgressIndicatorTokens
-import androidx.compose.material3.tokens.LinearProgressIndicatorTokens
+import androidx.compose.material3.tokens.ProgressIndicatorTokens
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -856,16 +855,16 @@
/** Default color for a linear progress indicator. */
val linearColor: Color
@Composable get() =
- LinearProgressIndicatorTokens.ActiveIndicatorColor.value
+ ProgressIndicatorTokens.ActiveIndicatorColor.value
/** Default color for a circular progress indicator. */
val circularColor: Color
@Composable get() =
- CircularProgressIndicatorTokens.ActiveIndicatorColor.value
+ ProgressIndicatorTokens.ActiveIndicatorColor.value
/** Default track color for a linear progress indicator. */
val linearTrackColor: Color
- @Composable get() = LinearProgressIndicatorTokens.TrackColor.value
+ @Composable get() = ProgressIndicatorTokens.TrackColor.value
/** Default track color for a circular progress indicator. */
@Deprecated(
@@ -878,14 +877,14 @@
/** Default track color for a circular determinate progress indicator. */
val circularDeterminateTrackColor: Color
- @Composable get() = LinearProgressIndicatorTokens.TrackColor.value
+ @Composable get() = ProgressIndicatorTokens.TrackColor.value
/** Default track color for a circular indeterminate progress indicator. */
val circularIndeterminateTrackColor: Color
@Composable get() = Color.Transparent
/** Default stroke width for a circular progress indicator. */
- val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.ActiveIndicatorWidth
+ val CircularStrokeWidth: Dp = ProgressIndicatorTokens.TrackThickness
/** Default stroke cap for a linear progress indicator. */
val LinearStrokeCap: StrokeCap = StrokeCap.Round
@@ -900,19 +899,19 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val LinearTrackStopIndicatorSize: Dp = 4.dp
+ val LinearTrackStopIndicatorSize: Dp = ProgressIndicatorTokens.StopSize
/** Default indicator track gap size for a linear progress indicator. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val LinearIndicatorTrackGapSize: Dp = 4.dp
+ val LinearIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
/** Default indicator track gap size for a circular progress indicator. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val CircularIndicatorTrackGapSize: Dp = 4.dp
+ val CircularIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
/**
* The default [AnimationSpec] that should be used when animating between progress in a
@@ -934,13 +933,13 @@
internal val LinearIndicatorWidth = 240.dp
/*@VisibleForTesting*/
-internal val LinearIndicatorHeight = LinearProgressIndicatorTokens.TrackHeight
+internal val LinearIndicatorHeight = ProgressIndicatorTokens.TrackThickness
// CircularProgressIndicator Material specs
// Diameter of the indicator circle
/*@VisibleForTesting*/
internal val CircularIndicatorDiameter =
- CircularProgressIndicatorTokens.Size - CircularProgressIndicatorTokens.ActiveIndicatorWidth * 2
+ ProgressIndicatorTokens.Size - ProgressIndicatorTokens.TrackThickness * 2
// Indeterminate linear indicator transition specs
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index cffb68d..935dc9e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -949,22 +949,20 @@
return defaultSliderColorsCached ?: SliderColors(
thumbColor = fromToken(SliderTokens.HandleColor),
activeTrackColor = fromToken(SliderTokens.ActiveTrackColor),
- activeTickColor = fromToken(SliderTokens.TickMarksActiveContainerColor)
- .copy(alpha = SliderTokens.TickMarksActiveContainerOpacity),
+ activeTickColor = fromToken(SliderTokens.InactiveTrackColor),
inactiveTrackColor = fromToken(SliderTokens.InactiveTrackColor),
- inactiveTickColor = fromToken(SliderTokens.TickMarksInactiveContainerColor)
- .copy(alpha = SliderTokens.TickMarksInactiveContainerOpacity),
+ inactiveTickColor = fromToken(SliderTokens.ActiveTrackColor),
disabledThumbColor = fromToken(SliderTokens.DisabledHandleColor)
.copy(alpha = SliderTokens.DisabledHandleOpacity)
.compositeOver(surface),
disabledActiveTrackColor = fromToken(SliderTokens.DisabledActiveTrackColor)
.copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
- disabledActiveTickColor = fromToken(SliderTokens.TickMarksDisabledContainerColor)
- .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity),
+ disabledActiveTickColor = fromToken(SliderTokens.DisabledInactiveTrackColor)
+ .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
disabledInactiveTrackColor = fromToken(SliderTokens.DisabledInactiveTrackColor)
.copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
- disabledInactiveTickColor = fromToken(SliderTokens.TickMarksDisabledContainerColor)
- .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity)
+ disabledInactiveTickColor = fromToken(SliderTokens.DisabledActiveTrackColor)
+ .copy(alpha = SliderTokens.DisabledActiveTrackOpacity)
).also {
defaultSliderColorsCached = it
}
@@ -1907,10 +1905,10 @@
internal val ThumbWidth = SliderTokens.HandleWidth
private val ThumbHeight = SliderTokens.HandleHeight
private val ThumbSize = DpSize(ThumbWidth, ThumbHeight)
-private val TickSize = SliderTokens.TickMarksContainerSize
-private val ThumbTrackGapSize: Dp = 6.dp
+private val TickSize: Dp = 2.dp
+private val ThumbTrackGapSize: Dp = SliderTokens.ActiveHandleLeadingSpace
private val TrackInsideCornerSize: Dp = 2.dp
-private val TrackStopIndicatorSize: Dp = 4.dp
+private val TrackStopIndicatorSize: Dp = SliderTokens.StopIndicatorSize
private const val SliderRangeTolerance = 0.0001
private enum class SliderComponents {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
deleted file mode 100644
index ae7c45b..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
+++ /dev/null
@@ -1,34 +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.
- */
-// VERSION: v0_103
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-package androidx.compose.material3.tokens
-
-import androidx.compose.ui.unit.dp
-
-internal object LinearProgressIndicatorTokens {
- val ActiveIndicatorColor = ColorSchemeKeyTokens.Primary
- val ActiveIndicatorHeight = 4.0.dp
- val ActiveShape = ShapeKeyTokens.CornerNone
- val FourColorActiveIndicatorFourColor = ColorSchemeKeyTokens.TertiaryContainer
- val FourColorActiveIndicatorOneColor = ColorSchemeKeyTokens.Primary
- val FourColorActiveIndicatorThreeColor = ColorSchemeKeyTokens.Tertiary
- val FourColorActiveIndicatorTwoColor = ColorSchemeKeyTokens.PrimaryContainer
- val TrackColor = ColorSchemeKeyTokens.PrimaryContainer // TODO(b/321712387): Update tokens
- val TrackHeight = 4.0.dp
- val TrackShape = ShapeKeyTokens.CornerNone
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PaletteTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PaletteTokens.kt
index b4e5e3b..d802980c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PaletteTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PaletteTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_162
+// VERSION: v0_210
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -36,29 +36,29 @@
val Error95 = Color(red = 252, green = 238, blue = 238)
val Error99 = Color(red = 255, green = 251, blue = 249)
val Neutral0 = Color(red = 0, green = 0, blue = 0)
- val Neutral10 = Color(red = 28, green = 27, blue = 31)
+ val Neutral10 = Color(red = 29, green = 27, blue = 32)
val Neutral100 = Color(red = 255, green = 255, blue = 255)
- val Neutral12 = Color(red = 32, green = 31, blue = 35)
- val Neutral17 = Color(red = 43, green = 41, blue = 45)
- val Neutral20 = Color(red = 49, green = 48, blue = 51)
- val Neutral22 = Color(red = 49, green = 48, blue = 51)
- val Neutral24 = Color(red = 49, green = 48, blue = 51)
- val Neutral30 = Color(red = 72, green = 70, blue = 73)
- val Neutral4 = Color(red = 14, green = 14, blue = 17)
- val Neutral40 = Color(red = 96, green = 93, blue = 98)
- val Neutral50 = Color(red = 120, green = 117, blue = 121)
- val Neutral6 = Color(red = 20, green = 19, blue = 23)
- val Neutral60 = Color(red = 147, green = 144, blue = 148)
- val Neutral70 = Color(red = 174, green = 170, blue = 174)
- val Neutral80 = Color(red = 201, green = 197, blue = 202)
- val Neutral87 = Color(red = 221, green = 216, blue = 221)
- val Neutral90 = Color(red = 230, green = 225, blue = 229)
- val Neutral92 = Color(red = 236, green = 231, blue = 236)
- val Neutral94 = Color(red = 241, green = 236, blue = 241)
- val Neutral95 = Color(red = 244, green = 239, blue = 244)
- val Neutral96 = Color(red = 247, green = 242, blue = 247)
- val Neutral98 = Color(red = 253, green = 248, blue = 253)
- val Neutral99 = Color(red = 255, green = 251, blue = 254)
+ val Neutral12 = Color(red = 33, green = 31, blue = 38)
+ val Neutral17 = Color(red = 43, green = 41, blue = 48)
+ val Neutral20 = Color(red = 50, green = 47, blue = 53)
+ val Neutral22 = Color(red = 54, green = 52, blue = 59)
+ val Neutral24 = Color(red = 59, green = 56, blue = 62)
+ val Neutral30 = Color(red = 72, green = 70, blue = 76)
+ val Neutral4 = Color(red = 15, green = 13, blue = 19)
+ val Neutral40 = Color(red = 96, green = 93, blue = 100)
+ val Neutral50 = Color(red = 121, green = 118, blue = 125)
+ val Neutral6 = Color(red = 20, green = 18, blue = 24)
+ val Neutral60 = Color(red = 147, green = 143, blue = 150)
+ val Neutral70 = Color(red = 174, green = 169, blue = 177)
+ val Neutral80 = Color(red = 202, green = 197, blue = 205)
+ val Neutral87 = Color(red = 222, green = 216, blue = 225)
+ val Neutral90 = Color(red = 230, green = 224, blue = 233)
+ val Neutral92 = Color(red = 236, green = 230, blue = 240)
+ val Neutral94 = Color(red = 243, green = 237, blue = 247)
+ val Neutral95 = Color(red = 245, green = 239, blue = 247)
+ val Neutral96 = Color(red = 247, green = 242, blue = 250)
+ val Neutral98 = Color(red = 254, green = 247, blue = 255)
+ val Neutral99 = Color(red = 255, green = 251, blue = 255)
val NeutralVariant0 = Color(red = 0, green = 0, blue = 0)
val NeutralVariant10 = Color(red = 29, green = 26, blue = 34)
val NeutralVariant100 = Color(red = 255, green = 255, blue = 255)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
similarity index 60%
rename from compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
rename to compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
index 26701dc..ade192d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,20 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_103
+// Version: v2_3_5
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
import androidx.compose.ui.unit.dp
-internal object CircularProgressIndicatorTokens {
+internal object ProgressIndicatorTokens {
val ActiveIndicatorColor = ColorSchemeKeyTokens.Primary
- val ActiveShape = ShapeKeyTokens.CornerNone
- val ActiveIndicatorWidth = 4.0.dp
- val FourColorActiveIndicatorFourColor = ColorSchemeKeyTokens.TertiaryContainer
- val FourColorActiveIndicatorOneColor = ColorSchemeKeyTokens.Primary
- val FourColorActiveIndicatorThreeColor = ColorSchemeKeyTokens.Tertiary
- val FourColorActiveIndicatorTwoColor = ColorSchemeKeyTokens.PrimaryContainer
+ val ActiveShape = ShapeKeyTokens.CornerFull
+ val ActiveThickness = 4.0.dp
+ val ActiveTrackSpace = 4.0.dp
+ val StopColor = ColorSchemeKeyTokens.Primary
+ val StopShape = 4.0.dp
+ val StopSize = 4.0.dp
+ val TrackColor = ColorSchemeKeyTokens.SecondaryContainer
+ val TrackShape = ShapeKeyTokens.CornerFull
+ val TrackThickness = 4.0.dp
val Size = 48.0.dp
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SliderTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SliderTokens.kt
index d9ecc67..607a2e8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SliderTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SliderTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_117
+// Version: v2_3_5
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -21,42 +21,55 @@
import androidx.compose.ui.unit.dp
internal object SliderTokens {
+ val ActiveContainerOpacity = 1.0f
+ val ActiveHandleHeight = 44.0.dp
+ val ActiveHandleLeadingSpace = 6.0.dp
+ val ActiveHandlePadding = 6.0.dp
+ val ActiveHandleShape = ShapeKeyTokens.CornerFull
+ val ActiveHandleTrailingSpace = 6.0.dp
+ val ActiveHandleWidth = 4.0.dp
val ActiveTrackColor = ColorSchemeKeyTokens.Primary
- val ActiveTrackHeight = 4.0.dp
+ val ActiveTrackHeight = 16.0.dp
val ActiveTrackShape = ShapeKeyTokens.CornerFull
+ val ActiveTrackShapeLeading = ShapeKeyTokens.CornerFull
val DisabledActiveTrackColor = ColorSchemeKeyTokens.OnSurface
- const val DisabledActiveTrackOpacity = 0.38f
+ val DisabledActiveTrackOpacity = 0.38f
val DisabledHandleColor = ColorSchemeKeyTokens.OnSurface
- val DisabledHandleElevation = ElevationTokens.Level0
- const val DisabledHandleOpacity = 0.38f
+ val DisabledHandleOpacity = 0.38f
+ val DisabledHandleWidth = 4.0.dp
val DisabledInactiveTrackColor = ColorSchemeKeyTokens.OnSurface
- const val DisabledInactiveTrackOpacity = 0.12f
- val FocusHandleColor = ColorSchemeKeyTokens.Primary
+ val DisabledInactiveTrackOpacity = 0.12f
+ val DisabledStopColor = ColorSchemeKeyTokens.OnSurface
+ val FocusActiveTrackColor = ColorSchemeKeyTokens.Primary
+ val FocusHandleWidth = 2.0.dp
+ val FocusInactiveTrackColor = ColorSchemeKeyTokens.SecondaryContainer
+ val FocusStopColor = ColorSchemeKeyTokens.Primary
val HandleColor = ColorSchemeKeyTokens.Primary
- val HandleElevation = ElevationTokens.Level1
val HandleHeight = 44.0.dp
val HandleShape = ShapeKeyTokens.CornerFull
val HandleWidth = 4.0.dp
val HoverHandleColor = ColorSchemeKeyTokens.Primary
- val InactiveTrackColor = ColorSchemeKeyTokens.SurfaceVariant
+ val HoverHandleWidth = 4.0.dp
+ val HoverStopColor = ColorSchemeKeyTokens.Primary
+ val InactiveContainerOpacity = 1.0f
+ val InactiveTrackColor = ColorSchemeKeyTokens.SecondaryContainer
val InactiveTrackHeight = 16.0.dp
val InactiveTrackShape = ShapeKeyTokens.CornerFull
val LabelContainerColor = ColorSchemeKeyTokens.Primary
- val LabelContainerElevation = ElevationTokens.Level0
- val LabelContainerHeight = 28.0.dp
- val LabelTextColor = ColorSchemeKeyTokens.OnPrimary
- val LabelTextFont = TypographyKeyTokens.LabelMedium
+ val LabelTextColor = ColorSchemeKeyTokens.InverseOnSurface
+ val PressedActiveTrackColor = ColorSchemeKeyTokens.Primary
val PressedHandleColor = ColorSchemeKeyTokens.Primary
- val StateLayerSize = 40.0.dp
- val TrackElevation = ElevationTokens.Level0
- val OverlapHandleOutlineColor = ColorSchemeKeyTokens.OnPrimary
- val OverlapHandleOutlineWidth = 1.0.dp
- val TickMarksActiveContainerColor = ColorSchemeKeyTokens.OnPrimary
- const val TickMarksActiveContainerOpacity = 0.38f
- val TickMarksContainerShape = ShapeKeyTokens.CornerFull
- val TickMarksContainerSize = 2.0.dp
- val TickMarksDisabledContainerColor = ColorSchemeKeyTokens.OnSurface
- const val TickMarksDisabledContainerOpacity = 0.38f
- val TickMarksInactiveContainerColor = ColorSchemeKeyTokens.OnSurfaceVariant
- const val TickMarksInactiveContainerOpacity = 0.38f
+ val PressedHandleWidth = 2.0.dp
+ val PressedInactiveTrackColor = ColorSchemeKeyTokens.SecondaryContainer
+ val PressedStopColor = ColorSchemeKeyTokens.Primary
+ val SliderActiveHandleColor = ColorSchemeKeyTokens.Primary
+ val StopIndicatorColor = ColorSchemeKeyTokens.SecondaryContainer
+ val StopIndicatorColorSelected = ColorSchemeKeyTokens.SecondaryContainer
+ val StopIndicatorShape = ShapeKeyTokens.CornerFull
+ val StopIndicatorSize = 4.0.dp
+ val StopIndicatorTrailingSpace = 6.0.dp
+ val ValueIndicatorActiveBottomSpace = 12.0.dp
+ val ValueIndicatorContainerColor = ColorSchemeKeyTokens.InverseSurface
+ val ValueIndicatorLabelTextColor = ColorSchemeKeyTokens.InverseOnSurface
+ val ValueIndicatorLabelTextFont = TypographyKeyTokens.LabelLarge
}
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index a4ace77..fe7f66b 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -716,7 +716,7 @@
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(optional int capacity);
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(int size, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends T> init);
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf();
- method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T...? elements);
+ method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T... elements);
}
}
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 339eb1e..63348ec 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -751,7 +751,7 @@
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(optional int capacity);
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(int size, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends T> init);
method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf();
- method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T...? elements);
+ method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T... elements);
}
}
diff --git a/compose/ui/ui-android-stubs/api/current.txt b/compose/ui/ui-android-stubs/api/current.txt
index 8e22228..b802776 100644
--- a/compose/ui/ui-android-stubs/api/current.txt
+++ b/compose/ui/ui-android-stubs/api/current.txt
@@ -6,6 +6,11 @@
method public void drawRenderNode(android.view.RenderNode);
}
+ public abstract class HardwareCanvas extends android.graphics.Canvas {
+ ctor public HardwareCanvas();
+ method public abstract int drawRenderNode(android.view.RenderNode, android.graphics.Rect, int);
+ }
+
public class RenderNode {
method public static android.view.RenderNode create(String?, android.view.View?);
method public void destroy();
diff --git a/compose/ui/ui-android-stubs/api/restricted_current.txt b/compose/ui/ui-android-stubs/api/restricted_current.txt
index 8e22228..b802776 100644
--- a/compose/ui/ui-android-stubs/api/restricted_current.txt
+++ b/compose/ui/ui-android-stubs/api/restricted_current.txt
@@ -6,6 +6,11 @@
method public void drawRenderNode(android.view.RenderNode);
}
+ public abstract class HardwareCanvas extends android.graphics.Canvas {
+ ctor public HardwareCanvas();
+ method public abstract int drawRenderNode(android.view.RenderNode, android.graphics.Rect, int);
+ }
+
public class RenderNode {
method public static android.view.RenderNode create(String?, android.view.View?);
method public void destroy();
diff --git a/compose/ui/ui-android-stubs/src/main/java/android/view/HardwareCanvas.java b/compose/ui/ui-android-stubs/src/main/java/android/view/HardwareCanvas.java
new file mode 100644
index 0000000..fae0b74
--- /dev/null
+++ b/compose/ui/ui-android-stubs/src/main/java/android/view/HardwareCanvas.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Stub for HardwareCanvas on Android L
+ */
+public abstract class HardwareCanvas extends Canvas {
+
+ /**
+ * Draws the specified display list onto this canvas.
+ *
+ * @param renderNode The RenderNode to replay.
+ * @param dirty Ignored, can be null.
+ * @param flags Optional flags about drawing, see {@link RenderNode} for
+ * the possible flags.
+ *
+ * @return One of {@link RenderNode#STATUS_DONE} or {@link RenderNode#STATUS_DREW}
+ * if anything was drawn.
+ */
+ public abstract int drawRenderNode(
+ @NonNull RenderNode renderNode,
+ @NonNull Rect dirty,
+ int flags
+ );
+
+ @Override
+ public void enableZ() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void disableZ() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 56a2d27..9e38779 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -73,6 +73,9 @@
androidMain {
dependsOn(jvmMain)
dependencies {
+ // This has stub APIs for access to legacy Android APIs, so we don't want
+ // any dependency on this module.
+ compileOnly(project(":compose:ui:ui-android-stubs"))
implementation("androidx.graphics:graphics-path:1.0.0-beta02")
api("androidx.annotation:annotation-experimental:1.4.0")
}
diff --git a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
index db4ac6b..9e3fa34 100644
--- a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
+++ b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
@@ -62,9 +62,8 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-// Temporarily restrict the minSdkVersion to Android Q as the minimum API requirement will
-// be reduced in subsequent CLs
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+// Relies on View.captureToImage which is Android O+ only
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class AndroidGraphicsLayerTest {
companion object {
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt
deleted file mode 100644
index 5858e3d..0000000
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.graphics
-
-import android.os.Build
-import android.util.Half
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresApi
-
-// Use the platform version to benefit from ART intrinsics on API 30+
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun floatToHalf(f: Float): Short = if (Build.VERSION.SDK_INT >= 26) {
- Api26Impl.floatToHalf(f)
-} else {
- softwareFloatToHalf(f)
-}
-
-// Use the platform version to benefit from ART intrinsics on API 30+
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun halfToFloat(h: Short): Float = if (Build.VERSION.SDK_INT >= 26) {
- Api26Impl.halfToFloat(h)
-} else {
- softwareHalfToFloat(h)
-}
-
-@RequiresApi(26)
-internal object Api26Impl {
- @JvmStatic
- @DoNotInline
- @Suppress("HalfFloat")
- fun floatToHalf(f: Float) = Half.toHalf(f)
-
- @JvmStatic
- @DoNotInline
- @Suppress("HalfFloat")
- fun halfToFloat(h: Short) = Half.toFloat(h)
-}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index 718543b..d9f676a 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -23,8 +23,12 @@
import androidx.compose.ui.graphics.drawscope.DefaultDensity
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.GraphicsLayerImpl
+import androidx.compose.ui.graphics.layer.GraphicsLayerV23
import androidx.compose.ui.graphics.layer.GraphicsLayerV29
+import androidx.compose.ui.graphics.layer.GraphicsViewLayer
import androidx.compose.ui.graphics.layer.LayerManager
+import androidx.compose.ui.graphics.layer.view.DrawChildContainer
+import androidx.compose.ui.graphics.layer.view.ViewLayerContainer
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -41,29 +45,51 @@
private val lock = Any()
private val layerManager = LayerManager(CanvasHolder())
+ private var viewLayerContainer: DrawChildContainer? = null
override fun createGraphicsLayer(): GraphicsLayer {
synchronized(lock) {
val ownerId = getUniqueDrawingId(ownerView)
val layerImpl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
GraphicsLayerV29(ownerId)
+ } else if (isRenderNodeCompatible && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ try {
+ GraphicsLayerV23(ownerView, ownerId)
+ } catch (_: Throwable) {
+ // If we ever failed to create an instance of the RenderNode stub based
+ // GraphicsLayer, always fallback to creation of View based layers as it is
+ // unlikely that subsequent attempts to create a GraphicsLayer with RenderNode
+ // stubs would be successful.
+ isRenderNodeCompatible = false
+ GraphicsViewLayer(
+ obtainViewLayerContainer(ownerView),
+ ownerId
+ )
+ }
} else {
- // Temporarily throw unsupported exceptions for API levels < Q as the GraphicsLayer
- // implementations for lower API levels are checked in
- throw UnsupportedOperationException(
- "GraphicsLayer is currently only supported on Android Q"
+ GraphicsViewLayer(
+ obtainViewLayerContainer(ownerView),
+ ownerId
)
}
return GraphicsLayer(layerImpl).also { layer ->
// Do a placeholder recording of drawing instructions to avoid errors when doing a
// persistence render.
// This will be overridden by the consumer of the created GraphicsLayer
- layer.buildLayer(
- DefaultDensity,
- LayoutDirection.Ltr,
- IntSize(1, 1),
- GraphicsLayerImpl.DefaultDrawBlock
- )
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // Only API levels between M (inclusive) and P (exclusive) require a placeholder
+ // displaylist for persistence rendering. On some API levels like (ex. API 28)
+ // actually doing a placeholder render before the activity is setup
+ // (ex in unit tests) causes the emulator to crash with an NPE in native code
+ // on the HWUI canvas implementation
+ layer.buildLayer(
+ DefaultDensity,
+ LayoutDirection.Ltr,
+ IntSize(1, 1),
+ GraphicsLayerImpl.DefaultDrawBlock
+ )
+ }
layerManager.persist(layer)
// Reset the size to zero so that immediately after GraphicsLayer creation
// we do not advertise a size of 1 x 1
@@ -78,6 +104,18 @@
}
}
+ private fun obtainViewLayerContainer(ownerView: ViewGroup): DrawChildContainer {
+ var container = viewLayerContainer
+ if (container == null) {
+ val context = ownerView.context
+
+ container = ViewLayerContainer(context)
+ ownerView.addView(container)
+ viewLayerContainer = container
+ }
+ return container
+ }
+
private fun getUniqueDrawingId(view: View): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
UniqueDrawingIdApi29.getUniqueDrawingId(view)
@@ -85,6 +123,10 @@
-1
}
+ internal companion object {
+ var isRenderNodeCompatible: Boolean = true
+ }
+
@RequiresApi(29)
private object UniqueDrawingIdApi29 {
@JvmStatic
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
new file mode 100644
index 0000000..4cfe87e
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.layer
+
+import android.graphics.Outline
+import android.graphics.PorterDuffXfermode
+import android.os.Build
+import android.view.DisplayListCanvas
+import android.view.RenderNode
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.RenderEffect
+import androidx.compose.ui.graphics.asAndroidColorFilter
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPorterDuffMode
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.toSize
+import java.util.concurrent.atomic.AtomicBoolean
+
+@RequiresApi(Build.VERSION_CODES.M)
+internal class GraphicsLayerV23(
+ ownerView: View,
+ override val ownerId: Long,
+ private val canvasHolder: CanvasHolder = CanvasHolder(),
+ private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
+) : GraphicsLayerImpl {
+
+ private val renderNode = RenderNode.create("Compose", ownerView)
+ private var size: IntSize = IntSize.Zero
+ private var layerPaint: android.graphics.Paint? = null
+
+ private fun obtainLayerPaint(): android.graphics.Paint =
+ layerPaint ?: android.graphics.Paint().also { layerPaint = it }
+
+ init {
+ // only need to do this once
+ if (needToValidateAccess.getAndSet(false)) {
+ // This is only to force loading the DisplayListCanvas class and causing the
+ // MRenderNode to fail with a NoClassDefFoundError during construction instead of
+ // later.
+ @Suppress("UNUSED_VARIABLE")
+ val displayListCanvas: DisplayListCanvas? = null
+
+ // Ensure that we can access properties of the RenderNode. We want to force an
+ // exception here if there is a problem accessing any of these so that we can
+ // fall back to the View implementation.
+ renderNode.scaleX = renderNode.scaleX
+ renderNode.scaleY = renderNode.scaleY
+ renderNode.translationX = renderNode.translationX
+ renderNode.translationY = renderNode.translationY
+ renderNode.elevation = renderNode.elevation
+ renderNode.rotation = renderNode.rotation
+ renderNode.rotationX = renderNode.rotationX
+ renderNode.rotationY = renderNode.rotationY
+ renderNode.cameraDistance = renderNode.cameraDistance
+ renderNode.pivotX = renderNode.pivotX
+ renderNode.pivotY = renderNode.pivotY
+ renderNode.clipToOutline = renderNode.clipToOutline
+ renderNode.setClipToBounds(false)
+ renderNode.alpha = renderNode.alpha
+ renderNode.isValid // only read
+ renderNode.setLeftTopRightBottom(0, 0, 0, 0)
+ renderNode.offsetLeftAndRight(0)
+ renderNode.offsetTopAndBottom(0)
+ verifyShadowColorProperties(renderNode)
+ discardDisplayListInternal()
+ renderNode.setLayerType(View.LAYER_TYPE_NONE)
+ renderNode.setHasOverlappingRendering(renderNode.hasOverlappingRendering())
+ }
+ if (testFailCreateRenderNode) {
+ throw NoClassDefFoundError()
+ }
+
+ renderNode.setClipToBounds(false)
+ applyCompositingStrategy(CompositingStrategy.Auto)
+ }
+
+ override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
+ set(value) {
+ field = value
+ updateLayerProperties()
+ }
+
+ private fun applyCompositingStrategy(compositingStrategy: CompositingStrategy) {
+ renderNode.apply {
+ when (compositingStrategy) {
+ CompositingStrategy.Offscreen -> {
+ setLayerType(View.LAYER_TYPE_HARDWARE)
+ setLayerPaint(layerPaint)
+ setHasOverlappingRendering(true)
+ }
+ CompositingStrategy.ModulateAlpha -> {
+ setLayerType(View.LAYER_TYPE_NONE)
+ setLayerPaint(layerPaint)
+ setHasOverlappingRendering(false)
+ }
+ else -> { // CompositingStrategy.Auto
+ setLayerType(View.LAYER_TYPE_NONE)
+ setLayerPaint(layerPaint)
+ setHasOverlappingRendering(true)
+ }
+ }
+ }
+ }
+
+ override var blendMode: BlendMode = BlendMode.SrcOver
+ set(value) {
+ if (field != value) {
+ field = value
+ obtainLayerPaint().apply { xfermode = PorterDuffXfermode(value.toPorterDuffMode()) }
+ updateLayerProperties()
+ }
+ }
+
+ private fun requiresCompositingLayer(): Boolean =
+ compositingStrategy == CompositingStrategy.Offscreen ||
+ blendMode != BlendMode.SrcOver ||
+ colorFilter != null
+
+ private fun updateLayerProperties() {
+ if (requiresCompositingLayer()) {
+ applyCompositingStrategy(CompositingStrategy.Offscreen)
+ } else {
+ applyCompositingStrategy(compositingStrategy)
+ }
+ }
+
+ override var colorFilter: ColorFilter? = null
+ set(value) {
+ field = value
+ if (value != null) {
+ applyCompositingStrategy(CompositingStrategy.Offscreen)
+ renderNode.setLayerPaint(obtainLayerPaint().apply {
+ colorFilter = value.asAndroidColorFilter()
+ })
+ } else {
+ updateLayerProperties()
+ }
+ }
+
+ override var alpha: Float = 1.0f
+ set(value) {
+ field = value
+ renderNode.setAlpha(value)
+ }
+
+ override var pivotOffset: Offset = Offset.Unspecified
+ set(value) {
+ field = value
+ renderNode.pivotX = value.x
+ renderNode.pivotY = value.y
+ }
+
+ override var scaleX: Float = 1f
+ set(value) {
+ field = value
+ renderNode.setScaleX(value)
+ }
+ override var scaleY: Float = 1f
+ set(value) {
+ field = value
+ renderNode.setScaleY(value)
+ }
+ override var translationX: Float = 0f
+ set(value) {
+ field = value
+ renderNode.setTranslationX(value)
+ }
+
+ override var translationY: Float = 1f
+ set(value) {
+ field = value
+ renderNode.setTranslationY(value)
+ }
+ override var shadowElevation: Float = 0f
+ set(value) {
+ field = value
+ renderNode.setElevation(value)
+ }
+ override var ambientShadowColor: Color = Color.Black
+ set(value) {
+ field = value
+ renderNode.setAmbientShadowColor(value.toArgb())
+ }
+ override var spotShadowColor: Color = Color.Black
+ set(value) {
+ field = value
+ renderNode.setSpotShadowColor(value.toArgb())
+ }
+ override var rotationX: Float = 0f
+ set(value) {
+ field = value
+ renderNode.setRotationX(value)
+ }
+ override var rotationY: Float = 0f
+ set(value) {
+ field = value
+ renderNode.setRotationY(value)
+ }
+ override var rotationZ: Float = 0f
+ set(value) {
+ field = value
+ renderNode.setRotation(value)
+ }
+ override var cameraDistance: Float = DefaultCameraDistance
+ set(value) {
+ // Camera distance was negated in older API levels. Maintain the same input parameters
+ // and negate the given camera distance before it is applied and also negate it when
+ // it is queried
+ field = value
+ renderNode.setCameraDistance(-value)
+ }
+
+ override var clip: Boolean = false
+ set(value) {
+ field = value
+ }
+
+ // API level 23 does not support RenderEffect so keep the field around for consistency
+ // however, it will not be applied to the rendered result. Consumers are encouraged
+ // to use the RenderEffect.isSupported API before consuming a [RenderEffect] instance.
+ // If RenderEffect is used on an unsupported API level, it should act as a no-op and not
+ // crash the compose application
+ override var renderEffect: RenderEffect? = null
+
+ override fun setPosition(topLeft: IntOffset, size: IntSize) {
+ renderNode.setLeftTopRightBottom(
+ topLeft.x,
+ topLeft.y,
+ topLeft.x + size.width,
+ topLeft.y + size.height
+ )
+ this.size = size
+ }
+
+ override fun setOutline(outline: Outline, clip: Boolean) {
+ renderNode.setOutline(outline)
+ renderNode.clipToOutline = clip
+ }
+
+ override var isInvalidated: Boolean = true
+
+ override fun buildLayer(
+ density: Density,
+ layoutDirection: LayoutDirection,
+ block: DrawScope.() -> Unit
+ ) {
+ val recordingCanvas = renderNode.start(size.width, size.height)
+ canvasHolder.drawInto(recordingCanvas) {
+ canvasDrawScope.draw(
+ density,
+ layoutDirection,
+ this,
+ size.toSize(),
+ block
+ )
+ }
+ renderNode.end(recordingCanvas)
+ isInvalidated = false
+ }
+
+ override fun draw(canvas: androidx.compose.ui.graphics.Canvas) {
+ (canvas.nativeCanvas as DisplayListCanvas).drawRenderNode(renderNode)
+ }
+
+ override fun release() {
+ discardDisplayListInternal()
+ }
+
+ override fun discardDisplayList() {
+ discardDisplayListInternal()
+ }
+
+ override val layerId: Long = 0
+
+ private fun verifyShadowColorProperties(renderNode: RenderNode) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ RenderNodeVerificationHelper28.setAmbientShadowColor(
+ renderNode,
+ RenderNodeVerificationHelper28.getAmbientShadowColor(renderNode)
+ )
+ RenderNodeVerificationHelper28.setSpotShadowColor(
+ renderNode,
+ RenderNodeVerificationHelper28.getSpotShadowColor(renderNode)
+ )
+ }
+ }
+
+ private fun discardDisplayListInternal() {
+ // See b/216660268. RenderNode#discardDisplayList was originally called
+ // destroyDisplayListData on Android M and below. Make sure we gate on the corresponding
+ // API level and call the original method name on these API levels, otherwise invoke
+ // the current method name of discardDisplayList
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ RenderNodeVerificationHelper24.discardDisplayList(renderNode)
+ } else {
+ RenderNodeVerificationHelper23.destroyDisplayListData(renderNode)
+ }
+ }
+
+ companion object {
+ // Used by tests to force failing creating a RenderNode to simulate a device that
+ // doesn't support RenderNodes before Q.
+ internal var testFailCreateRenderNode = false
+
+ // We need to validate that RenderNodes can be accessed before using the RenderNode
+ // stub implementation, but we only need to validate it once. This flag indicates that
+ // validation is still needed.
+ private val needToValidateAccess = AtomicBoolean(true)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+private object RenderNodeVerificationHelper28 {
+
+ @androidx.annotation.DoNotInline
+ fun getAmbientShadowColor(renderNode: RenderNode): Int {
+ return renderNode.ambientShadowColor
+ }
+
+ @androidx.annotation.DoNotInline
+ fun setAmbientShadowColor(renderNode: RenderNode, target: Int) {
+ renderNode.ambientShadowColor = target
+ }
+
+ @androidx.annotation.DoNotInline
+ fun getSpotShadowColor(renderNode: RenderNode): Int {
+ return renderNode.spotShadowColor
+ }
+
+ @androidx.annotation.DoNotInline
+ fun setSpotShadowColor(renderNode: RenderNode, target: Int) {
+ renderNode.spotShadowColor = target
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.N)
+private object RenderNodeVerificationHelper24 {
+
+ @androidx.annotation.DoNotInline
+ fun discardDisplayList(renderNode: RenderNode) {
+ renderNode.discardDisplayList()
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.M)
+private object RenderNodeVerificationHelper23 {
+
+ @androidx.annotation.DoNotInline
+ fun destroyDisplayListData(renderNode: RenderNode) {
+ renderNode.destroyDisplayListData()
+ }
+}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
new file mode 100644
index 0000000..aa70c09
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.layer
+
+import android.graphics.Canvas
+import android.graphics.Outline
+import android.graphics.PorterDuffXfermode
+import android.os.Build
+import android.view.View
+import android.view.View.LAYER_TYPE_HARDWARE
+import android.view.View.LAYER_TYPE_NONE
+import android.view.ViewOutlineProvider
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.RenderEffect
+import androidx.compose.ui.graphics.asAndroidColorFilter
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.drawscope.DefaultDensity
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.layer.GraphicsLayerImpl.Companion.DefaultDrawBlock
+import androidx.compose.ui.graphics.layer.view.DrawChildContainer
+import androidx.compose.ui.graphics.layer.view.PlaceholderHardwareCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPorterDuffMode
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+
+internal class ViewLayer(
+ val ownerView: View,
+ val canvasHolder: CanvasHolder = CanvasHolder(),
+ private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
+) : View(ownerView.context) {
+
+ var isInvalidated = false
+
+ init {
+ outlineProvider = LayerOutlineProvider
+ }
+
+ var layerOutline: Outline? = null
+ set(value) {
+ field = value
+ invalidateOutline()
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
+ // b/18175261 On the initial Lollipop release invalidateOutline
+ // would not invalidate shadows so force an invalidation here instead
+ invalidate()
+ }
+ }
+
+ internal var canUseCompositingLayer = true
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidate()
+ }
+ }
+
+ private var density: Density = DefaultDensity
+ private var layoutDirection: LayoutDirection = LayoutDirection.Ltr
+ private var drawBlock: DrawScope.() -> Unit = DefaultDrawBlock
+
+ fun setDrawParams(
+ density: Density,
+ layoutDirection: LayoutDirection,
+ drawBlock: DrawScope.() -> Unit
+ ) {
+ this.density = density
+ this.layoutDirection = layoutDirection
+ this.drawBlock = drawBlock
+ }
+
+ init {
+ setWillNotDraw(false) // we WILL draw
+ this.clipBounds = null
+ }
+
+ override fun invalidate() {
+ if (!isInvalidated) {
+ isInvalidated = true
+ super.invalidate()
+ }
+ }
+
+ override fun hasOverlappingRendering(): Boolean {
+ return canUseCompositingLayer
+ }
+
+ override fun dispatchDraw(canvas: android.graphics.Canvas) {
+ canvasHolder.drawInto(canvas) {
+ canvasDrawScope.draw(
+ density,
+ layoutDirection,
+ this,
+ Size(width.toFloat(), height.toFloat()),
+ drawBlock
+ )
+ }
+ isInvalidated = false
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ }
+
+ override fun forceLayout() {
+ // Don't do anything. These Views are treated as RenderNodes, so a forced layout
+ // should not do anything. If we keep this, we get more redrawing than is necessary.
+ }
+
+ companion object {
+ internal val LayerOutlineProvider = object : ViewOutlineProvider() {
+ override fun getOutline(view: View?, outline: Outline) {
+ if (view is ViewLayer) {
+ view.layerOutline?.let { layerOutline ->
+ outline.set(layerOutline)
+ }
+ }
+ }
+ }
+ }
+}
+
+internal class GraphicsViewLayer(
+ private val layerContainer: DrawChildContainer,
+ override val ownerId: Long,
+ val canvasHolder: CanvasHolder = CanvasHolder(),
+ canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
+) : GraphicsLayerImpl {
+
+ private val viewLayer = ViewLayer(layerContainer, canvasHolder, canvasDrawScope)
+ private val resources = layerContainer.resources
+ private val clipRect = android.graphics.Rect()
+ private var layerPaint: android.graphics.Paint? = null
+
+ init {
+ layerContainer.addView(viewLayer)
+ viewLayer.clipBounds = null
+ }
+
+ private var topLeft = IntOffset.Zero
+ private var size = IntSize.Zero
+ private var clipInvalidated = false
+ override var isInvalidated: Boolean = true
+
+ override val layerId: Long = View.generateViewId().toLong()
+
+ override var blendMode: BlendMode = BlendMode.SrcOver
+ set(value) {
+ field = value
+ obtainLayerPaint().apply { xfermode = PorterDuffXfermode(value.toPorterDuffMode()) }
+ updateLayerProperties()
+ }
+ override var colorFilter: ColorFilter? = null
+ set(value) {
+ field = value
+ obtainLayerPaint().apply { this.colorFilter = value?.asAndroidColorFilter() }
+ updateLayerProperties()
+ }
+ override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
+ set(value) {
+ field = value
+ updateLayerProperties()
+ }
+
+ private fun applyCompositingLayer(compositingStrategy: CompositingStrategy) {
+ viewLayer.canUseCompositingLayer = when (compositingStrategy) {
+ CompositingStrategy.Offscreen -> {
+ viewLayer.setLayerType(LAYER_TYPE_HARDWARE, layerPaint)
+ true
+ }
+ CompositingStrategy.ModulateAlpha -> {
+ viewLayer.setLayerType(LAYER_TYPE_NONE, layerPaint)
+ false
+ }
+ else -> {
+ viewLayer.setLayerType(LAYER_TYPE_NONE, layerPaint)
+ true
+ }
+ }
+ }
+
+ private fun updateLayerProperties() {
+ if (requiresCompositingLayer()) {
+ applyCompositingLayer(CompositingStrategy.Offscreen)
+ } else {
+ applyCompositingLayer(compositingStrategy)
+ }
+ }
+
+ private fun obtainLayerPaint(): android.graphics.Paint =
+ layerPaint ?: android.graphics.Paint().also { layerPaint = it }
+
+ private fun requiresCompositingLayer(): Boolean =
+ compositingStrategy == CompositingStrategy.Offscreen ||
+ requiresLayerPaint()
+
+ private fun requiresLayerPaint(): Boolean =
+ blendMode != BlendMode.SrcOver || colorFilter != null
+
+ override var alpha: Float = 1f
+ set(value) {
+ field = value
+ viewLayer.setAlpha(value)
+ }
+
+ override var pivotOffset: Offset = Offset.Zero
+ set(value) {
+ field = value
+ viewLayer.pivotX = value.x
+ viewLayer.pivotY = value.y
+ }
+ override var scaleX: Float = 1f
+ set(value) {
+ field = value
+ viewLayer.scaleX = value
+ }
+ override var scaleY: Float = 1f
+ set(value) {
+ field = value
+ viewLayer.scaleY = value
+ }
+
+ override var translationX: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.translationX = value
+ }
+ override var translationY: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.translationY = value
+ }
+
+ override var shadowElevation: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.elevation = value
+ }
+ override var ambientShadowColor: Color = Color.Black
+ set(value) {
+ field = value
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ ViewLayerVerificationHelper28.setOutlineAmbientShadowColor(
+ viewLayer,
+ value.toArgb()
+ )
+ }
+ }
+ override var spotShadowColor: Color = Color.Black
+ set(value) {
+ field = value
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ ViewLayerVerificationHelper28.setOutlineSpotShadowColor(viewLayer, value.toArgb())
+ }
+ }
+ override var rotationX: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.rotationX = value
+ }
+ override var rotationY: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.rotationY = value
+ }
+ override var rotationZ: Float = 0f
+ set(value) {
+ field = value
+ viewLayer.rotation = value
+ }
+ override var cameraDistance: Float
+ get() {
+ return viewLayer.getCameraDistance() / resources.displayMetrics.densityDpi
+ }
+ set(value) {
+ viewLayer.setCameraDistance(value * resources.displayMetrics.densityDpi)
+ }
+ override var clip: Boolean = false
+ set(value) {
+ field = value
+ clipInvalidated = true
+ }
+ override var renderEffect: RenderEffect? = null
+ set(value) {
+ field = value
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ViewLayerVerificationHelper31.setRenderEffect(viewLayer, value)
+ }
+ }
+
+ override fun setPosition(topLeft: IntOffset, size: IntSize) {
+ if (this.topLeft.x != topLeft.x) {
+ viewLayer.offsetLeftAndRight(topLeft.x - this.topLeft.x)
+ }
+
+ if (this.topLeft.y != topLeft.y) {
+ viewLayer.offsetTopAndBottom(topLeft.y - this.topLeft.y)
+ }
+
+ if (this.size != size) {
+ if (clip) {
+ clipInvalidated = true
+ }
+ viewLayer.layout(topLeft.x, topLeft.y, topLeft.x + size.width, topLeft.y + size.height)
+ }
+ this.topLeft = topLeft
+ this.size = size
+ }
+
+ override fun setOutline(outline: Outline, clip: Boolean) {
+ viewLayer.layerOutline = outline
+ viewLayer.clipToOutline = clip
+ }
+
+ override fun buildLayer(
+ density: Density,
+ layoutDirection: LayoutDirection,
+ block: DrawScope.() -> Unit
+ ) {
+ viewLayer.setDrawParams(density, layoutDirection, block)
+ try {
+ canvasHolder.drawInto(PlaceholderCanvas) {
+ layerContainer.drawChild(this, viewLayer, viewLayer.drawingTime)
+ }
+ } catch (t: Throwable) {
+ // We will run into class cast exceptions as View rendering attempts to
+ // cast a canvas as a DisplayListCanvas. However, this cast happens after the call to
+ // updateDisplayListIfDirty so just catch the error here and keep going
+ }
+ }
+
+ override fun draw(canvas: androidx.compose.ui.graphics.Canvas) {
+ updateClip()
+ layerContainer.drawChild(canvas, viewLayer, viewLayer.drawingTime)
+ }
+
+ private fun updateClip() {
+ if (clipInvalidated) {
+ viewLayer.clipBounds = if (clip) {
+ clipRect.apply {
+ left = 0
+ top = 0
+ right = viewLayer.width
+ bottom = viewLayer.height
+ }
+ } else {
+ null
+ }
+ }
+ }
+
+ override fun release() {
+ layerContainer.removeViewInLayout(viewLayer)
+ }
+
+ override fun discardDisplayList() {
+ release()
+ }
+
+ companion object {
+
+ val PlaceholderCanvas = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // For Android M+ we just need a Canvas that returns true for isHardwareAccelerated
+ // in order to get the draw calls to update the displaylist of the backing View
+ object : Canvas() {
+ override fun isHardwareAccelerated(): Boolean = true
+ }
+ } else {
+ // On Android L, there is an instanceof check that verify that the Canvas is a
+ // HardwareCanvas so return our subclass of the HardwareCanvas stub
+ PlaceholderHardwareCanvas()
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object ViewLayerVerificationHelper31 {
+
+ @androidx.annotation.DoNotInline
+ fun setRenderEffect(view: View, target: RenderEffect?) {
+ view.setRenderEffect(target?.asAndroidRenderEffect())
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+private object ViewLayerVerificationHelper28 {
+
+ @androidx.annotation.DoNotInline
+ fun setOutlineAmbientShadowColor(view: View, target: Int) {
+ view.outlineAmbientShadowColor = target
+ }
+
+ @androidx.annotation.DoNotInline
+ fun setOutlineSpotShadowColor(view: View, target: Int) {
+ view.outlineSpotShadowColor = target
+ }
+}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/PlaceholderHardwareCanvas.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/PlaceholderHardwareCanvas.android.kt
new file mode 100644
index 0000000..a11c6b9
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/PlaceholderHardwareCanvas.android.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.layer.view
+
+import android.graphics.Rect
+import android.view.HardwareCanvas
+import android.view.RenderNode
+
+/**
+ * Implementation of HardwareCanvas abstract class used to record a displaylist
+ * on demand by passing directly to View#draw(canvas)
+ */
+internal class PlaceholderHardwareCanvas : HardwareCanvas() {
+
+ override fun drawRenderNode(renderNode: RenderNode, dirty: Rect, flags: Int): Int {
+ return 0
+ }
+
+ override fun isHardwareAccelerated(): Boolean {
+ return true
+ }
+}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
new file mode 100644
index 0000000..23185dd
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.layer.view;
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.R
+import androidx.compose.ui.graphics.layer.ViewLayer
+import androidx.compose.ui.graphics.nativeCanvas
+
+/**
+ * The container we will use for [GraphicsViewLayer]s.
+ */
+internal class ViewLayerContainer(context: Context) : DrawChildContainer(context) {
+ override fun dispatchDraw(canvas: android.graphics.Canvas) {
+ // we draw our children as part of AndroidComposeView.dispatchDraw
+ }
+
+ /**
+ * We control our own child Views and we don't want the View system to force updating
+ * the display lists.
+ * We override hidden protected method from ViewGroup
+ */
+ protected fun dispatchGetDisplayList() {
+ }
+}
+
+/**
+ * The container we will use for [ViewLayer]s when [ViewLayer.shouldUseDispatchDraw] is true.
+ */
+internal open class DrawChildContainer(context: Context) : ViewGroup(context) {
+ private var isDrawing = false
+
+ init {
+ clipChildren = false
+ clipToPadding = false
+
+ // Hide this view and its children in tools:
+ setTag(R.id.hide_in_inspector_tag, true)
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ // we don't layout our children
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // we don't measure our children
+ setMeasuredDimension(0, 0)
+ }
+
+ override fun dispatchDraw(canvas: android.graphics.Canvas) {
+ // We must updateDisplayListIfDirty for all invalidated Views.
+
+ // We only want to call super.dispatchDraw() if there is an invalidated layer
+ var doDispatch = false
+ for (i in 0 until super.getChildCount()) {
+ val child = getChildAt(i) as ViewLayer
+ if (child.isInvalidated) {
+ doDispatch = true
+ break
+ }
+ }
+
+ if (doDispatch) {
+ isDrawing = true
+ try {
+ super.dispatchDraw(canvas)
+ } finally {
+ isDrawing = false
+ }
+ }
+ }
+
+ /**
+ * We don't want to advertise children to the transition system. ViewLayers shouldn't be
+ * watched for add/remove for transitions purposes.
+ */
+ override fun getChildCount(): Int = if (isDrawing) super.getChildCount() else 0
+
+ // we change visibility for this method so ViewLayer can use it for drawing
+ internal fun drawChild(canvas: Canvas, view: View, drawingTime: Long) {
+ super.drawChild(canvas.nativeCanvas, view, drawingTime)
+ }
+}
diff --git a/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml b/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml
new file mode 100644
index 0000000..989568a
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <item name="hide_in_inspector_tag" type="id" />
+</resources>
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
index 27b2ece..d04cfcd 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
@@ -27,16 +27,18 @@
*
* The IEEE 754 standard specifies an fp16 as having the following format:
*
- * * Sign bit: 1 bit
- * * Exponent width: 5 bits
- * * Significand: 10 bits
+ * - Sign bit: 1 bit
+ * - Exponent width: 5 bits
+ * - Significand: 10 bits
*
* The format is laid out as follows:
+ * ```
* 1 11111 1111111111
* ^ --^-- -----^----
* sign | |_______ significand
* |
* -- exponent
+ * ```
*
* Half-precision floating points can be useful to save memory and/or
* bandwidth at the expense of range and precision when compared to single-precision
@@ -49,31 +51,31 @@
*
* <table summary="Precision of fp16 across the range">
* <tr><th>Range start</th><th>Precision</th></tr>
- * <tr><td>0</td><td>1 16,777,216</td></tr>
- * <tr><td>1 16,384</td><td>1 16,777,216</td></tr>
- * <tr><td>1 8,192</td><td>1 8,388,608</td></tr>
- * <tr><td>1 4,096</td><td>1 4,194,304</td></tr>
- * <tr><td>1 2,048</td><td>1 2,097,152</td></tr>
- * <tr><td>1 1,024</td><td>1 1,048,576</td></tr>
- * <tr><td>1 512</td><td>1 524,288</td></tr>
- * <tr><td>1 256</td><td>1 262,144</td></tr>
- * <tr><td>1 128</td><td>1 131,072</td></tr>
- * <tr><td>1 64</td><td>1 65,536</td></tr>
- * <tr><td>1 32</td><td>1 32,768</td></tr>
- * <tr><td>1 16</td><td>1 16,384</td></tr>
- * <tr><td>1 8</td><td>1 8,192</td></tr>
- * <tr><td>1 4</td><td>1 4,096</td></tr>
- * <tr><td>1 2</td><td>1 2,048</td></tr>
- * <tr><td>1</td><td>1 1,024</td></tr>
- * <tr><td>2</td><td>1 512</td></tr>
- * <tr><td>4</td><td>1 256</td></tr>
- * <tr><td>8</td><td>1 128</td></tr>
- * <tr><td>16</td><td>1 64</td></tr>
- * <tr><td>32</td><td>1 32</td></tr>
- * <tr><td>64</td><td>1 16</td></tr>
- * <tr><td>128</td><td>1 8</td></tr>
- * <tr><td>256</td><td>1 4</td></tr>
- * <tr><td>512</td><td>1 2</td></tr>
+ * <tr><td>0</td><td>1/16,777,216</td></tr>
+ * <tr><td>1/16,384</td><td>1/16,777,216</td></tr>
+ * <tr><td>1/8,192</td><td>1/8,388,608</td></tr>
+ * <tr><td>1/4,096</td><td>1/4,194,304</td></tr>
+ * <tr><td>1/2,048</td><td>1/2,097,152</td></tr>
+ * <tr><td>1/1,024</td><td>1/1,048,576</td></tr>
+ * <tr><td>1/512</td><td>1/524,288</td></tr>
+ * <tr><td>1/256</td><td>1/262,144</td></tr>
+ * <tr><td>1/128</td><td>1/131,072</td></tr>
+ * <tr><td>1/64</td><td>1/65,536</td></tr>
+ * <tr><td>1/32</td><td>1/32,768</td></tr>
+ * <tr><td>1/16</td><td>1/16,384</td></tr>
+ * <tr><td>1/8</td><td>1/8,192</td></tr>
+ * <tr><td>1/4</td><td>1/4,096</td></tr>
+ * <tr><td>1/2</td><td>1/2,048</td></tr>
+ * <tr><td>1</td><td>1/1,024</td></tr>
+ * <tr><td>2</td><td>1/512</td></tr>
+ * <tr><td>4</td><td>1/256</td></tr>
+ * <tr><td>8</td><td>1/128</td></tr>
+ * <tr><td>16</td><td>1/64</td></tr>
+ * <tr><td>32</td><td>1/32</td></tr>
+ * <tr><td>64</td><td>1/16</td></tr>
+ * <tr><td>128</td><td>1/8</td></tr>
+ * <tr><td>256</td><td>1/4</td></tr>
+ * <tr><td>512</td><td>1/2</td></tr>
* <tr><td>1,024</td><td>1</td></tr>
* <tr><td>2,048</td><td>2</td></tr>
* <tr><td>4,096</td><td>4</td></tr>
@@ -82,12 +84,10 @@
* <tr><td>32,768</td><td>32</td></tr>
* </table>
*
- *
* This table shows that numbers higher than 1024 lose all fractional precision.
*/
@JvmInline
internal value class Float16(val halfValue: Short) : Comparable<Float16> {
-
/**
* Constructs a newly allocated `Float16` object that represents the
* argument converted to a half-precision float.
@@ -599,14 +599,11 @@
* Convert a single-precision float to a half-precision float, stored as
* [Short] data type to hold its 16 bits.
*/
-internal expect fun floatToHalf(f: Float): Short
-
-// Provided here as a convenience for `actual` implementations
@Suppress("NOTHING_TO_INLINE")
-internal inline fun softwareFloatToHalf(f: Float): Short {
+internal inline fun floatToHalf(f: Float): Short {
val bits = f.toRawBits()
- val s = bits.ushr(Fp32SignShift)
- var e = bits.ushr(Fp32ExponentShift) and Fp32ExponentMask
+ val s = bits ushr Fp32SignShift
+ var e = bits ushr Fp32ExponentShift and Fp32ExponentMask
var m = bits and Fp32SignificandMask
var outE = 0
@@ -647,14 +644,11 @@
/**
* Convert a half-precision float to a single-precision float.
*/
-internal expect fun halfToFloat(h: Short): Float
-
-// Provided here as a convenience for `actual` implementations
@Suppress("NOTHING_TO_INLINE")
-internal inline fun softwareHalfToFloat(h: Short): Float {
+internal inline fun halfToFloat(h: Short): Float {
val bits = h.toInt() and 0xffff
val s = bits and Fp16SignMask
- val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
+ val e = bits ushr Fp16ExponentShift and Fp16ExponentMask
val m = bits and Fp16SignificandMask
var outE = 0
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt
deleted file mode 100644
index c102496..0000000
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.graphics
-
-internal actual fun floatToHalf(f: Float): Short = softwareFloatToHalf(f)
-
-internal actual fun halfToFloat(h: Short): Float = softwareHalfToFloat(h)
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index ecf1f1c..5f0952ee 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -42,6 +42,7 @@
compileOnly(libs.kotlinStdlib)
compileOnly("androidx.inspection:inspection:1.0.0")
compileOnly("androidx.compose.runtime:runtime:1.2.1")
+ compileOnly(project(":compose:ui:ui-graphics"))
compileOnly(project(":compose:ui:ui"))
// we ignore its transitive dependencies, because ui-inspection should
// depend on them as "compile-only" deps.
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 85f5c4b..0140b6d 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
@@ -21,7 +21,7 @@
import android.view.ViewGroup
import androidx.collection.LongList
import androidx.collection.mutableLongListOf
-import androidx.compose.ui.R
+import androidx.compose.ui.graphics.R
import androidx.compose.ui.inspection.framework.ancestors
import androidx.compose.ui.inspection.framework.getChildren
import androidx.compose.ui.inspection.framework.isAndroidComposeView
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 5266e91..5a5f447 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -715,7 +715,7 @@
}
public static interface AndroidFont.TypefaceLoader {
- method public suspend Object? awaitLoad(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font, kotlin.coroutines.Continuation<? super android.graphics.Typeface>);
+ method public suspend Object? awaitLoad(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font, kotlin.coroutines.Continuation<? super android.graphics.Typeface?>);
method public android.graphics.Typeface? loadBlocking(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font);
}
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 07aa3d7..272f48a 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -715,7 +715,7 @@
}
public static interface AndroidFont.TypefaceLoader {
- method public suspend Object? awaitLoad(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font, kotlin.coroutines.Continuation<? super android.graphics.Typeface>);
+ method public suspend Object? awaitLoad(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font, kotlin.coroutines.Continuation<? super android.graphics.Typeface?>);
method public android.graphics.Typeface? loadBlocking(android.content.Context context, androidx.compose.ui.text.font.AndroidFont font);
}
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index e0e50d7..67395d6 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -71,7 +71,7 @@
api("androidx.annotation:annotation:1.1.0")
implementation(project(":compose:animation:animation"))
implementation("androidx.savedstate:savedstate-ktx:1.2.1")
- implementation("androidx.compose.material:material:1.0.0")
+ implementation(project(":compose:material:material"))
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 530e8cf..0e97748 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -70,13 +70,22 @@
@androidx.compose.runtime.Immutable public final class BiasAbsoluteAlignment implements androidx.compose.ui.Alignment {
ctor public BiasAbsoluteAlignment(float horizontalBias, float verticalBias);
method public long align(long size, long space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
+ method public float component2();
method public androidx.compose.ui.BiasAbsoluteAlignment copy(float horizontalBias, float verticalBias);
+ method public float getHorizontalBias();
+ method public float getVerticalBias();
+ property public final float horizontalBias;
+ property public final float verticalBias;
}
@androidx.compose.runtime.Immutable public static final class BiasAbsoluteAlignment.Horizontal implements androidx.compose.ui.Alignment.Horizontal {
ctor public BiasAbsoluteAlignment.Horizontal(float bias);
method public int align(int size, int space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
method public androidx.compose.ui.BiasAbsoluteAlignment.Horizontal copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
@androidx.compose.runtime.Immutable public final class BiasAlignment implements androidx.compose.ui.Alignment {
@@ -94,13 +103,19 @@
@androidx.compose.runtime.Immutable public static final class BiasAlignment.Horizontal implements androidx.compose.ui.Alignment.Horizontal {
ctor public BiasAlignment.Horizontal(float bias);
method public int align(int size, int space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
method public androidx.compose.ui.BiasAlignment.Horizontal copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
@androidx.compose.runtime.Immutable public static final class BiasAlignment.Vertical implements androidx.compose.ui.Alignment.Vertical {
ctor public BiasAlignment.Vertical(float bias);
method public int align(int size, int space);
+ method public float component1();
method public androidx.compose.ui.BiasAlignment.Vertical copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
public final class CombinedModifier implements androidx.compose.ui.Modifier {
@@ -1708,7 +1723,7 @@
method public long getSize();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public default suspend <T> Object? withTimeout(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
- method public default suspend <T> Object? withTimeoutOrNull(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method public default suspend <T> Object? withTimeoutOrNull(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T?>);
property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
property public default long extendedTouchPadding;
property public abstract long size;
@@ -1960,10 +1975,10 @@
}
public sealed interface SuspendingPointerInputModifierNode extends androidx.compose.ui.node.PointerInputModifierNode {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object> getPointerInputHandler();
+ method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
method public void resetPointerInputHandler();
method public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>);
- property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object> pointerInputHandler;
+ property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
}
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 394a8532..54d037a 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -70,13 +70,22 @@
@androidx.compose.runtime.Immutable public final class BiasAbsoluteAlignment implements androidx.compose.ui.Alignment {
ctor public BiasAbsoluteAlignment(float horizontalBias, float verticalBias);
method public long align(long size, long space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
+ method public float component2();
method public androidx.compose.ui.BiasAbsoluteAlignment copy(float horizontalBias, float verticalBias);
+ method public float getHorizontalBias();
+ method public float getVerticalBias();
+ property public final float horizontalBias;
+ property public final float verticalBias;
}
@androidx.compose.runtime.Immutable public static final class BiasAbsoluteAlignment.Horizontal implements androidx.compose.ui.Alignment.Horizontal {
ctor public BiasAbsoluteAlignment.Horizontal(float bias);
method public int align(int size, int space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
method public androidx.compose.ui.BiasAbsoluteAlignment.Horizontal copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
@androidx.compose.runtime.Immutable public final class BiasAlignment implements androidx.compose.ui.Alignment {
@@ -94,13 +103,19 @@
@androidx.compose.runtime.Immutable public static final class BiasAlignment.Horizontal implements androidx.compose.ui.Alignment.Horizontal {
ctor public BiasAlignment.Horizontal(float bias);
method public int align(int size, int space, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public float component1();
method public androidx.compose.ui.BiasAlignment.Horizontal copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
@androidx.compose.runtime.Immutable public static final class BiasAlignment.Vertical implements androidx.compose.ui.Alignment.Vertical {
ctor public BiasAlignment.Vertical(float bias);
method public int align(int size, int space);
+ method public float component1();
method public androidx.compose.ui.BiasAlignment.Vertical copy(float bias);
+ method public float getBias();
+ property public final float bias;
}
public final class CombinedModifier implements androidx.compose.ui.Modifier {
@@ -1708,7 +1723,7 @@
method public long getSize();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public default suspend <T> Object? withTimeout(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
- method public default suspend <T> Object? withTimeoutOrNull(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method public default suspend <T> Object? withTimeoutOrNull(long timeMillis, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T?>);
property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
property public default long extendedTouchPadding;
property public abstract long size;
@@ -1960,10 +1975,10 @@
}
public sealed interface SuspendingPointerInputModifierNode extends androidx.compose.ui.node.PointerInputModifierNode {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object> getPointerInputHandler();
+ method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
method public void resetPointerInputHandler();
method public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>);
- property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object> pointerInputHandler;
+ property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 2314312..b591a0e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -109,10 +109,7 @@
isDebugInspectorInfoEnabled = false
}
- // Temporarily restrict test to Android Q+ as minimum API requirements are loosened
- // with support for lower API levels in subsequent CLs
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testRememberGraphicsLayerReleasedAfterComposableDisposed() {
var graphicsLayer: GraphicsLayer? = null
val useGraphicsLayerComposable = mutableStateOf(true)
@@ -133,10 +130,7 @@
assertTrue(graphicsLayer!!.isReleased)
}
- // Temporarily restrict test to Android Q+ as minimum API requirements are loosened
- // with support for lower API levels in subsequent CLs
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testObtainGraphicsLayerReleasedAfterModifierDetached() {
var graphicsLayer: GraphicsLayer? = null
val useCacheModifier = mutableStateOf(true)
@@ -168,10 +162,8 @@
assertTrue(graphicsLayer!!.isReleased)
}
- // Temporarily restrict test to Android Q+ as minimum API requirements are loosened
- // with support for lower API levels in subsequent CLs
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testBuildLayerWithCache() {
var graphicsLayer: GraphicsLayer? = null
val testTag = "TestTag"
@@ -218,10 +210,8 @@
}
}
- // Temporarily restrict test to Android Q+ as minimum API requirements are loosened
- // with support for lower API levels in subsequent CLs
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testGraphicsLayerPersistence() {
val testTag = "TestTag"
val drawGraphicsLayer = mutableStateOf(0)
@@ -279,10 +269,8 @@
rule.onNodeWithTag(testTag).captureToImage().toPixelMap().apply { verifyColor(rectColor) }
}
- // Temporarily restrict test to Android Q+ as minimum API requirements are loosened
- // with support for lower API levels in subsequent CLs
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testBuildLayerDrawContent() {
val testTag = "TestTag"
val targetColor = Color.Blue
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 812dabd..2baedd8 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.graphics.vector
+import android.app.Activity
import android.app.Application
import android.content.ComponentCallbacks2
import android.content.pm.ActivityInfo
@@ -23,6 +24,7 @@
import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Build
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
@@ -1154,17 +1156,13 @@
assertTrue("Cache was not cleared after trim memory call", cacheCleared)
}
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun testImageVectorConfigChange() {
- val tag = "testTag"
- rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
-
- val latch = CountDownLatch(1)
-
- rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
+ private fun Activity.rotate(rotation: Int): Boolean {
+ var rotationCount = 0
+ var rotateSuccess = false
+ var latch: CountDownLatch? = null
+ val callbacks = object : ComponentCallbacks2 {
override fun onConfigurationChanged(p0: Configuration) {
- latch.countDown()
+ latch?.countDown()
}
override fun onLowMemory() {
@@ -1174,10 +1172,31 @@
override fun onTrimMemory(p0: Int) {
// NO-OP
}
- })
-
+ }
+ application.registerComponentCallbacks(callbacks)
try {
- latch.await(1500, TimeUnit.MILLISECONDS)
+ while (rotationCount < 3 && !rotateSuccess) {
+ latch = CountDownLatch(1)
+ this.requestedOrientation = rotation
+ rotateSuccess = latch.await(3000, TimeUnit.MILLISECONDS) &&
+ this.requestedOrientation == rotation
+ rotationCount++
+ }
+ } finally {
+ application.unregisterComponentCallbacks(callbacks)
+ }
+ return rotateSuccess
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun testImageVectorConfigChange() {
+ if (!rule.activity.rotate(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)) {
+ Log.w(TAG, "device rotation unsuccessful")
+ return
+ }
+ val tag = "testTag"
+ try {
rule.setContent {
Image(
painterResource(R.drawable.ic_triangle_config),
@@ -1191,7 +1210,7 @@
} catch (e: InterruptedException) {
fail("Unable to verify vector asset in landscape orientation")
} finally {
- rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ rule.activity.rotate(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
}
@@ -1458,4 +1477,6 @@
Assert.assertEquals(height, bitmap.height)
return bitmap
}
+
+ private val TAG = "VectorTest"
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
index 6716cc4..118695c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
@@ -19,8 +19,8 @@
import android.content.Context
import android.view.View
import android.view.ViewGroup
-import androidx.compose.ui.R
import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.R
import androidx.compose.ui.graphics.nativeCanvas
/**
diff --git a/compose/ui/ui/src/androidMain/res/values/ids.xml b/compose/ui/ui/src/androidMain/res/values/ids.xml
index 721b41d..140ecaf 100644
--- a/compose/ui/ui/src/androidMain/res/values/ids.xml
+++ b/compose/ui/ui/src/androidMain/res/values/ids.xml
@@ -52,6 +52,5 @@
<item name="inspection_slot_table_set" type="id" />
<item name="androidx_compose_ui_view_composition_context" type="id" />
<item name="compose_view_saveable_id_tag" type="id" />
- <item name="hide_in_inspector_tag" type="id" />
<item name="consume_window_insets_tag" type="id" />
</resources>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Alignment.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Alignment.kt
index 94e2368..1226144 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Alignment.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Alignment.kt
@@ -185,7 +185,7 @@
* @see Vertical
*/
@Immutable
- data class Horizontal(private val bias: Float) : Alignment.Horizontal {
+ data class Horizontal(val bias: Float) : Alignment.Horizontal {
override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int {
// Convert to Px first and only round at the end, to avoid rounding twice while
// calculating the new positions
@@ -205,7 +205,7 @@
* @see Horizontal
*/
@Immutable
- data class Vertical(private val bias: Float) : Alignment.Vertical {
+ data class Vertical(val bias: Float) : Alignment.Vertical {
override fun align(size: Int, space: Int): Int {
// Convert to Px first and only round at the end, to avoid rounding twice while
// calculating the new positions
@@ -227,8 +227,8 @@
*/
@Immutable
data class BiasAbsoluteAlignment(
- private val horizontalBias: Float,
- private val verticalBias: Float
+ val horizontalBias: Float,
+ val verticalBias: Float
) : Alignment {
/**
* Returns the position of a 2D point in a container of a given size, according to this
@@ -256,7 +256,7 @@
* @see BiasAlignment.Horizontal
*/
@Immutable
- data class Horizontal(private val bias: Float) : Alignment.Horizontal {
+ data class Horizontal(val bias: Float) : Alignment.Horizontal {
/**
* Returns the position of a 2D point in a container of a given size,
* according to this [BiasAbsoluteAlignment.Horizontal]. This position will not be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverModifierNode.kt
index 34f9ef3..9b2fa6f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverModifierNode.kt
@@ -49,7 +49,8 @@
/**
* Use this function to observe snapshot reads for any target within the specified [block].
- * [onDrawCacheReadsChanged] is called when any of the observed values within the snapshot change.
+ * [ObserverModifierNode.onObservedReadsChanged] is called when any of the observed values within
+ * the snapshot change.
*/
fun <T> T.observeReads(block: () -> Unit) where T : Modifier.Node, T : ObserverModifierNode {
val target = ownerScope ?: ObserverNodeOwnerScope(this).also { ownerScope = it }
diff --git a/core/core/src/main/java/androidx/core/DeleteMe.kt b/core/core/src/main/java/androidx/core/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/core/core/src/main/java/androidx/core/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/development/bench-flame-diff/.gitignore b/development/bench-flame-diff/.gitignore
new file mode 100644
index 0000000..9324f7a
--- /dev/null
+++ b/development/bench-flame-diff/.gitignore
@@ -0,0 +1,22 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+build
+
+# Ignore dependencies
+.deps
+
+# Ignore saved traces
+saved-traces
+
+# Ignore saved diffs
+saved-diffs
+
+# Ignore archived
+archive/*
+
+# Ignore completion files
+completion_bash.sh
+completion_fish.sh
+completion_zsh.sh
diff --git a/development/bench-flame-diff/README.md b/development/bench-flame-diff/README.md
new file mode 100644
index 0000000..87977c0
--- /dev/null
+++ b/development/bench-flame-diff/README.md
@@ -0,0 +1,77 @@
+# Overview
+
+The project provides an easy way to save before/after CPU traces from Microbenchmark runs, and compare them visually using Differential Flame Graphs.
+
+
+
+Areas where the code got slower are highlighted in red, while areas that are now faster are marked in blue; the intensity of the colour is proportional to the size of the difference.
+
+See also the [end-to-end demo (video)](https://drive.google.com/file/d/119nI_zlAMbTHzh-Rdzf8UuUVCGEKnKFQ/view?usp=drive_link&resourcekey=0-SRRmKgVZYfAlnkL4Hvh-cg).
+
+# Usage
+
+## Interacting with the script
+
+- Overview of all commands: `./bench-flame-diff.sh -h`
+- Help for a specific command: `./bench-flame-diff.sh <command> -h`
+
+## First usage
+
+On first usage, initialise all dependencies by running: `./bench-flame-diff.sh init`
+
+## General workflow
+
+1. Run a specific Microbenchmark with CPU Stack sampling enabled (see below for instructions)
+1. Save the trace as _base_ for comparison using `./bench-flame-diff.sh save`. It's worth picking a good names for the saved traces since you're likely going to e.g. re-use the _base_ while iterating on code changes.
+1. Apply changes in your code and run the same benchmark as in step 1
+1. Save the trace as _current_ `./bench-flame-diff.sh save`
+1. Compare both traces using `./bench-flame-diff.sh diff` which will create and open a diff in a web browser
+1. Toggle between graphs using the buttons on the top:
+ - `base`: flamegraph for the _base_ trace
+ - `base-vs-curr`: differential flame graph showing _base_ vs _current_ on the _base_ trace
+ - `curr`: flamegraph for the _current_ trace
+ - `curr-vs-base`: differential flame graph showing _base_ vs _current_ on the _current_ trace
+1. You can later go back to generated diffs using `./bench-flame-diff.sh open`
+
+# Misc
+
+## Enabling stack sampling in Benchmark traces
+
+This can be done in CLI or by editing `build.gradle`. Full documentation is [here](https://developer.android.com/topic/performance/benchmarking/microbenchmark-profile).
+
+Quick CLI example:
+```
+# pick a target benchmark
+tgt=:compose:foundation:foundation-benchmark:connectedCheck
+
+# create a regex that targets a specific benchmark (test)
+test_rx="androidx.compose.foundation.benchmark.lazy.LazyListScrollingBenchmark.scrollProgrammatically_noNewItems\[.*Row.*\]"
+
+# run the benchmark and gather a 5 second (default) stack sample at 1000 Hz (default)
+./gradlew $tgt -Pandroid.testInstrumentationRunnerArguments.tests_regex="$test_rx" \
+ -P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.mode=StackSampling \
+ -P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.sampleDurationSeconds=5 \
+ -P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.sampleFrequency=1000
+```
+
+## CLI completion
+
+Generate completion files with `./generate-completion-files.sh` and source in your shell config, e.g.:
+- For `bash`: `dst="$(pwd)/completion_bash.sh"; echo "source '$dst'" >> ~/.bashrc`
+- For `zsh`: `dst="$(pwd)/completion_zsh.sh"; echo "source '$dst'" >> ~/.zshrc`
+
+After restarting the shell session, you will be able to 'tab-autocomplete' commands and argument names.
+
+# Dependencies
+
+On top of dependencies discoverable with `./gradlew app:dependencies` the project depends on:
+- https://github.com/brendangregg/FlameGraph
+- https://android.googlesource.com/platform/system/extras/+/refs/heads/main/simpleperf/scripts
+
+Both are fetched from the network in the `init` command and pinned to known-good-revisions.
+
+# Reporting issues
+
+File an issue on Buganizer using [this link](https://b.corp.google.com/issues/new?component=1229612&hotlistIds=3622386&hotlistIds=5709693&assignee=jgielzak@google.com&title=bench-flame-diff:%20) or reach out directly to [jgielzak@](http://go/moma/chat?with=jgielzak).
+
+Known issues and future work items are tracked [here](https://b.corp.google.com/hotlists/5709693).
diff --git a/development/bench-flame-diff/app/build.gradle.kts b/development/bench-flame-diff/app/build.gradle.kts
new file mode 100644
index 0000000..39efc0b
--- /dev/null
+++ b/development/bench-flame-diff/app/build.gradle.kts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.jvm)
+ application
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation("com.github.ajalt.clikt:clikt:4.2.1")
+ implementation("com.zaxxer:nuprocess:2.0.6")
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(11))
+ }
+}
+
+application {
+ mainClass.set("bench.flame.diff.AppKt")
+ applicationName = "bench-flame-diff"
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/App.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/App.kt
new file mode 100644
index 0000000..2d45e58
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/App.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff
+
+import bench.flame.diff.command.Diff
+import bench.flame.diff.command.Init
+import bench.flame.diff.command.List
+import bench.flame.diff.command.Open
+import bench.flame.diff.command.Save
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.context
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.output.MordantHelpFormatter
+import com.github.ajalt.mordant.rendering.Widget
+import com.github.ajalt.mordant.table.horizontalLayout
+import com.github.ajalt.mordant.table.verticalLayout
+import com.github.ajalt.mordant.widgets.Text
+import com.github.ajalt.mordant.widgets.withPadding
+
+fun main(args: Array<out String>) {
+ BenchFlameDiff().subcommands(Init(), Save(), List(), Diff(), Open()).main(args)
+}
+
+class BenchFlameDiff : CliktCommand(
+ help = "Generate differential flame graphs from microbenchmark CPU traces.",
+ epilog = """
+ Dependencies
+ FlameGraph - https://github.com/brendangregg/FlameGraph for generating graphs.
+ Simpleperf - Android NDK's Simpleperf for converting traces into FlameGraph input format.
+ Clikt - com.github.ajalt.clikt:clikt for the CLI interface.
+ NuProcess - com.zaxxer:nuprocess for running external processes.
+ """.trimIndent()
+) {
+ init {
+ context {
+ helpFormatter = {
+ object : MordantHelpFormatter(it, showRequiredTag = true) {
+ override fun renderEpilog(epilog: String): Widget = verticalLayout {
+ val lines = epilog.lines()
+ val header = lines.first() + ":"
+ val dependencies = lines.drop(1).map {
+ it.split(" - ").also { check(it.size == 2) }
+ .let { (name, desc) -> name to desc }
+ }
+
+ cell(Text(theme.warning(header)))
+ for ((name, desc) in dependencies) cell(
+ horizontalLayout {
+ cell(Text(theme.info(name)).withPadding { left = 2; right = 1 })
+ cell(desc)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun run() = Unit
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Diff.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Diff.kt
new file mode 100644
index 0000000..48ed373
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Diff.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.command
+
+import bench.flame.diff.config.Paths
+import bench.flame.diff.interop.execWithChecks
+import bench.flame.diff.interop.file
+import bench.flame.diff.interop.id
+import bench.flame.diff.interop.log
+import bench.flame.diff.interop.openFileInOs
+import bench.flame.diff.ui.promptProvideFile
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.groups.MutuallyExclusiveOptions
+import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
+import com.github.ajalt.clikt.parameters.groups.single
+import com.github.ajalt.clikt.parameters.options.check
+import com.github.ajalt.clikt.parameters.options.convert
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.types.file
+import com.github.ajalt.clikt.parameters.types.int
+import java.io.File
+import kotlin.io.path.absolutePathString
+
+class Diff : CliktCommand(help = "Compare two saved trace files.") {
+ private val before by fileOption("before")
+ private val after by fileOption("after")
+
+ override fun run() {
+ Init.verifyDependencies()
+
+ val before: File = when {
+ before == null -> promptProvideFile("Provide the 'before' path",
+ defaultSrcDir = Paths.savedTracesDir.toFile())
+ else -> checkNotNull(before)
+ }
+ val after: File = when {
+ after == null -> promptProvideFile("Provide the 'after' path",
+ defaultSrcDir = Paths.savedTracesDir.toFile())
+ else -> checkNotNull(after)
+ }
+
+ // TODO: support custom labels
+ val diffDir: File = run {
+ fun createPath(number: Int): File {
+ val dstDirBaseName =
+ "diff_${before.nameWithoutExtension}_${after.nameWithoutExtension}"
+ val suffix = if (number == 0) "" else "_${String.format("%03d", number)}"
+ return Paths.savedDiffsDir.resolve("$dstDirBaseName$suffix").toFile()
+ }
+
+ var i = 0
+ var result = createPath(i)
+ while (result.exists()) result = createPath(++i)
+ result.mkdirs()
+ result
+ }
+
+ val indexHtml = createDiffHtmlPage(before, after, diffDir)
+ openFileInOs(indexHtml)
+ log("Opened '${indexHtml.canonicalPath}' in the browser.", isStdErr = true)
+ }
+
+ private fun createDiffHtmlPage(beforeRaw: File, afterRaw: File, diffDir: File): File {
+ val beforeRawFolded = diffDir.resolve("before-raw.folded")
+ val afterRawFolded = diffDir.resolve("after-raw.folded")
+ val beforeRawSvg = diffDir.resolve("before-raw.svg")
+ val afterRawSvg = diffDir.resolve("after-raw.svg")
+
+ val afterDiffFolded = diffDir.resolve("after-diff.folded")
+ val beforeDiffFolded = diffDir.resolve("before-diff.folded")
+ val afterDiffSvg = diffDir.resolve("after-diff.svg")
+ val beforeDiffSvg = diffDir.resolve("before-diff.svg")
+
+ val indexHtml = diffDir.resolve("index.html")
+
+ collapseStacks(beforeRaw, beforeRawFolded)
+ collapseStacks(afterRaw, afterRawFolded)
+ createDiff(beforeRawFolded, afterRawFolded, afterDiffFolded)
+ createDiff(afterRawFolded, beforeRawFolded, beforeDiffFolded)
+
+ createFlameGraph(beforeRawFolded, beforeRawSvg)
+ createFlameGraph(afterRawFolded, afterRawSvg)
+ createFlameGraph(afterDiffFolded, afterDiffSvg)
+ createFlameGraph(beforeDiffFolded, beforeDiffSvg, negate = true)
+
+ createIndexHtml(
+ TraceDiffResult(beforeRawSvg, beforeDiffSvg, afterRawSvg, afterDiffSvg), indexHtml
+ )
+
+ for (tmpFile in listOf(beforeRawFolded, afterRawFolded, beforeDiffFolded, afterDiffFolded))
+ tmpFile.delete()
+ return indexHtml
+ }
+
+ private data class TraceDiffResult(
+ val beforeRawGraph: File,
+ val beforeDiffGraph: File,
+ val afterRawGraph: File,
+ val afterDiffGraph: File
+ )
+
+ private fun fileOption(role: String): MutuallyExclusiveOptions<File, File?> {
+ return mutuallyExclusiveOptions(
+ option(
+ "--$role-file",
+ help = "Path to the '$role' file."
+ ).file(mustExist = true, canBeDir = false),
+ option(
+ "--$role-id",
+ help = "Id of the '$role' file as per the **${List().commandName}** command"
+ ).int()
+ .convert { id: Int ->
+ checkNotNull(List.savedTraces(Paths.savedTracesDir.toFile())
+ .singleOrNull { it.id == id }) { "no saved trace with id '$id'" }
+ .file
+ }
+ .check { f: File -> f.exists() && f.isFile },
+ name = "${role.take(1).uppercase()}${role.drop(1)} file",
+ ).single()
+ }
+
+ private fun collapseStacks(srcFile: File, dstFile: File) =
+ execWithChecks(
+ Paths.stackcollapsePy.absolutePathString(),
+ "--trace-offcpu=mixed-on-off-cpu",
+ "--event-filter", "cpu-clock", // this only allows for on-cpu; pending (b/325484390)
+ "-i",
+ srcFile.absolutePath,
+ cwd = Paths.stackcollapsePy.parent
+ ) {
+ dstFile.bufferedWriter().use { writer -> writer.write(it.stdOut) }
+ }
+
+ private fun createDiff(folded1: File, folded2: File, dstFile: File) =
+ execWithChecks(
+ Paths.difffoldedPl.absolutePathString(), "-n", folded1.absolutePath,
+ folded2.absolutePath
+ ) {
+ dstFile.bufferedWriter().use { writer -> writer.write(it.stdOut) }
+ }
+
+ private fun createFlameGraph(srcFile: File, dstFile: File, negate: Boolean = false) =
+ execWithChecks(
+ Paths.flamegraphPl.absolutePathString(),
+ "--title=${dstFile.nameWithoutExtension}"
+ .replace("before-raw", "base") // TODO(327208814)
+ .replace("after-raw", "current")
+ .replace("before-diff", "base vs current")
+ .replace("after-diff", "current vs base"),
+ "--fonttype=Roboto, sans-serif",
+ "--fontsize=13",
+ "--bgcolors=#f5f5f5",
+ "--minwidth=2.0",
+ "--width=1800", // TODO: autosize
+ "--inverted",
+ "--hash",
+ "--totaldiff",
+ "--mindeltapc=0.1",
+ "--fillopacity=0.65",
+ if (negate) "--negate" else "",
+ "--colors=grey",
+ "--countname=ns",
+ srcFile.absolutePath,
+ ) {
+ dstFile.bufferedWriter().use { writer -> writer.write(it.stdOut) }
+ }
+
+ private fun createIndexHtml(src: TraceDiffResult, dstFile: File) {
+ val rawContent = checkNotNull(
+ this::class.java.classLoader.getResourceAsStream("templates/index.html")
+ ).bufferedReader().use { it.readText() }
+ val content = rawContent
+ .replace("%before_raw_file%", src.beforeRawGraph.name)
+ .replace("%before_diff_file%", src.beforeDiffGraph.name)
+ .replace("%after_raw_file%", src.afterRawGraph.name)
+ .replace("%after_diff_file%", src.afterDiffGraph.name)
+ .replace("%before_raw_name%", "show base")
+ .replace("%before_diff_name%", "diff base vs curr")
+ .replace("%after_raw_name%", "show curr")
+ .replace("%after_diff_name%", "diff curr vs base")
+ dstFile.bufferedWriter().use { writer -> writer.write(content) }
+ }
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Init.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Init.kt
new file mode 100644
index 0000000..de5ba08
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Init.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.command
+
+import bench.flame.diff.config.Paths
+import bench.flame.diff.config.Uris
+import bench.flame.diff.interop.Os
+import bench.flame.diff.interop.execWithChecks
+import bench.flame.diff.interop.exitProcessWithError
+import bench.flame.diff.interop.output
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.mordant.markdown.Markdown
+import java.nio.file.Files
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.exists
+import kotlin.io.path.name
+
+private const val verifyOnlyFlagName = "--verify-only"
+
+class Init : CliktCommand(help = "Initialize the tool's dependencies.") {
+ private val verifyOnly by option(
+ names = arrayOf(verifyOnlyFlagName),
+ help = "Verify the dependencies without initialising any."
+ ).flag(default = false)
+
+ override fun run() {
+ // Ensure we are on a Mac or Linux
+ if (!Os.isMac && !Os.isLinux) {
+ exitProcessWithError(
+ "unsupported operating system '${Os.rawName}', only Mac and Linux are supported."
+ )
+ }
+
+ /** Check out [Paths.flamegraphPl] from Git if not checked out */
+ if (!Paths.flamegraphPl.exists()) {
+ if (verifyOnly) exitProcessWithError(
+ Markdown(
+ "dependency '${Paths.flamegraphDir.name}'" +
+ " not satisfied. Run the **${this.commandName}** command to fix."
+ )
+ )
+ execWithChecks("mkdir", "-p", Paths.flamegraphDir.absolutePathString())
+ execWithChecks(
+ "git",
+ "clone",
+ "-n", // no checkout of HEAD is performed, ensuring we only use known revisions
+ Uris.flamegraphGitHub,
+ Paths.flamegraphDir.absolutePathString()
+ )
+ execWithChecks(
+ "git", "checkout",
+ "252e09ac0a56469c1f4a7d3440e5a7d352e1761e", // known good revision
+ cwd = Paths.flamegraphDir
+ )
+ }
+
+ /** Check that we can execute [Paths.flamegraphPl] */
+ execWithChecks(
+ Paths.flamegraphPl.absolutePathString(),
+ "--help",
+ checkIsSuccess = {
+ it.output.contains("USAGE") && it.output.contains(Paths.flamegraphPl.name)
+ }
+ )
+
+ /** Check that we can execute Simpleperf's [Paths.stackcollapsePy] */
+ if (!Paths.stackcollapsePy.exists()) {
+ if (verifyOnly) exitProcessWithError(
+ Markdown(
+ "dependency '${Paths.simpleperfDir.name}'" +
+ " not satisfied. Run the **${this.commandName}** command to fix."
+ )
+ )
+ val simplePerfDirPath = Paths.simpleperfDir.absolutePathString()
+ execWithChecks("mkdir", "-p", simplePerfDirPath)
+ val tmpSourceZip =
+ Files.createTempFile(Paths.simpleperfDir, "simpleperf-scripts-snapshot", ".tar.gz")
+ val tmpSourceZipPath = tmpSourceZip.absolutePathString()
+ execWithChecks("curl", Uris.simpleperfGoogleSource, "-o", tmpSourceZipPath)
+ execWithChecks(
+ "bash", "-c", "tar -xzf '$tmpSourceZipPath' -O | shasum -a256 | awk '{print $1}'"
+ ) {
+ val expected = "b9b5b41b270a7cb77be56b6a8c55930e2d565cf3a6e07617e198a2c336e19f91"
+ val actual = it.stdOut.trim()
+ check(actual == expected) {
+ Files.delete(tmpSourceZip)
+ "Simpleperf checksum mismatch. Expected: $expected. Actual: $actual"
+ }
+ }
+ execWithChecks("tar", "-xzf", tmpSourceZipPath, "-C", simplePerfDirPath)
+ }
+ execWithChecks(
+ Paths.stackcollapsePy.absolutePathString(),
+ "--help",
+ cwd = Paths.stackcollapsePy.parent
+ )
+ }
+
+ internal companion object {
+ fun verifyDependencies() = Init().main(arrayOf(verifyOnlyFlagName))
+ }
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/List.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/List.kt
new file mode 100644
index 0000000..ccda04a
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/List.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.command
+
+import bench.flame.diff.config.Paths
+import bench.flame.diff.interop.FileWithId
+import bench.flame.diff.interop.withId
+import bench.flame.diff.ui.printFileTable
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.mordant.markdown.Markdown
+import java.io.File
+import kotlin.collections.List
+
+class List : CliktCommand(help = "List all saved trace files.") {
+ override fun run() {
+ val savedTracesDir = Paths.savedTracesDir.toFile()
+ val traces = savedTraces(savedTracesDir)
+
+ if (traces.isEmpty()) {
+ echo(
+ Markdown(
+ "No trace files saved. Run the **${Save().commandName}** command" +
+ " to save traces for future comparison."
+ )
+ )
+ return
+ }
+
+ printFileTable(traces, savedTracesDir)
+ }
+
+ companion object {
+ /** Returns a list of saved traces sorted by 'most recently modified first' */
+ internal fun savedTraces(savedTracesDir: File): List<FileWithId> {
+ val files: Array<File> = when {
+ !savedTracesDir.exists() -> emptyArray<File>()
+ else -> savedTracesDir.listFiles() ?: emptyArray<File>()
+ }
+ files.sortWith(compareBy { f: File -> -f.lastModified() })
+ return files.asSequence().withId().toList()
+ }
+ }
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Open.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Open.kt
new file mode 100644
index 0000000..f210c8d
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Open.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.command
+
+import bench.flame.diff.config.Paths
+import bench.flame.diff.interop.log
+import bench.flame.diff.interop.openFileInOs
+import bench.flame.diff.interop.withId
+import bench.flame.diff.ui.promptPickFile
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.mordant.markdown.Markdown
+
+class Open : CliktCommand(help = "Open a saved diff.") {
+ override fun run() {
+ val diffsDir = Paths.savedDiffsDir.toFile()
+ val diffFiles =
+ diffsDir.walkTopDown().filter { it.name == "index.html" }
+ .sortedBy { -it.lastModified() }.withId().toList()
+
+ if (diffFiles.isEmpty()) {
+ echo(
+ Markdown(
+ "No diffs saved. Run the **${Diff().commandName}** command to compare traces."
+ )
+ )
+ return
+ }
+
+ val pickedDiff = promptPickFile(diffFiles, diffsDir)
+ openFileInOs(pickedDiff)
+ log("Opened '${pickedDiff.canonicalPath}' in the browser.", isStdErr = true)
+ }
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Save.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Save.kt
new file mode 100644
index 0000000..70fec93
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/command/Save.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.command
+
+import bench.flame.diff.config.Paths
+import bench.flame.diff.interop.isValidFileName
+import bench.flame.diff.ui.promptOverwriteFile
+import bench.flame.diff.ui.promptProvideFile
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.terminal
+import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
+import com.github.ajalt.clikt.parameters.groups.single
+import com.github.ajalt.clikt.parameters.options.check
+import com.github.ajalt.clikt.parameters.options.default
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.types.file
+import com.github.ajalt.mordant.terminal.ConversionResult
+import java.io.File
+
+class Save : CliktCommand(help = "Save a trace file for future comparison.") {
+ private val src by mutuallyExclusiveOptions(
+ option("--src-file", help = "Path to a trace file.")
+ .file(mustExist = true, canBeDir = false),
+ option("--src-dir", help = "Path to a directory containing trace files.")
+ .file(mustExist = true, canBeFile = false),
+ name = "Trace source",
+ ).single()
+
+ private val dst by option(help = "Name for the saved trace file.")
+ .check { it.isValidFileName() }
+
+ private val pattern by option("--pattern", help = "Trace file name regex.")
+ .default(Paths.traceFileNamePattern)
+
+ override fun run() {
+ val src = src // allows for smart casts
+ val srcFile: File = when {
+ src != null && src.isFile -> src
+ else -> promptProvideFile("Provide trace source", pattern, src, Paths.outDir.toFile())
+ }
+ check(srcFile.exists() && srcFile.isFile)
+
+ val dstFile: File =
+ Paths.savedTracesDir.resolve(dst ?: promptDestinationName(srcFile.name)).toFile()
+ if (dstFile.exists()) promptOverwriteFile(dstFile).let { overwrite ->
+ if (!overwrite) return
+ }
+
+ dstFile.parentFile.mkdirs() // ensure destination dir is present
+ srcFile.copyTo(dstFile, overwrite = true)
+ }
+
+ private fun promptDestinationName(default: String? = null): String =
+ terminal.prompt("Provide destination file name", default) {
+ when {
+ it.isValidFileName() -> ConversionResult.Valid(it)
+ else -> ConversionResult.Invalid("Invalid value")
+ }
+ }!!
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/config/Paths.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/config/Paths.kt
new file mode 100644
index 0000000..94ad14f
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/config/Paths.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.config
+
+import java.nio.file.Path
+import kotlin.io.path.name
+
+internal object Paths {
+ val currentDir get() = Path.of("").toAbsolutePath()
+ private val dependenciesDir get() = currentDir.resolve(".deps")
+ val savedTracesDir get() = currentDir.resolve("saved-traces")
+ val savedDiffsDir get() = currentDir.resolve("saved-diffs")
+ val outDir get() = frameworksSupportDir.parent.parent.resolve("out")
+ private val frameworksSupportDir get() = currentDir.parent.parent.also {
+ check(it.parent.name == "frameworks" && it.name == "support")
+ }
+ val simpleperfDir get() = dependenciesDir.resolve("simpleperf")
+ val stackcollapsePy get() = simpleperfDir.resolve("stackcollapse.py")
+ val flamegraphDir get() = dependenciesDir.resolve("Flamegraph")
+ val flamegraphPl get() = flamegraphDir.resolve("flamegraph.pl")
+ val difffoldedPl get() = flamegraphDir.resolve("difffolded.pl")
+
+ val traceFileNamePattern = ".*stackSampling.*\\.trace"
+}
+
+internal object Uris {
+ // Using jgielzak@ fork of https://github.com/brendangregg/FlameGraph until
+ // https://github.com/brendangregg/FlameGraph/pull/329 is merged.
+ val flamegraphGitHub = "https://github.com/gielzak/FlameGraph"
+
+ // Using a snapshot of Simpleperf until https://r.android.com/2980531 makes it into the NDK.
+ // Next check: 2024-Q4.
+ val simpleperfGoogleSource =
+ "https://android.googlesource.com/platform/system/extras/+archive/" +
+ "436786af3a357db5fd72cdac97903d6d587944a1/simpleperf/scripts.tar.gz"
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/File.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/File.kt
new file mode 100644
index 0000000..517b7ae
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/File.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.interop
+
+import com.github.ajalt.clikt.core.CliktCommand
+import java.io.File
+import java.nio.file.InvalidPathException
+import java.nio.file.Path
+
+internal fun String.isValidFileName() = isNotBlank() && isValidPath() && isFileNameOnly()
+
+private fun String.isValidPath(): Boolean = try {
+ Path.of(this)
+ true
+} catch (_: InvalidPathException) {
+ false
+}
+
+private fun String.isFileNameOnly(): Boolean = isNotBlank() && isValidPath() &&
+ Path.of(this).parent == null
+
+internal fun CliktCommand.openFileInOs(target: File) = execWithChecks(
+ if (Os.isMac) "open" else "xdg-open", target.absolutePath
+)
+
+internal typealias FileWithId = IndexedValue<File>
+internal val FileWithId.id get() = index + 1
+internal val FileWithId.file get() = value
+internal fun Sequence<File>.withId(): Sequence<FileWithId> = withIndex()
diff --git a/appcompat/appcompat/src/main/java/DeleteMe.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Os.kt
similarity index 64%
copy from appcompat/appcompat/src/main/java/DeleteMe.kt
copy to development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Os.kt
index 38f8b7a..18b914d 100644
--- a/appcompat/appcompat/src/main/java/DeleteMe.kt
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Os.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,5 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package bench.flame.diff.interop
-// This file exists to trick AGP/lint to work around b/234865137
+internal object Os {
+ val isMac: Boolean get() = rawName.lowercase().contains("mac os")
+ val isLinux: Boolean get() = rawName.lowercase().contains("linux")
+ val rawName: String get() = System.getProperty("os.name")
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Process.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Process.kt
new file mode 100644
index 0000000..cf61871
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Process.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.interop
+
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.mordant.rendering.Widget
+import kotlin.system.exitProcess
+
+internal fun CliktCommand.exitProcessWithError(error: String): Nothing =
+ exitProcessWithErrorImpl(error)
+
+internal fun CliktCommand.exitProcessWithError(error: Widget): Nothing =
+ exitProcessWithErrorImpl(error)
+
+internal fun CliktCommand.log(message: String, isStdErr: Boolean = false): Unit =
+ logImpl(message, isStdErr)
+
+private fun CliktCommand.exitProcessWithErrorImpl(error: Any): Nothing {
+ logImpl(error, true)
+ exitProcess(1)
+}
+
+private fun CliktCommand.logImpl(error: Any, isStdErr: Boolean) {
+ echo("bench-flame-diff: ", err = true, trailingNewline = false)
+ echo(error, err = isStdErr)
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Shell.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Shell.kt
new file mode 100644
index 0000000..a039c90
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/interop/Shell.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.interop
+
+import bench.flame.diff.config.Paths
+import com.github.ajalt.clikt.core.CliktCommand
+import com.zaxxer.nuprocess.NuAbstractProcessHandler
+import com.zaxxer.nuprocess.NuProcessBuilder
+import java.nio.ByteBuffer
+import java.nio.file.Path
+import java.util.concurrent.TimeUnit
+
+internal class Shell(private val cwd: Path) {
+ fun exec(vararg command: String): ExecutionResult {
+ val handler = object : NuAbstractProcessHandler() {
+ var outs = StringBuilder()
+ var errs = StringBuilder()
+ var exitCode: Int = 0
+
+ override fun onExit(exitCode: Int) {
+ this.exitCode = exitCode
+ }
+
+ override fun onStdout(buffer: ByteBuffer, closed: Boolean) = consumeLines(buffer, outs)
+
+ override fun onStderr(buffer: ByteBuffer, closed: Boolean) = consumeLines(buffer, errs)
+
+ private fun consumeLines(srcBuffer: ByteBuffer, destination: StringBuilder) {
+ val dstBuffer = ByteArray(srcBuffer.remaining())
+ srcBuffer.get(dstBuffer)
+ destination.append(String(dstBuffer))
+ }
+ }
+
+ NuProcessBuilder(command.asList()).also {
+ it.setCwd(cwd)
+ it.setProcessListener(handler)
+ }.start().waitFor(0, TimeUnit.SECONDS)
+
+ return ExecutionResult(handler.exitCode, handler.outs.toString(), handler.errs.toString())
+ }
+
+ data class ExecutionResult(val exitCode: Int, val stdOut: String, val stdErr: String)
+}
+
+internal val Shell.ExecutionResult.output get() = stdOut + stdErr
+
+internal fun CliktCommand.execWithChecks(
+ vararg command: String,
+ cwd: Path = Paths.currentDir,
+ checkIsSuccess: (Shell.ExecutionResult) -> Boolean = { it.exitCode == 0 },
+ onError: (Shell.ExecutionResult) -> Unit = {
+ exitProcessWithError(
+ "error occurred while executing command '${command.asList()}'." +
+ " \nexit code: ${it.exitCode}" +
+ " \nstdout: ${it.stdOut}" +
+ " \nstderr: ${it.stdErr}"
+ )
+ },
+ onSuccess: (Shell.ExecutionResult) -> Unit = { },
+) {
+ val result = Shell(cwd).exec(*command)
+ if (!checkIsSuccess(result)) onError(result) else onSuccess(result)
+}
diff --git a/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/ui/Terminal.kt b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/ui/Terminal.kt
new file mode 100644
index 0000000..b78f809
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/kotlin/bench/flame/diff/ui/Terminal.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package bench.flame.diff.ui
+
+import bench.flame.diff.interop.FileWithId
+import bench.flame.diff.interop.exitProcessWithError
+import bench.flame.diff.interop.file
+import bench.flame.diff.interop.id
+import bench.flame.diff.interop.withId
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.terminal
+import com.github.ajalt.mordant.rendering.Whitespace
+import com.github.ajalt.mordant.table.table
+import com.github.ajalt.mordant.terminal.ConversionResult
+import com.github.ajalt.mordant.terminal.YesNoPrompt
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+
+internal fun CliktCommand.printFileTable(files: List<FileWithId>, trimBaseDir: File?) {
+ val trimPrefix: String = if (trimBaseDir == null) "" else "${trimBaseDir.canonicalPath}/"
+ terminal.println(table {
+ whitespace = Whitespace.PRE_WRAP
+ header { row("Id", "Name", "Last Modified") }
+ body {
+ files.map {
+ val lastModified = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+ .format(Date(it.file.lastModified()))
+ row(it.id, it.file.canonicalPath.removePrefix(trimPrefix), lastModified)
+ }
+ }
+ })
+}
+
+internal fun CliktCommand.promptPickFile(candidates: List<FileWithId>, trimBaseDir: File?): File {
+ printFileTable(candidates, trimBaseDir)
+ return terminal.prompt("Choose a file by id") {
+ val number = it.toIntOrNull()
+ val min = candidates.minOf { f -> f.id }
+ val max = candidates.maxOf { f -> f.id }
+ when {
+ number == null || candidates.none { f -> f.id == number } ->
+ ConversionResult.Invalid("Choose a number between $min and $max")
+ else ->
+ ConversionResult.Valid(candidates.single { f -> f.id == number }.file)
+ }
+ }!!
+}
+
+internal fun CliktCommand.promptProvideFile(
+ prompt: String,
+ pattern: String = ".*",
+ srcDir: File? = null,
+ defaultSrcDir: File? = null
+): File {
+ check(srcDir == null || srcDir.isDirectory)
+ check(defaultSrcDir == null || defaultSrcDir.isDirectory)
+ val baseDir = run {
+ val src = srcDir ?: terminal.prompt(prompt, defaultSrcDir) {
+ when {
+ it.isBlank() || !File(it).exists() -> ConversionResult.Invalid("Invalid value")
+ else -> ConversionResult.Valid(File(it))
+ }
+ }!!
+ if (src.isFile) return src
+ src
+ }
+
+ check(baseDir.isDirectory)
+ echo("Looking for files in '${baseDir.absolutePath}' matching '$pattern'...")
+ val candidates = baseDir.walkTopDown().filter { it.isFile && it.name.matches(Regex(pattern)) }
+ .sortedBy { -it.lastModified() }.withId().toList()
+
+ if (candidates.isEmpty()) exitProcessWithError("No files matching '$pattern' in '$baseDir'")
+ return promptPickFile(candidates, baseDir)
+}
+
+internal fun CliktCommand.promptOverwriteFile(file: File): Boolean = YesNoPrompt(
+ "Overwrite existing file '${file.absolutePath}'",
+ terminal
+).ask()!!
diff --git a/development/bench-flame-diff/app/src/main/resources/templates/index.html b/development/bench-flame-diff/app/src/main/resources/templates/index.html
new file mode 100644
index 0000000..90ee7c4
--- /dev/null
+++ b/development/bench-flame-diff/app/src/main/resources/templates/index.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Diff</title>
+
+ <link
+ href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css"
+ rel="stylesheet"
+ />
+ <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
+ <style>
+ .spacer {
+ margin: 10px;
+ display: inline;
+ }
+ .top-nav-title {
+ font-family: 'Courier New', monospace;
+ font-size: small;
+ margin-left: 2px;
+ }
+ .svg {
+ margin-top: 7px
+ }
+ </style>
+ </head>
+ <body>
+ <div>
+ <div>
+ <span class="top-nav-title">bench-flame-diff</span>
+ <div class="spacer"></div>
+ <button class="button" onclick="openTab('%before_raw_name%')">%before_raw_name%</button>
+ <button class="button" onclick="openTab('%before_diff_name%')">%before_diff_name%</button>
+ <div class="spacer">|</div>
+ <button class="button" onclick="openTab('%after_raw_name%')">%after_raw_name%</button>
+ <button class="button" onclick="openTab('%after_diff_name%')">%after_diff_name%</button>
+ <div class="spacer">|</div>
+ <button class="button" onclick="window.open('http://go/bench-flame-diff-readme')">help</button>
+ </div>
+ <div id="%before_raw_name%" class="svg">
+ <object type="image/svg+xml" data="%before_raw_file%">Failed to display the file: %before_raw_file%</object>
+ </div>
+ <div id="%before_diff_name%" class="svg">
+ <object type="image/svg+xml" data="%before_diff_file%">Failed to display the file: %before_diff_file%</object>
+ </div>
+ <div id="%after_raw_name%" class="svg">
+ <object type="image/svg+xml" data="%after_raw_file%">Failed to display the file: %after_raw_file%</object>
+ </div>
+ <div id="%after_diff_name%" class="svg">
+ <object type="image/svg+xml" data="%after_diff_file%">Failed to display the file: %after_diff_file%</object>
+ </div>
+ </div>
+
+ <script>
+ mdc.ripple.MDCRipple.attachTo(document.querySelector(".button"));
+
+ function openTab(tabName) {
+ let tab = document.getElementsByClassName("svg");
+ for (let i = 0; i < tab.length; i++) tab[i].style.display = "none";
+ document.getElementById(tabName).style.display = "block";
+ }
+
+ openTab("%after_diff_name%");
+ </script>
+ </body>
+</html>
diff --git a/development/bench-flame-diff/assets/illustration-diff.webp b/development/bench-flame-diff/assets/illustration-diff.webp
new file mode 100644
index 0000000..f7479db
--- /dev/null
+++ b/development/bench-flame-diff/assets/illustration-diff.webp
Binary files differ
diff --git a/development/bench-flame-diff/bench-flame-diff.sh b/development/bench-flame-diff/bench-flame-diff.sh
new file mode 100755
index 0000000..66a6e40
--- /dev/null
+++ b/development/bench-flame-diff/bench-flame-diff.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+./gradlew --quiet installDist && ./app/build/install/bench-flame-diff/bin/bench-flame-diff "$@"
diff --git a/development/bench-flame-diff/generate-completion.sh b/development/bench-flame-diff/generate-completion.sh
new file mode 100755
index 0000000..99d29ec
--- /dev/null
+++ b/development/bench-flame-diff/generate-completion.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+function generate_completion_files() {
+ for shell in bash fish zsh; do
+ dst=completion_$shell.sh
+ (export _BENCH_FLAME_DIFF_COMPLETE=$shell; ./bench-flame-diff.sh | sed -E "s_bench-flame-diff( |$)_bench-flame-diff.sh\1_" >| $dst)
+ echo "Generated $dst"
+ done
+}
+
+generate_completion_files
diff --git a/development/bench-flame-diff/gradle/libs.versions.toml b/development/bench-flame-diff/gradle/libs.versions.toml
new file mode 100644
index 0000000..a157c04
--- /dev/null
+++ b/development/bench-flame-diff/gradle/libs.versions.toml
@@ -0,0 +1,2 @@
+[plugins]
+jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" }
diff --git a/development/bench-flame-diff/gradle/verification-metadata.xml b/development/bench-flame-diff/gradle/verification-metadata.xml
new file mode 100644
index 0000000..35f3a46
--- /dev/null
+++ b/development/bench-flame-diff/gradle/verification-metadata.xml
@@ -0,0 +1,423 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
+ <configuration>
+ <verify-metadata>true</verify-metadata>
+ <verify-signatures>false</verify-signatures>
+ </configuration>
+ <components>
+ <component group="com.github.ajalt.clikt" name="clikt" version="4.2.1">
+ <artifact name="clikt-4.2.1.module">
+ <sha256 value="0f134f26129ed70b6694661176a54e702ffe325b737130fcc8a5f0ea20cf24b9" origin="Generated by Gradle">
+ <also-trust value="1f134f26129ed70b6694661176a54e702ffe325b737130fcc8a5f0ea20cf24b9"/>
+ </sha256>
+ </artifact>
+ <artifact name="clikt-metadata.jar">
+ <sha256 value="934ed6297dfa5945ddb07622182b3737a6d3b95c26b44efc41a0a86db614d25f" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.github.ajalt.clikt" name="clikt-jvm" version="4.2.1">
+ <artifact name="clikt-jvm-4.2.1.module">
+ <sha256 value="4e680f0706d80a50e9a7f17580a8cbaa733cb35c5fb6651adbef8c653a841d09" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="clikt-jvm.jar">
+ <sha256 value="efdf5fc38dfa2953a2e1995cecba0dc281aab35385d595fe042155158c61647f" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.github.ajalt.colormath" name="colormath" version="3.3.1">
+ <artifact name="colormath-3.3.1.module">
+ <sha256 value="01d4d68d02f2a32d3032472f88b1d2c1e0c8bfab3bdd34e003db2c0319ead0ba" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="colormath-metadata.jar">
+ <sha256 value="b80bf47aa347db77b7e0fb4518af7e84252027c47e2c443c38b04fa979d8b97d" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.github.ajalt.colormath" name="colormath-jvm" version="3.3.1">
+ <artifact name="colormath-jvm-3.3.1.module">
+ <sha256 value="2bf76054dcb23a76e52651906bc6a25055ba6ed05afe36b79208309f2be8924f" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="colormath-jvm.jar">
+ <sha256 value="2613283415e2e12661697dc7295adada0f60ec17ecfcaf3ef1c4ee0fdb788913" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.github.ajalt.mordant" name="mordant" version="2.2.0">
+ <artifact name="mordant-2.2.0.module">
+ <sha256 value="3d9b2c0e1ea43c63cdc99bbea9aa38b543a6c8c889528ebd2a5e4e53e2341e27" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="mordant-metadata.jar">
+ <sha256 value="6857ff8610461a0c0df17318c5898b86272173cacec76a2309c1c2a58305385c" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.github.ajalt.mordant" name="mordant-jvm" version="2.2.0">
+ <artifact name="mordant-jvm-2.2.0.module">
+ <sha256 value="a4112264923207ae621642498b572fa688a63879a17a87c2dfb760512ef539f2" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="mordant-jvm.jar">
+ <sha256 value="2fa59e5b81afcc71b6ab6128b27676ffc611d0a3c182ebc0e437b3666ab56201" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.google.code.gson" name="gson" version="2.9.1">
+ <artifact name="gson-2.9.1.jar">
+ <sha256 value="378534e339e6e6d50b1736fb3abb76f1c15d1be3f4c13cec6d536412e23da603" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="gson-2.9.1.pom">
+ <sha256 value="e5966323d7142570b37a4be979e21bc2dae848107e4dc416d8f44d9aa3f02903" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.google.code.gson" name="gson-parent" version="2.9.1">
+ <artifact name="gson-parent-2.9.1.pom">
+ <sha256 value="7ca0845e73685618de3e46bd3434d03a4a373d520fab93a680318ad6c8cb2a79" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.zaxxer" name="nuprocess" version="2.0.6">
+ <artifact name="nuprocess-2.0.6.jar">
+ <sha256 value="978c9224c38cddece6b572d2743770208e6c29b4c7710db92089247ce2125aef" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="nuprocess-2.0.6.pom">
+ <sha256 value="6d4c0ea8e76c3c372dfac7a206654ffcd4e37eea43d647fbd264a49bbcd2e352" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="it.unimi.dsi" name="fastutil-core" version="8.5.12">
+ <artifact name="fastutil-core-8.5.12.jar">
+ <sha256 value="f31c20f5b06312f3d5e06e6160a32e274d819aa6cebf27528b26b6b5c0c1df19" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="fastutil-core-8.5.12.pom">
+ <sha256 value="839243bbe61611f937bb0b5d9b31d0c8e096c7d0d679922cf6ed39f82c6ee0d2" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="net.java.dev.jna" name="jna" version="5.11.0">
+ <artifact name="jna-5.11.0.jar">
+ <sha256 value="e2bce99e4aefd4dab097019a799d317cb3b5d56c3ddd7984c69a772dceed0dd3" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="jna-5.11.0.pom">
+ <sha256 value="5fde1ae98a30bceb6516dc8f7e19a3ee504143ceeb9b2ba4ae107105dfe3c326" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="net.java.dev.jna" name="jna" version="5.13.0">
+ <artifact name="jna-5.13.0.jar">
+ <sha256 value="66d4f819a062a51a1d5627bffc23fac55d1677f0e0a1feba144aabdd670a64bb" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="jna-5.13.0.pom">
+ <sha256 value="f515c2578178f45247ecca7a9e1db109531b1c42f2424e253ceeb0f6b8d42374" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.gradle.toolchains" name="foojay-resolver" version="0.7.0">
+ <artifact name="foojay-resolver-0.7.0.jar">
+ <sha256 value="93672b4740a0fdbfbb5baf08353db8ae9a9bb25f6b10a93c078126c717db3ac0" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="foojay-resolver-0.7.0.module">
+ <sha256 value="ed6746a09f32bfaddb90b029102ae62326e248129a68b74662a4a433d55314b8" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.gradle.toolchains.foojay-resolver-convention" name="org.gradle.toolchains.foojay-resolver-convention.gradle.plugin" version="0.7.0">
+ <artifact name="org.gradle.toolchains.foojay-resolver-convention.gradle.plugin-0.7.0.pom">
+ <sha256 value="c8a443e2faef876f338a391233c20a91ad5b83f69c1c6ba20d39a7b39106240a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains" name="annotations" version="13.0">
+ <artifact name="annotations-13.0.jar">
+ <sha256 value="ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="annotations-13.0.pom">
+ <sha256 value="965aeb2bedff369819bdde1bf7a0b3b89b8247dd69c88b86375d76163bb8c397" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains" name="markdown" version="0.5.2">
+ <artifact name="markdown-0.5.2.module">
+ <sha256 value="6e91401a6a4b6e77356ae2337630e8a2f2dc41ac261b705b3a966ffc623f23e4" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="markdown-metadata-0.5.2.jar">
+ <sha256 value="1c6b1b6bb9ce83048b8648990ac1d3427bfacfebc8e58e32ee9043ddd0dc3a2a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains" name="markdown-jvm" version="0.5.2">
+ <artifact name="markdown-jvm-0.5.2.jar">
+ <sha256 value="726484477260a552dc7c19b09d4656c48ae3b10db2725000e182d6159529b46e" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="markdown-jvm-0.5.2.module">
+ <sha256 value="7f303666042d78d24c555120c882478c309a228ee92b51bd71adbffa49c33b50" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.intellij.deps" name="trove4j" version="1.0.20200330">
+ <artifact name="trove4j-1.0.20200330.jar">
+ <sha256 value="c5fd725bffab51846bf3c77db1383c60aaaebfe1b7fe2f00d23fe1b7df0a439d" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="trove4j-1.0.20200330.pom">
+ <sha256 value="87721cbaa65a3c97d8b1ba9d207840f164c9fe38759fc9ea10ffe26565f8d3e9" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-android-extensions" version="1.9.20">
+ <artifact name="kotlin-android-extensions-1.9.20.jar">
+ <sha256 value="b771239469f0af07e180f746cfde6a7956c2b6261e1ae20e5b1d620a0dd29bff" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-android-extensions-1.9.20.pom">
+ <sha256 value="0d7adb86de59d5de3807eded1b095bc9e14b00adf1dc250a5f62c9c3f54183e6" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-build-common" version="1.9.20">
+ <artifact name="kotlin-build-common-1.9.20.jar">
+ <sha256 value="17319416d0fa12cd77a9f365f8b8cb9c616953883368a5c7f529cf082da9e98d" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-build-common-1.9.20.pom">
+ <sha256 value="8aadfa0eefe9ed27aa6ee17e748f4d0ec4afa6cdfd81fa4939077ac0631d5046" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="1.9.20">
+ <artifact name="kotlin-build-tools-api-1.9.20.jar">
+ <sha256 value="c722948c568352cdc19dc8a8b245d14aae507d4dcffde6a7b26c535c472c1b17" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-build-tools-api-1.9.20.pom">
+ <sha256 value="3fbc370f8587a3d8e80c69f2dccd65c55173bb44b9d3942e15536bce3ccdc1a6" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="1.9.20">
+ <artifact name="kotlin-build-tools-impl-1.9.20.jar">
+ <sha256 value="b7377a08d67dcddcbe4f7930d8cb0f7d0055789fbb30efdbe97008405d1f026d" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-build-tools-impl-1.9.20.pom">
+ <sha256 value="44f80a02ea520d24c8473ec27879e3b16b873381c057757e1b8f51bee07d5cb1" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="1.9.20">
+ <artifact name="kotlin-compiler-embeddable-1.9.20.jar">
+ <sha256 value="a25024fe5da8440de01af045c4fcb954a22f078738ec02616085f0cfc57b2702" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-compiler-embeddable-1.9.20.pom">
+ <sha256 value="c7d8dfc89e621442504506b9492fb763117406120ce91072bc067ff96d264c23" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="1.9.20">
+ <artifact name="kotlin-compiler-runner-1.9.20.jar">
+ <sha256 value="49769c046f8d392654a4ab52af795455bd41e88d8392aeab9028f0edd5e8d50b" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-compiler-runner-1.9.20.pom">
+ <sha256 value="5ca27d9b2104e3230e7f81abaa6eb611bf3986d74a00e565e25eeb18610d0d8a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="1.9.20">
+ <artifact name="kotlin-daemon-client-1.9.20.jar">
+ <sha256 value="582230cbcfd65d36b94bc9d127f90024b8cf17dfa4a67ef6a929f14c6c27661c" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-daemon-client-1.9.20.pom">
+ <sha256 value="d2a418163a6948d72b04cbe35203a2e305e1c38d568090170ac872f881fa9381" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="1.9.20">
+ <artifact name="kotlin-daemon-embeddable-1.9.20.jar">
+ <sha256 value="a939cb5d6ee2a758c9285bd9f3286824beabe12d9a4b5f49f784d0bca329dea5" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-daemon-embeddable-1.9.20.pom">
+ <sha256 value="cd892de769cc20b9798e22538d9963074b05f1c448f2e01ab77547eeb6597d41" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-1.9.20-gradle81.jar">
+ <sha256 value="04910fca652f8dbe804a49c8e72971bf641d03cd8b45a065ba4ce10c6584eaac" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-1.9.20.module">
+ <sha256 value="55525c3e03692529c8f196607789adc8eeb1a207ea131f8d9c1c32b497f7e636" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-annotations" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-annotations-1.9.20.jar">
+ <sha256 value="2a5c3622e2468584d1ab7dab3acd8ffb60403b637dd0603af675e26d3a054329" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-annotations-1.9.20.pom">
+ <sha256 value="f4bfa0aa2c2392e4f23e26b7f1a419865beb76a016d8180872bb086cc003594e" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-api" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-api-1.9.20.jar">
+ <sha256 value="287c26765f8692e5eb5505854126819cfbb0c7d5d49bbe5f45771427ea19913d" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-api-1.9.20.module">
+ <sha256 value="483e7577f4e9e2d3ca17f6aa64f4655d4603c2b1d0e6dfd5951552ef40d5d745" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-idea-1.9.20.jar">
+ <sha256 value="8d1af87632d95148f122a9fa0ae2903c19ee6fab7d01e017f76e0d2c9a022c20" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-idea-1.9.20.module">
+ <sha256 value="1e6b39381f3a4f9b1497454e825f8942e0c9c6ec3dc1b8a2fd636e237195f8ab" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea-proto" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-idea-proto-1.9.20.jar">
+ <sha256 value="c67b0d8849febdd9a964eda0bd167c167c4d056ca8dd389241d92e1d763c9490" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-idea-proto-1.9.20.pom">
+ <sha256 value="d4010d4b3bb9e9240f3de0e78e8df5a581236f33d62e8070c8fdf25317703594" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-model" version="1.9.20">
+ <artifact name="kotlin-gradle-plugin-model-1.9.20.jar">
+ <sha256 value="7f930f0e454b75818f5f8976ba515f3aec887671a5fe85380ac97f05da9986a7" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugin-model-1.9.20.module">
+ <sha256 value="3996aaab9b546ccf455b1caf0c316ea32e9ce646feb54e973af4d69fb605f248" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugins-bom" version="1.9.20">
+ <artifact name="kotlin-gradle-plugins-bom-1.9.20.module">
+ <sha256 value="7720f845cfe319aa1a6e5b23387e6920a35e99ae4218ca0f6f6e07fd1713093c" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-gradle-plugins-bom-1.9.20.pom">
+ <sha256 value="6f3ba42a3e981700284c956146ef4d716b89adbe5d803ab92553dec216344330" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-api" version="1.9.20">
+ <artifact name="kotlin-klib-commonizer-api-1.9.20.jar">
+ <sha256 value="89b6260828953042e310a52592aa5b595f5f89b641cc6a3d3a8155ef92d88ffe" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-klib-commonizer-api-1.9.20.pom">
+ <sha256 value="9780fdc5679a4db1a170fa5210f561ceb2ad46b6f829c86ac31fc78cf9296c65" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-embeddable" version="1.9.20">
+ <artifact name="kotlin-klib-commonizer-embeddable-1.9.20.jar">
+ <sha256 value="8b36530fb4b68198c7733adbe3749d481af2bd9c0b03e89d88bfe93e12fda0f9" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-klib-commonizer-embeddable-1.9.20.pom">
+ <sha256 value="ef24d0adecfe9adcd33af7a8f3eeec022aebb478612f3602f47c6a33a5b4a24b" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-native-utils" version="1.9.20">
+ <artifact name="kotlin-native-utils-1.9.20.jar">
+ <sha256 value="b0f92bc9253a907f0ce285328643fe8a36c27ed494b5c5919ee09c2926d8e8d2" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-native-utils-1.9.20.pom">
+ <sha256 value="f3ef597806931b40ceed81d91b69e7cc7a7867ecced92db44bbfe6124776c5bc" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-project-model" version="1.9.20">
+ <artifact name="kotlin-project-model-1.9.20.jar">
+ <sha256 value="261a9b40e240e259ac359c16938002ecc6c08434a5c6e5e5bffee242a3c50218" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-project-model-1.9.20.pom">
+ <sha256 value="e0584405d5beef440a66b290d6e4c0dd43ea965dc35b770c1ea3264a87d2733c" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10">
+ <artifact name="kotlin-reflect-1.6.10.jar">
+ <sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-reflect-1.6.10.pom">
+ <sha256 value="57905524274a00ae028aaccc27283f6bc5925a934a046c1cc5d06c8ee4d6d5a9" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="1.9.20">
+ <artifact name="kotlin-script-runtime-1.9.20.jar">
+ <sha256 value="a26a6256a76f766ab8bacdb409b3f8c940d999712a8e88864252b678d66bab9e" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-script-runtime-1.9.20.pom">
+ <sha256 value="bcd227d293742c511a24bb2c56822d95f9638af5135b973e7cda3e8a76686579" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-scripting-common" version="1.9.20">
+ <artifact name="kotlin-scripting-common-1.9.20.jar">
+ <sha256 value="5aa08477cb73f7927413aec683a4aa3b3f99e87be0630255ce697452a1a42d65" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-scripting-common-1.9.20.pom">
+ <sha256 value="187e9f3b74bd56a083649818c7b9a40f8bc23f62012b08a7bc99c3fa886bff03" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-embeddable" version="1.9.20">
+ <artifact name="kotlin-scripting-compiler-embeddable-1.9.20.jar">
+ <sha256 value="2181dd0c4d52c6f696ad9f17934233790f4d68234b1418d6376fda7e5c374c4e" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-scripting-compiler-embeddable-1.9.20.pom">
+ <sha256 value="266128dbf435e3aebfee65357dde102c9ed929dfeb0901e56be78ca148c573f7" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-impl-embeddable" version="1.9.20">
+ <artifact name="kotlin-scripting-compiler-impl-embeddable-1.9.20.jar">
+ <sha256 value="dc9ab6f69c592ad1f1d2e2b994b97509d0ee09480bea6bc771eeeef3071eb817" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-scripting-compiler-impl-embeddable-1.9.20.pom">
+ <sha256 value="6dba3003e8cadcb14992dff134e866cab5905eab80e27023de41c26385838198" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-scripting-jvm" version="1.9.20">
+ <artifact name="kotlin-scripting-jvm-1.9.20.jar">
+ <sha256 value="809f73bdd4dd7766ae1ef2ced968896ce9c03d6a5fe6de6f6799778851f75bd3" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-scripting-jvm-1.9.20.pom">
+ <sha256 value="bf881c6e2fc6a37ea9d09ffe98ca072dfcda8dc8288fd89b24cadc60b14db75e" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.20">
+ <artifact name="kotlin-stdlib-1.9.20-all.jar">
+ <sha256 value="cec38bc3302e72a8aaf9cde436b5a9071ee0331e2ad05e84d8bb897334d7e9d4" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-stdlib-1.9.20.jar">
+ <sha256 value="28a35bcdff46d864f80f346a617e486284b208d17378c41900dfb1de95a90e6c" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-stdlib-1.9.20.module">
+ <sha256 value="dccaa5d315470fab3920502886bbb85f2da6c86102c65d9c04410544eedb2019" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.20">
+ <artifact name="kotlin-stdlib-common-1.9.20.module">
+ <sha256 value="858828bc5191b9e602affa14e01d66489dafb08c4c18d2faee3cbed7ba7d9992" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.9.10">
+ <artifact name="kotlin-stdlib-jdk7-1.9.10.jar">
+ <sha256 value="ac6361bf9ad1ed382c2103d9712c47cdec166232b4903ed596e8876b0681c9b7" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-stdlib-jdk7-1.9.10.pom">
+ <sha256 value="c7fa67c7961320b89d85a3ca59a2e18c2c65850845595dcae4b46af6945edcd5" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.9.10">
+ <artifact name="kotlin-stdlib-jdk8-1.9.10.jar">
+ <sha256 value="a4c74d94d64ce1abe53760fe0389dd941f6fc558d0dab35e47c085a11ec80f28" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-stdlib-jdk8-1.9.10.pom">
+ <sha256 value="5f4b94dd3065a7764c37fa15de2ad6d81f40d59f8cb33f17d181c6384fb7a72e" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-tooling-core" version="1.9.20">
+ <artifact name="kotlin-tooling-core-1.9.20.jar">
+ <sha256 value="8938eb97e36320daa3e6fb2a60fd2a05b232ff4a557173c5019f045b8832d9f4" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-tooling-core-1.9.20.pom">
+ <sha256 value="73daf491403928b15c4930957ae001acf04f6b3a4bc28fe4aa5777ef09709ed6" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-util-io" version="1.9.20">
+ <artifact name="kotlin-util-io-1.9.20.jar">
+ <sha256 value="c74fdaaae9d79fdf03327ee8738251e024b24b24d8b5377a1a429ac3b7f72cca" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-util-io-1.9.20.pom">
+ <sha256 value="adb1f435b30b22563aa0ae57d3f6396245988487a79eafae38e6676fd8202a89" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin" name="kotlin-util-klib" version="1.9.20">
+ <artifact name="kotlin-util-klib-1.9.20.jar">
+ <sha256 value="c453efe27a0632d16151bfdf0084a12b8cc019fd2cb342e2b8892accce4e91b2" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlin-util-klib-1.9.20.pom">
+ <sha256 value="56ebfa3e92f00a25f3c0bfedc9a631095e2862c8ff9ba598f93e691a409eedcd" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlin.jvm" name="org.jetbrains.kotlin.jvm.gradle.plugin" version="1.9.20">
+ <artifact name="org.jetbrains.kotlin.jvm.gradle.plugin-1.9.20.pom">
+ <sha256 value="8a66a9ecd1d2de82f70e280bbcf5eea0a54a7af63a36567ddea5ac1cf7f19b68" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0">
+ <artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
+ <sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="kotlinx-coroutines-core-jvm-1.5.0.module">
+ <sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="org.sonatype.oss" name="oss-parent" version="9">
+ <artifact name="oss-parent-9.pom">
+ <sha256 value="fb40265f982548212ff82e362e59732b2187ec6f0d80182885c14ef1f982827a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ </components>
+</verification-metadata>
diff --git a/development/bench-flame-diff/gradle/wrapper b/development/bench-flame-diff/gradle/wrapper
new file mode 120000
index 0000000..166ffc0
--- /dev/null
+++ b/development/bench-flame-diff/gradle/wrapper
@@ -0,0 +1 @@
+../../../gradle/wrapper
\ No newline at end of file
diff --git a/development/bench-flame-diff/gradlew b/development/bench-flame-diff/gradlew
new file mode 120000
index 0000000..d9f055c
--- /dev/null
+++ b/development/bench-flame-diff/gradlew
@@ -0,0 +1 @@
+../../playground-common/gradlew
\ No newline at end of file
diff --git a/development/bench-flame-diff/settings.gradle.kts b/development/bench-flame-diff/settings.gradle.kts
new file mode 100644
index 0000000..eee15d4
--- /dev/null
+++ b/development/bench-flame-diff/settings.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ // Apply the foojay-resolver plugin to allow automatic download of JDKs
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
+}
+
+rootProject.name = "bench-flame-diff"
+include("app")
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 6f6e4f7..083ad78 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -395,7 +395,7 @@
WARN: .*\/unzippedJvmSources\/androidx\/wear\/watchface\/ComplicationSlot\.kt:[0-9]+ Unable to find reference @param supportedTypes in DClass Builder\. Are you trying to refer to something not visible to users\?
WARN: .*\/unzippedJvmSources\/androidx\/wear\/watchface\/Renderer\.kt:[0-9]+ Link does not resolve for @throws Renderer\.GlesException in DClass GlesRenderer2\. Is it from a package that the containing file does not import\? Are docs inherited by an un-documented override function, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name, e\.g\. `@throws java\.io\.IOException under some conditions`\.
WARN: .*\/unzippedJvmSources\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt:[0-9]+ Unable to find reference @param copySelectedOptions in DClass UserStyle\. Are you trying to refer to something not visible to users\?
-WARN\: \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedMultiplatformSources\/nativeMain\/androidx\/lifecycle\/LifecycleRegistry\.native\.kt\:[0-9]+ Link does not resolve for \@throws IllegalStateException in DFunction addObserver\. Is it from a package that the containing file does not import\? Are docs inherited by an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\, e\.g\. \`\@throws java\.io\.IOException under some conditions\`\.
+WARN\: \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedMultiplatformSources\/nonJvmMain\/androidx\/lifecycle\/LifecycleRegistry\.nonJvm\.kt\:[0-9]+ Link does not resolve for \@throws IllegalStateException in DFunction addObserver\. Is it from a package that the containing file does not import\? Are docs inherited by an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\, e\.g\. \`\@throws java\.io\.IOException under some conditions\`\.
WARN: .*\/unzippedJvmSources\/androidx\/webkit\/CookieManagerCompat\.java:UnknownLine Missing @param tag for parameter `cookieManager` in DFunction getCookieInfo
WARN: .*\/unzippedJvmSources\/androidx\/webkit\/WebSettingsCompat\.java:[0-9]+ Missing @param tag for parameter `settings` in DFunction setSafeBrowsingEnabled
WARN: .*\/unzippedJvmSources\/androidx\/webkit\/WebSettingsCompat\.java:[0-9]+ Missing @param tag for parameter `settings` in DFunction setDisabledActionModeMenuItems
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
index fa6b521..3d98305 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
@@ -53,7 +53,8 @@
internal val jetbrainsRepositories = listOf(
"https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/",
"https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev",
- "https://maven.pkg.jetbrains.space/public/p/compose/dev"
+ "https://maven.pkg.jetbrains.space/public/p/compose/dev",
+ "https://maven.pkg.jetbrains.space/kotlin/p/dokka/test"
)
internal val gradlePluginPortalRepo = "https://plugins.gradle.org/m2/"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1445138..355f0fa 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -105,7 +105,7 @@
checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.4.3" }
+dackka = { module = "com.google.devsite:dackka", version = "1.5.0" }
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.3" }
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
index d2ceaf3..76efc66 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
@@ -1313,6 +1313,7 @@
}
}
) { _, renderer, surfaceView ->
+ commitCount.set(0)
val latch = CountDownLatch(1)
commitLatch.set(latch)
renderer.renderFrontBufferedLayer(Color.RED)
@@ -1321,8 +1322,8 @@
renderer.renderFrontBufferedLayer(Color.BLUE)
renderer.commit()
+ pendingCommitLatch.set(CountDownLatch(2))
latch.countDown()
- pendingCommitLatch.set(CountDownLatch(1))
assertTrue(pendingCommitLatch.get()!!.await(3000, TimeUnit.MILLISECONDS))
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
index e8edc46e..6d386fb 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
@@ -35,8 +35,6 @@
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
@@ -45,9 +43,8 @@
@CacheableTask
abstract class DexInspectorTask : DefaultTask() {
- @get:PathSensitive(PathSensitivity.NONE)
- @get:InputFile
- abstract val d8Executable: RegularFileProperty
+ @get:Classpath
+ abstract val d8Executable: ConfigurableFileCollection
@get:Classpath
@get:InputFile
@@ -76,7 +73,7 @@
output.parentFile.mkdirs()
val errorStream = ByteArrayOutputStream()
val executionResult = execOperations.javaexec {
- it.classpath(File(File(d8Executable.get().asFile.parentFile, "lib"), "d8.jar"))
+ it.classpath(d8Executable.files)
it.mainClass.set("com.android.tools.r8.D8")
it.allJvmArgs.add("-Xmx2G")
@@ -112,10 +109,6 @@
}
}
- fun setD8(sdkDir: File, toolsVersion: String) {
- d8Executable.set(File(sdkDir, "build-tools/$toolsVersion/d8"))
- }
-
fun setAndroidJar(sdkDir: File, compileSdk: String) {
// Preview SDK compileSdkVersions are prefixed with "android-", e.g. "android-S".
val platform = if (compileSdk.startsWith("android")) compileSdk else "android-$compileSdk"
@@ -144,7 +137,9 @@
val dex = tasks.register(variant.taskName("dexInspector"), DexInspectorTask::class.java) {
it.minSdkVersion = extension.defaultConfig.minSdk!!
- it.setD8(extension.sdkDirectory, extension.buildToolsVersion)
+ it.d8Executable.setFrom(
+ configurations.detachedConfiguration(dependencies.create("com.android.tools:r8:8.2.47"))
+ )
it.setAndroidJar(extension.sdkDirectory, extension.compileSdkVersion!!)
it.jars.from(jar.get().archiveFile)
it.outputFile.set(output)
diff --git a/leanback/leanback/src/main/res/values-ne/strings.xml b/leanback/leanback/src/main/res/values-ne/strings.xml
index 6e05863..444af5e 100644
--- a/leanback/leanback/src/main/res/values-ne/strings.xml
+++ b/leanback/leanback/src/main/res/values-ne/strings.xml
@@ -54,6 +54,6 @@
<string name="lb_guidedaction_finish_title" msgid="3330958750346333890">"पूरा गर्नुहोस्"</string>
<string name="lb_guidedaction_continue_title" msgid="893619591225519922">"जारी राख्नुहोस्"</string>
<string name="lb_media_player_error" msgid="3228326776757666747">"MediaPlayer को त्रुटिको कोड %1$d, यसको अतिरिक्त %2$d"</string>
- <string name="lb_onboarding_get_started" msgid="5549711907371161292">"सुरु गरौँ"</string>
+ <string name="lb_onboarding_get_started" msgid="5549711907371161292">"सुरु गर्नुहोस्"</string>
<string name="lb_onboarding_accessibility_next" msgid="2394451791544864917">"अर्को"</string>
</resources>
diff --git a/libraryversions.toml b/libraryversions.toml
index 067c73d..1f73d6d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
[versions]
ACTIVITY = "1.9.0-beta01"
-ANNOTATION = "1.8.0-alpha01"
+ANNOTATION = "1.8.0-alpha02"
ANNOTATION_EXPERIMENTAL = "1.4.0-rc01"
APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
APPACTIONS_INTERACTION = "1.0.0-alpha01"
@@ -64,7 +64,7 @@
ENTERPRISE = "1.1.0-rc01"
EXIFINTERFACE = "1.4.0-alpha01"
FRAGMENT = "1.7.0-beta01"
-FUTURES = "1.2.0-alpha02"
+FUTURES = "1.2.0-alpha03"
GLANCE = "1.1.0-alpha01"
GLANCE_PREVIEW = "1.0.0-alpha06"
GLANCE_TEMPLATE = "1.0.0-alpha06"
@@ -74,7 +74,7 @@
GRAPHICS_PATH = "1.0.0-rc01"
GRAPHICS_SHAPES = "1.0.0-beta01"
GRIDLAYOUT = "1.1.0-beta02"
-HEALTH_CONNECT = "1.1.0-alpha07"
+HEALTH_CONNECT = "1.1.0-alpha08"
HEALTH_SERVICES_CLIENT = "1.1.0-alpha02"
HEIFWRITER = "1.1.0-alpha03"
HILT = "1.2.0-rc01"
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index a92bda3..322c633 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -24,7 +24,7 @@
import androidx.build.PlatformIdentifier
import androidx.build.Publish
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
-import org.jetbrains.kotlin.konan.target.Family
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
plugins {
id("AndroidXPlugin")
@@ -53,9 +53,7 @@
}
}
- jvmMain {
- dependsOn(commonMain)
- }
+ jvmMain.dependsOn(commonMain)
jvmTest {
dependencies {
@@ -64,33 +62,17 @@
}
}
- nativeMain {
+ nonJvmMain {
dependsOn(commonMain)
dependencies {
implementation(libs.atomicFu)
}
}
- darwinMain {
- dependsOn(nativeMain)
- }
- linuxMain {
- dependsOn(nativeMain)
- }
targets.all { target ->
- if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+ if (target.platformType !in [KotlinPlatformType.jvm, KotlinPlatformType.common]) {
target.compilations["main"].defaultSourceSet {
- def konanTargetFamily = target.konanTarget.family
- if (konanTargetFamily == Family.OSX || konanTargetFamily == Family.IOS) {
- dependsOn(darwinMain)
- } else if (konanTargetFamily == Family.LINUX) {
- dependsOn(linuxMain)
- } else {
- throw new GradleException("unknown native target ${target}")
- }
- }
- target.compilations["test"].defaultSourceSet {
- dependsOn(nativeTest)
+ dependsOn(nonJvmMain)
}
}
}
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt b/lifecycle/lifecycle-common/src/nonJvmMain/kotlin/androidx/lifecycle/Lifecycle.nonJvm.kt
similarity index 92%
rename from lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
rename to lifecycle/lifecycle-common/src/nonJvmMain/kotlin/androidx/lifecycle/Lifecycle.nonJvm.kt
index f992081..148f8f7 100644
--- a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
+++ b/lifecycle/lifecycle-common/src/nonJvmMain/kotlin/androidx/lifecycle/Lifecycle.nonJvm.kt
@@ -15,9 +15,11 @@
*/
package androidx.lifecycle
+import androidx.annotation.RestrictTo
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public actual class AtomicReference<V> actual constructor(value: V) {
private val delegate = atomic(value)
public actual fun get(): V = delegate.value
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt b/lifecycle/lifecycle-common/src/nonJvmMain/kotlin/androidx/lifecycle/Lifecycling.nonJvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt
rename to lifecycle/lifecycle-common/src/nonJvmMain/kotlin/androidx/lifecycle/Lifecycling.nonJvm.kt
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index bbbc6a5..f16a328 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -10,7 +10,6 @@
import androidx.build.Publish
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.konan.target.Family
plugins {
id("AndroidXPlugin")
@@ -86,8 +85,12 @@
}
}
- nativeMain {
+ nonJvmMain {
dependsOn(commonMain)
+ }
+
+ nativeMain {
+ dependsOn(nonJvmMain)
// Required for WeakReference usage
languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi")
@@ -96,30 +99,11 @@
nativeTest {
dependsOn(commonTest)
}
- darwinMain {
- dependsOn(nativeMain)
-
- // Required for WeakReference usage
- languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi")
- }
- linuxMain {
- dependsOn(nativeMain)
-
- // Required for WeakReference usage
- languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi")
- }
targets.all { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet {
- def konanTargetFamily = target.konanTarget.family
- if (konanTargetFamily == Family.OSX || konanTargetFamily == Family.IOS) {
- dependsOn(darwinMain)
- } else if (konanTargetFamily == Family.LINUX) {
- dependsOn(linuxMain)
- } else {
- throw new GradleException("unknown native target ${target}")
- }
+ dependsOn(nativeMain)
// Required for WeakReference usage
languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi")
diff --git a/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
index 978ee7c..90c6aa6 100644
--- a/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
+++ b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
@@ -15,342 +15,5 @@
*/
package androidx.lifecycle
-import androidx.annotation.VisibleForTesting
-import kotlin.native.ref.WeakReference
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/**
- * An implementation of [Lifecycle] that can handle multiple observers.
- *
- * It is used by Fragments and Support Library Activities. You can also directly use it if you have
- * a custom LifecycleOwner.
- */
-public actual open class LifecycleRegistry private constructor(
- provider: LifecycleOwner,
- private val enforceMainThread: Boolean
-) : Lifecycle() {
- /**
- * Invariant: at any moment of time for observer1 & observer2:
- * if addition_order(observer1) < addition_order(observer2), then
- * state(observer1) >= state(observer2),
- */
- private var observerMap = linkedMapOf<LifecycleObserver, ObserverWithState>()
-
- /**
- * Current state
- */
- private var state: State = State.INITIALIZED
-
- /**
- * The provider that owns this Lifecycle.
- * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
- * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
- * because it keeps strong references on all other listeners, so you'll leak all of them as
- * well.
- */
- private val lifecycleOwner: WeakReference<LifecycleOwner>
- private var addingObserverCounter = 0
- private var handlingEvent = false
- private var newEventOccurred = false
-
- // we have to keep it for cases:
- // void onStart() {
- // mRegistry.removeObserver(this);
- // mRegistry.add(newObserver);
- // }
- // newObserver should be brought only to CREATED state during the execution of
- // this onStart method. our invariant with observerMap doesn't help, because parent observer
- // is no longer in the map.
- private var parentStates = ArrayList<State>()
-
- /**
- * Creates a new LifecycleRegistry for the given provider.
- *
- * You should usually create this inside your LifecycleOwner class's constructor and hold
- * onto the same instance.
- *
- * @param provider The owner LifecycleOwner
- */
- public actual constructor(provider: LifecycleOwner) : this(provider, true)
-
- init {
- lifecycleOwner = WeakReference(provider)
- }
-
- actual override var currentState: State
- get() = state
- /**
- * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
- *
- * @param state new state
- */
- set(state) {
- enforceMainThreadIfNeeded("setCurrentState")
- moveToState(state)
- }
-
- private val _currentStateFlow: MutableStateFlow<State> = MutableStateFlow(State.INITIALIZED)
- override val currentStateFlow: StateFlow<State>
- get() = _currentStateFlow.asStateFlow()
-
- /**
- * Sets the current state and notifies the observers.
- *
- * Note that if the `currentState` is the same state as the last call to this method,
- * calling this method has no effect.
- *
- * @param event The event that was received
- */
- public actual open fun handleLifecycleEvent(event: Event) {
- enforceMainThreadIfNeeded("handleLifecycleEvent")
- moveToState(event.targetState)
- }
-
- private fun moveToState(next: State) {
- if (state == next) {
- return
- }
- check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
- "State must be at least CREATED to move to $next, but was $state in component " +
- "${lifecycleOwner.get()}"
- }
- state = next
- if (handlingEvent || addingObserverCounter != 0) {
- newEventOccurred = true
- // we will figure out what to do on upper level.
- return
- }
- handlingEvent = true
- sync()
- handlingEvent = false
- if (state == State.DESTROYED) {
- observerMap = linkedMapOf()
- }
- }
-
- private val isSynced: Boolean
- get() {
- if (observerMap.isEmpty()) {
- return true
- }
- val eldestObserverState = observerMap.values.first().state
- val newestObserverState = observerMap.values.last().state
- return eldestObserverState == newestObserverState && state == newestObserverState
- }
-
- private fun calculateTargetState(observer: LifecycleObserver): State {
- val siblingState = observerMap.keys.toList().let {
- val index = it.indexOf(observer)
- if (index > 0) observerMap[it[index - 1]]?.state else null
- }
- val parentState =
- if (parentStates.isNotEmpty()) parentStates[parentStates.size - 1] else null
- return min(min(state, siblingState), parentState)
- }
-
- /**
- * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
- * state.
- *
- * The given observer will be brought to the current state of the LifecycleOwner.
- * For example, if the LifecycleOwner is in [Lifecycle.State.STARTED] state, the given observer
- * will receive [Lifecycle.Event.ON_CREATE], [Lifecycle.Event.ON_START] events.
- *
- * @param observer The observer to notify.
- *
- * @throws IllegalStateException if no event up from observer's initial state
- */
- override fun addObserver(observer: LifecycleObserver) {
- enforceMainThreadIfNeeded("addObserver")
- val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
- val statefulObserver = ObserverWithState(observer, initialState)
- val previous = observerMap.put(observer, statefulObserver)
- if (previous != null) {
- return
- }
- val lifecycleOwner = lifecycleOwner.get()
- ?: // it is null we should be destroyed. Fallback quickly
- return
- val isReentrance = addingObserverCounter != 0 || handlingEvent
- var targetState = calculateTargetState(observer)
- addingObserverCounter++
- while (statefulObserver.state < targetState && observerMap.contains(observer)) {
- pushParentState(statefulObserver.state)
- val event = Event.upFrom(statefulObserver.state)
- ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
- statefulObserver.dispatchEvent(lifecycleOwner, event)
- popParentState()
- // mState / subling may have been changed recalculate
- targetState = calculateTargetState(observer)
- }
- if (!isReentrance) {
- // we do sync only on the top level.
- sync()
- }
- addingObserverCounter--
- }
-
- private fun popParentState() {
- parentStates.removeAt(parentStates.size - 1)
- }
-
- private fun pushParentState(state: State) {
- parentStates.add(state)
- }
-
- override fun removeObserver(observer: LifecycleObserver) {
- enforceMainThreadIfNeeded("removeObserver")
- // we consciously decided not to send destruction events here in opposition to addObserver.
- // Our reasons for that:
- // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
- // actually occurred but earlier.
- // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
- // event. If removeObserver method sends destruction events, then a clean up routine becomes
- // more cumbersome. More specific example of that is: your LifecycleObserver listens for
- // a web connection, in the usual routine in OnStop method you report to a server that a
- // session has just ended and you close the connection. Now let's assume now that you
- // lost an internet and as a result you removed this observer. If you get destruction
- // events in removeObserver, you should have a special case in your onStop method that
- // checks if your web connection died and you shouldn't try to report anything to a server.
- observerMap.remove(observer)
- }
-
- /**
- * The number of observers.
- *
- * @return The number of observers.
- */
- public actual open val observerCount: Int
- get() {
- enforceMainThreadIfNeeded("getObserverCount")
- return observerMap.size
- }
-
- private fun forwardPass(lifecycleOwner: LifecycleOwner) {
- forEachObserverWithAdditions { key, observer ->
- while (observer.state < state && !newEventOccurred && observerMap.contains(key)) {
- pushParentState(observer.state)
- val event = Event.upFrom(observer.state)
- ?: throw IllegalStateException("no event up from ${observer.state}")
- observer.dispatchEvent(lifecycleOwner, event)
- popParentState()
- }
- }
- }
-
- private fun backwardPass(lifecycleOwner: LifecycleOwner) {
- forEachObserverReversed { key, observer ->
- while (observer.state > state && !newEventOccurred && observerMap.contains(key)) {
- val event = Event.downFrom(observer.state)
- ?: throw IllegalStateException("no event down from ${observer.state}")
- pushParentState(event.targetState)
- observer.dispatchEvent(lifecycleOwner, event)
- popParentState()
- }
- }
- }
-
- private inline fun forEachObserverWithAdditions(
- block: (LifecycleObserver, ObserverWithState) -> Unit
- ) {
- val visited = mutableSetOf<LifecycleObserver>()
- while (!newEventOccurred) {
- val keys = observerMap.keys.filter { it !in visited }
- if (keys.isEmpty()) {
- break
- }
- for (key in keys) {
- if (newEventOccurred) {
- break
- }
- val value = observerMap[key] ?: continue
- block(key, value)
- visited.add(key)
- }
- }
- }
-
- private inline fun forEachObserverReversed(
- block: (LifecycleObserver, ObserverWithState) -> Unit
- ) {
- val keys = observerMap.keys.reversed()
- for (key in keys) {
- if (newEventOccurred) {
- break
- }
- val value = observerMap[key] ?: continue
- block(key, value)
- }
- }
-
- // happens only on the top of stack (never in reentrance),
- // so it doesn't have to take in account parents
- private fun sync() {
- val lifecycleOwner = lifecycleOwner.get()
- ?: throw IllegalStateException(
- "LifecycleOwner of this LifecycleRegistry is already " +
- "garbage collected. It is too late to change lifecycle state."
- )
- while (!isSynced) {
- newEventOccurred = false
- if (state < observerMap.values.first().state) {
- backwardPass(lifecycleOwner)
- }
- val newest = observerMap.values.lastOrNull()
- if (!newEventOccurred && newest != null && state > newest.state) {
- forwardPass(lifecycleOwner)
- }
- }
- newEventOccurred = false
- _currentStateFlow.value = currentState
- }
-
- private fun enforceMainThreadIfNeeded(methodName: String) {
- if (enforceMainThread) {
- check(isMainThread()) {
- ("Method $methodName must be called on the main thread")
- }
- }
- }
-
- internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
- var state: State
- private var lifecycleObserver: LifecycleEventObserver
-
- init {
- lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
- state = initialState
- }
-
- fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
- val newState = event.targetState
- state = min(state, newState)
- lifecycleObserver.onStateChanged(owner!!, event)
- state = newState
- }
- }
-
- public actual companion object {
- /**
- * Creates a new LifecycleRegistry for the given provider, that doesn't check
- * that its methods are called on the threads other than main.
- *
- * LifecycleRegistry is not synchronized: if multiple threads access this `LifecycleRegistry`, it must be synchronized externally.
- *
- * Another possible use-case for this method is JVM testing, when main thread is not present.
- */
- @VisibleForTesting
- public actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
- return LifecycleRegistry(owner, false)
- }
-
- internal fun min(state1: State, state2: State?): State {
- return if ((state2 != null) && (state2 < state1)) state2 else state1
- }
- }
-}
-
-private fun isMainThread(): Boolean =
+internal actual fun isMainThread(): Boolean =
MainDispatcherChecker.isMainDispatcherThread()
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/DeleteMe.kt b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/WeakReference.native.kt
similarity index 64%
rename from wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/DeleteMe.kt
rename to lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/WeakReference.native.kt
index 8f2cad2..9b16c02 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/DeleteMe.kt
+++ b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/WeakReference.native.kt
@@ -1,5 +1,5 @@
-package androidx.wear.protolayout.material/*
- * Copyright 2022 The Android Open Source Project
+/*
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,4 +14,11 @@
* limitations under the License.
*/
-// This file exists to trick AGP/lint to work around b/234865137
+package androidx.lifecycle
+
+internal actual class WeakReference<T : Any> actual constructor(
+ reference: T
+) {
+ private val kotlinNativeReference = kotlin.native.ref.WeakReference(reference)
+ actual fun get(): T? = kotlinNativeReference.get()
+}
diff --git a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt
new file mode 100644
index 0000000..2a7e188
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * An implementation of [Lifecycle] that can handle multiple observers.
+ *
+ * It is used by Fragments and Support Library Activities. You can also directly use it if you have
+ * a custom LifecycleOwner.
+ */
+public actual open class LifecycleRegistry private constructor(
+ provider: LifecycleOwner,
+ private val enforceMainThread: Boolean
+) : Lifecycle() {
+ /**
+ * Invariant: at any moment of time for observer1 & observer2:
+ * if addition_order(observer1) < addition_order(observer2), then
+ * state(observer1) >= state(observer2),
+ */
+ private var observerMap = linkedMapOf<LifecycleObserver, ObserverWithState>()
+
+ /**
+ * Current state
+ */
+ private var state: State = State.INITIALIZED
+
+ /**
+ * The provider that owns this Lifecycle.
+ * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
+ * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
+ * because it keeps strong references on all other listeners, so you'll leak all of them as
+ * well.
+ */
+ private val lifecycleOwner: WeakReference<LifecycleOwner>
+ private var addingObserverCounter = 0
+ private var handlingEvent = false
+ private var newEventOccurred = false
+
+ // we have to keep it for cases:
+ // void onStart() {
+ // mRegistry.removeObserver(this);
+ // mRegistry.add(newObserver);
+ // }
+ // newObserver should be brought only to CREATED state during the execution of
+ // this onStart method. our invariant with observerMap doesn't help, because parent observer
+ // is no longer in the map.
+ private var parentStates = ArrayList<State>()
+
+ /**
+ * Creates a new LifecycleRegistry for the given provider.
+ *
+ * You should usually create this inside your LifecycleOwner class's constructor and hold
+ * onto the same instance.
+ *
+ * @param provider The owner LifecycleOwner
+ */
+ public actual constructor(provider: LifecycleOwner) : this(provider, true)
+
+ init {
+ lifecycleOwner = WeakReference(provider)
+ }
+
+ actual override var currentState: State
+ get() = state
+ /**
+ * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+ *
+ * @param state new state
+ */
+ set(state) {
+ enforceMainThreadIfNeeded("setCurrentState")
+ moveToState(state)
+ }
+
+ private val _currentStateFlow: MutableStateFlow<State> = MutableStateFlow(State.INITIALIZED)
+ override val currentStateFlow: StateFlow<State>
+ get() = _currentStateFlow.asStateFlow()
+
+ /**
+ * Sets the current state and notifies the observers.
+ *
+ * Note that if the `currentState` is the same state as the last call to this method,
+ * calling this method has no effect.
+ *
+ * @param event The event that was received
+ */
+ public actual open fun handleLifecycleEvent(event: Event) {
+ enforceMainThreadIfNeeded("handleLifecycleEvent")
+ moveToState(event.targetState)
+ }
+
+ private fun moveToState(next: State) {
+ if (state == next) {
+ return
+ }
+ check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
+ "State must be at least CREATED to move to $next, but was $state in component " +
+ "${lifecycleOwner.get()}"
+ }
+ state = next
+ if (handlingEvent || addingObserverCounter != 0) {
+ newEventOccurred = true
+ // we will figure out what to do on upper level.
+ return
+ }
+ handlingEvent = true
+ sync()
+ handlingEvent = false
+ if (state == State.DESTROYED) {
+ observerMap = linkedMapOf()
+ }
+ }
+
+ private val isSynced: Boolean
+ get() {
+ if (observerMap.isEmpty()) {
+ return true
+ }
+ val eldestObserverState = observerMap.values.first().state
+ val newestObserverState = observerMap.values.last().state
+ return eldestObserverState == newestObserverState && state == newestObserverState
+ }
+
+ private fun calculateTargetState(observer: LifecycleObserver): State {
+ val siblingState = observerMap.keys.toList().let {
+ val index = it.indexOf(observer)
+ if (index > 0) observerMap[it[index - 1]]?.state else null
+ }
+ val parentState =
+ if (parentStates.isNotEmpty()) parentStates[parentStates.size - 1] else null
+ return min(min(state, siblingState), parentState)
+ }
+
+ /**
+ * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+ * state.
+ *
+ * The given observer will be brought to the current state of the LifecycleOwner.
+ * For example, if the LifecycleOwner is in [Lifecycle.State.STARTED] state, the given observer
+ * will receive [Lifecycle.Event.ON_CREATE], [Lifecycle.Event.ON_START] events.
+ *
+ * @param observer The observer to notify.
+ *
+ * @throws IllegalStateException if no event up from observer's initial state
+ */
+ override fun addObserver(observer: LifecycleObserver) {
+ enforceMainThreadIfNeeded("addObserver")
+ val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
+ val statefulObserver = ObserverWithState(observer, initialState)
+ val previous = observerMap.put(observer, statefulObserver)
+ if (previous != null) {
+ return
+ }
+ val lifecycleOwner = lifecycleOwner.get()
+ ?: // it is null we should be destroyed. Fallback quickly
+ return
+ val isReentrance = addingObserverCounter != 0 || handlingEvent
+ var targetState = calculateTargetState(observer)
+ addingObserverCounter++
+ while (statefulObserver.state < targetState && observerMap.contains(observer)) {
+ pushParentState(statefulObserver.state)
+ val event = Event.upFrom(statefulObserver.state)
+ ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
+ statefulObserver.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ // mState / subling may have been changed recalculate
+ targetState = calculateTargetState(observer)
+ }
+ if (!isReentrance) {
+ // we do sync only on the top level.
+ sync()
+ }
+ addingObserverCounter--
+ }
+
+ private fun popParentState() {
+ parentStates.removeAt(parentStates.size - 1)
+ }
+
+ private fun pushParentState(state: State) {
+ parentStates.add(state)
+ }
+
+ override fun removeObserver(observer: LifecycleObserver) {
+ enforceMainThreadIfNeeded("removeObserver")
+ // we consciously decided not to send destruction events here in opposition to addObserver.
+ // Our reasons for that:
+ // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
+ // actually occurred but earlier.
+ // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
+ // event. If removeObserver method sends destruction events, then a clean up routine becomes
+ // more cumbersome. More specific example of that is: your LifecycleObserver listens for
+ // a web connection, in the usual routine in OnStop method you report to a server that a
+ // session has just ended and you close the connection. Now let's assume now that you
+ // lost an internet and as a result you removed this observer. If you get destruction
+ // events in removeObserver, you should have a special case in your onStop method that
+ // checks if your web connection died and you shouldn't try to report anything to a server.
+ observerMap.remove(observer)
+ }
+
+ /**
+ * The number of observers.
+ *
+ * @return The number of observers.
+ */
+ public actual open val observerCount: Int
+ get() {
+ enforceMainThreadIfNeeded("getObserverCount")
+ return observerMap.size
+ }
+
+ private fun forwardPass(lifecycleOwner: LifecycleOwner) {
+ forEachObserverWithAdditions { key, observer ->
+ while (observer.state < state && !newEventOccurred && observerMap.contains(key)) {
+ pushParentState(observer.state)
+ val event = Event.upFrom(observer.state)
+ ?: throw IllegalStateException("no event up from ${observer.state}")
+ observer.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ }
+ }
+ }
+
+ private fun backwardPass(lifecycleOwner: LifecycleOwner) {
+ forEachObserverReversed { key, observer ->
+ while (observer.state > state && !newEventOccurred && observerMap.contains(key)) {
+ val event = Event.downFrom(observer.state)
+ ?: throw IllegalStateException("no event down from ${observer.state}")
+ pushParentState(event.targetState)
+ observer.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ }
+ }
+ }
+
+ private inline fun forEachObserverWithAdditions(
+ block: (LifecycleObserver, ObserverWithState) -> Unit
+ ) {
+ val visited = mutableSetOf<LifecycleObserver>()
+ while (!newEventOccurred) {
+ val keys = observerMap.keys.filter { it !in visited }
+ if (keys.isEmpty()) {
+ break
+ }
+ for (key in keys) {
+ if (newEventOccurred) {
+ break
+ }
+ val value = observerMap[key] ?: continue
+ block(key, value)
+ visited.add(key)
+ }
+ }
+ }
+
+ private inline fun forEachObserverReversed(
+ block: (LifecycleObserver, ObserverWithState) -> Unit
+ ) {
+ val keys = observerMap.keys.reversed()
+ for (key in keys) {
+ if (newEventOccurred) {
+ break
+ }
+ val value = observerMap[key] ?: continue
+ block(key, value)
+ }
+ }
+
+ // happens only on the top of stack (never in reentrance),
+ // so it doesn't have to take in account parents
+ private fun sync() {
+ val lifecycleOwner = lifecycleOwner.get()
+ ?: throw IllegalStateException(
+ "LifecycleOwner of this LifecycleRegistry is already " +
+ "garbage collected. It is too late to change lifecycle state."
+ )
+ while (!isSynced) {
+ newEventOccurred = false
+ if (state < observerMap.values.first().state) {
+ backwardPass(lifecycleOwner)
+ }
+ val newest = observerMap.values.lastOrNull()
+ if (!newEventOccurred && newest != null && state > newest.state) {
+ forwardPass(lifecycleOwner)
+ }
+ }
+ newEventOccurred = false
+ _currentStateFlow.value = currentState
+ }
+
+ private fun enforceMainThreadIfNeeded(methodName: String) {
+ if (enforceMainThread) {
+ check(isMainThread()) {
+ ("Method $methodName must be called on the main thread")
+ }
+ }
+ }
+
+ internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
+ var state: State
+ private var lifecycleObserver: LifecycleEventObserver
+
+ init {
+ lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
+ state = initialState
+ }
+
+ fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
+ val newState = event.targetState
+ state = min(state, newState)
+ lifecycleObserver.onStateChanged(owner!!, event)
+ state = newState
+ }
+ }
+
+ public actual companion object {
+ /**
+ * Creates a new LifecycleRegistry for the given provider, that doesn't check
+ * that its methods are called on the threads other than main.
+ *
+ * LifecycleRegistry is not synchronized: if multiple threads access this
+ * `LifecycleRegistry`, it must be synchronized externally.
+ *
+ * Another possible use-case for this method is JVM testing, when main thread is not present.
+ */
+ @VisibleForTesting
+ public actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
+ return LifecycleRegistry(owner, false)
+ }
+
+ internal fun min(state1: State, state2: State?): State {
+ return if ((state2 != null) && (state2 < state1)) state2 else state1
+ }
+ }
+}
+
+internal expect fun isMainThread(): Boolean
diff --git a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/WeakReference.nonJvm.kt b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/WeakReference.nonJvm.kt
new file mode 100644
index 0000000..62417f8
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/WeakReference.nonJvm.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+/**
+ * Class WeakReference encapsulates weak reference to an object, which could be used to either
+ * retrieve a strong reference to an object, or return null, if object was already destroyed by
+ * the memory manager.
+ */
+internal expect class WeakReference<T : Any>(reference: T) {
+ fun get(): T?
+}
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 92ade99..b356f96 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -24,7 +24,7 @@
}
public final class CombinedLoadStatesKt {
- method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates>);
+ method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates?>);
}
public abstract class DataSource<Key, Value> {
@@ -410,7 +410,7 @@
method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, T item);
method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T?,? super T?,? extends R?> generator);
- method @CheckResult @kotlin.jvm.JvmSynthetic public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, kotlin.jvm.functions.Function3<? super T?,? super T?,? super kotlin.coroutines.Continuation<? super R>?,?> generator);
+ method @CheckResult @kotlin.jvm.JvmSynthetic public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, kotlin.jvm.functions.Function3<? super T?,? super T?,? super kotlin.coroutines.Continuation<? super R?>,?> generator);
method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T?,? super T?,? extends R?> generator);
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method @CheckResult @kotlin.jvm.JvmSynthetic public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super R>,?> transform);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 92ade99..b356f96 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -24,7 +24,7 @@
}
public final class CombinedLoadStatesKt {
- method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates>);
+ method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates?>);
}
public abstract class DataSource<Key, Value> {
@@ -410,7 +410,7 @@
method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, T item);
method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T?,? super T?,? extends R?> generator);
- method @CheckResult @kotlin.jvm.JvmSynthetic public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, kotlin.jvm.functions.Function3<? super T?,? super T?,? super kotlin.coroutines.Continuation<? super R>?,?> generator);
+ method @CheckResult @kotlin.jvm.JvmSynthetic public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, kotlin.jvm.functions.Function3<? super T?,? super T?,? super kotlin.coroutines.Continuation<? super R?>,?> generator);
method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T?,? super T?,? extends R?> generator);
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method @CheckResult @kotlin.jvm.JvmSynthetic public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super R>,?> transform);
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 44bcd15..026cb0a 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -30,12 +30,12 @@
@VisibleForTesting public final class TestPager<Key, Value> {
ctor public TestPager(androidx.paging.PagingConfig config, androidx.paging.PagingSource<Key,Value> pagingSource);
- method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
- method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
+ method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>?>);
+ method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>?>);
method public suspend Object? getPages(kotlin.coroutines.Continuation<java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
method public suspend Object? getPagingState(int anchorPosition, kotlin.coroutines.Continuation<androidx.paging.PagingState<Key,Value>>);
method public suspend Object? getPagingState(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> anchorPositionLookup, kotlin.coroutines.Continuation<androidx.paging.PagingState<Key,Value>>);
- method public suspend Object? prepend(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
+ method public suspend Object? prepend(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>?>);
method public suspend Object? refresh(optional Key? initialKey, kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
}
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 44bcd15..026cb0a 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -30,12 +30,12 @@
@VisibleForTesting public final class TestPager<Key, Value> {
ctor public TestPager(androidx.paging.PagingConfig config, androidx.paging.PagingSource<Key,Value> pagingSource);
- method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
- method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
+ method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>?>);
+ method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>?>);
method public suspend Object? getPages(kotlin.coroutines.Continuation<java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
method public suspend Object? getPagingState(int anchorPosition, kotlin.coroutines.Continuation<androidx.paging.PagingState<Key,Value>>);
method public suspend Object? getPagingState(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> anchorPositionLookup, kotlin.coroutines.Continuation<androidx.paging.PagingState<Key,Value>>);
- method public suspend Object? prepend(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
+ method public suspend Object? prepend(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>?>);
method public suspend Object? refresh(optional Key? initialKey, kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
}
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index db5988f..792f76f 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
# Disable docs
androidx.enableDocumentation=false
androidx.playground.snapshotBuildId=11349412
-androidx.playground.metalavaBuildId=11524763
+androidx.playground.metalavaBuildId=11549526
androidx.studio.type=playground
\ No newline at end of file
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/DeleteMe.kt b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/security/security-identity-credential/src/main/java/androidx/security/DeleteMe.kt b/security/security-identity-credential/src/main/java/androidx/security/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/security/security-identity-credential/src/main/java/androidx/security/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/settings.gradle b/settings.gradle
index 0b8888d..b8dd445f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -504,6 +504,7 @@
includeProject(":compose:foundation:foundation-layout:foundation-layout-samples", "compose/foundation/foundation-layout/samples", [BuildType.COMPOSE])
includeProject(":compose:foundation:foundation-lint", [BuildType.COMPOSE])
includeProject(":compose:foundation:foundation:integration-tests:foundation-demos", [BuildType.COMPOSE])
+includeProject(":compose:foundation:foundation:integration-tests:lazy-tests", [BuildType.COMPOSE])
includeProject(":compose:foundation:foundation:foundation-samples", "compose/foundation/foundation/samples", [BuildType.COMPOSE])
includeProject(":compose:integration-tests", [BuildType.COMPOSE])
includeProject(":compose:integration-tests:demos", [BuildType.COMPOSE])
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTestUtil.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTestUtil.kt
index 78fa81c..94bb1db 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTestUtil.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTestUtil.kt
@@ -25,8 +25,10 @@
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.TargetTracking
+import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.runOnUiThreadRethrow
+import androidx.testutils.withActivity
import androidx.transition.test.R
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -51,6 +53,12 @@
return ret
}
+inline fun <reified A : FragmentActivity> ActivityScenario<A>.executePendingTransactions(
+ fm: FragmentManager = withActivity { supportFragmentManager }
+) {
+ onActivity { fm.executePendingTransactions() }
+}
+
@Suppress("DEPRECATION")
fun androidx.test.rule.ActivityTestRule<out FragmentActivity>.popBackStackImmediate(): Boolean {
val instrumentation = InstrumentationRegistry.getInstrumentation()
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
index a2445a9..1d3f0bf 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
@@ -33,7 +33,6 @@
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,7 +48,6 @@
FragmentTransitionTestActivity::class.java
)
- @Ignore // b/324309532
@Test
fun replaceOperationWithTransitionsThenGestureBack() {
val fm1 = activityRule.activity.supportFragmentManager
@@ -126,78 +124,83 @@
@Test
fun replaceOperationWithTransitionsThenBackCancelled() {
- val fm1 = activityRule.activity.supportFragmentManager
-
- var startedEnter = false
- val fragment1 = TransitionFragment(R.layout.scene1)
- fragment1.setReenterTransition(Fade().apply {
- duration = 300
- addListener(object : TransitionListenerAdapter() {
- override fun onTransitionStart(transition: Transition) {
- startedEnter = true
- }
+ withUse(ActivityScenario.launch(FragmentTransitionTestActivity::class.java)) {
+ val fm1 = withActivity {
+ supportFragmentManager
+ }
+ var startedEnter = false
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ fragment1.setReenterTransition(Fade().apply {
+ duration = 300
+ addListener(object : TransitionListenerAdapter() {
+ override fun onTransitionStart(transition: Transition) {
+ startedEnter = true
+ }
+ })
})
- })
- fm1.beginTransaction()
- .replace(R.id.fragmentContainer, fragment1, "1")
- .setReorderingAllowed(true)
- .addToBackStack(null)
- .commit()
- activityRule.waitForExecution()
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1, "1")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
- val startedExitCountDownLatch = CountDownLatch(1)
- val fragment2 = TransitionFragment()
- fragment2.setReturnTransition(Fade().apply {
- duration = 300
- addListener(object : TransitionListenerAdapter() {
- override fun onTransitionStart(transition: Transition) {
- startedExitCountDownLatch.countDown()
- }
+ val startedExitCountDownLatch = CountDownLatch(1)
+ val fragment2 = TransitionFragment()
+ fragment2.setReturnTransition(Fade().apply {
+ duration = 300
+ addListener(object : TransitionListenerAdapter() {
+ override fun onTransitionStart(transition: Transition) {
+ startedExitCountDownLatch.countDown()
+ }
+ })
})
- })
- fm1.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2, "2")
- .setReorderingAllowed(true)
- .addToBackStack(null)
- .commit()
- activityRule.executePendingTransactions()
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2, "2")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ executePendingTransactions()
- fragment1.waitForTransition()
- fragment2.waitForTransition()
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
- val dispatcher = activityRule.activity.onBackPressedDispatcher
- activityRule.runOnUiThread {
- dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+ val dispatcher = activityRule.activity.onBackPressedDispatcher
+ activityRule.runOnUiThread {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ }
+ executePendingTransactions(fm1)
+
+ activityRule.runOnUiThread {
+ dispatcher.dispatchOnBackProgressed(
+ BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+ )
+ }
+ executePendingTransactions(fm1)
+
+ assertThat(startedEnter).isTrue()
+ assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+ activityRule.runOnUiThread {
+ dispatcher.dispatchOnBackCancelled()
+ }
+ executePendingTransactions(fm1)
+
+ // The executePendingTransaction will end the transition so we should not wait here.
+ fragment1.waitForNoTransition()
+
+ assertThat(fragment2.isAdded).isTrue()
+ assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
+
+ // Make sure the original fragment was correctly readded to the container
+ assertThat(fragment2.requireView()).isNotNull()
}
- activityRule.executePendingTransactions(fm1)
-
- activityRule.runOnUiThread {
- dispatcher.dispatchOnBackProgressed(
- BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
- )
- }
- activityRule.executePendingTransactions(fm1)
-
- assertThat(startedEnter).isTrue()
- assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-
- activityRule.runOnUiThread {
- dispatcher.dispatchOnBackCancelled()
- }
- activityRule.executePendingTransactions(fm1)
-
- fragment1.waitForTransition()
-
- assertThat(fragment2.isAdded).isTrue()
- assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
-
- // Make sure the original fragment was correctly readded to the container
- assertThat(fragment2.requireView()).isNotNull()
}
- @Ignore // b/324309532
@Test
fun replaceOperationWithTransitionsThenGestureBackTwice() {
val fm1 = activityRule.activity.supportFragmentManager
@@ -339,7 +342,6 @@
assertThat(fragment1.requireView().parent).isNotNull()
}
- @Ignore // b/300694860
@Test
fun replaceOperationWithTransitionsThenOnBackPressedTwice() {
val fm1 = activityRule.activity.supportFragmentManager
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
index 4a76dc8..91efb1c 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
@@ -41,7 +41,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.transition.test.R;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -64,7 +63,6 @@
mReorderingAllowed = reorderingAllowed;
}
- @Ignore // b/326237469
@Test
public void preconditions() {
final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
@@ -79,7 +77,6 @@
assertNotNull(fragment2.mBlue);
}
- @Ignore // b/326237469
@Test
public void nonSharedTransition() {
final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
@@ -101,7 +98,6 @@
.onTransitionStart(any(Transition.class));
}
- @Ignore // b/326237469
@Test
public void sharedTransition() {
final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionTest.kt
index d5c1329..678b18e 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionTest.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionTest.kt
@@ -36,7 +36,6 @@
import org.junit.After
import org.junit.Assert.fail
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -77,7 +76,6 @@
fragmentManager.removeOnBackStackChangedListener(onBackStackChangedListener)
}
- @Ignore // b/326237469
// Test that normal view transitions (enter, exit, reenter, return) run with
// a single fragment.
@Test
@@ -122,7 +120,6 @@
assertThat(onBackStackChangedTimes).isEqualTo(4)
}
- @Ignore // b/326237469
// Test removing a Fragment with a Transition and adding it back before the Transition
// finishes is handled correctly.
@Test
@@ -159,7 +156,6 @@
verifyNoOtherTransitions(fragment)
}
- @Ignore // b/326237469
@Test
fun testTimedPostponeImmediateStartNotCanceled() {
val fm = activityRule.activity.supportFragmentManager
@@ -198,7 +194,6 @@
assertThat(cancelCount).isEqualTo(0)
}
- @Ignore // b/326237469
@Test
fun ensureTransitionsFinishBeforeViewDestroyed() {
// enter transition
@@ -235,7 +230,6 @@
assertThat(fragment.transitionCountInOnDestroyView).isEqualTo(0)
}
- @Ignore // b/326237469
// Test that shared elements transition from one fragment to the next
// and back during pop.
@Test
@@ -251,7 +245,6 @@
verifyPopTransition(1, fragment2, fragment1)
}
- @Ignore // b/326237469
@Test
fun sharedElementNoOtherTransition() {
val fragment1 = setupInitialFragment()
@@ -293,7 +286,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
@Test
fun sharedElementAddNoOtherTransition() {
val fragment1 = setupInitialFragment()
@@ -328,7 +320,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
// Test that shared elements transition from one fragment to the next
// and back during pop.
@Suppress("DEPRECATION")
@@ -346,7 +337,6 @@
verifyPopTransition(1, fragment2, fragment1)
}
- @Ignore // b/326237469
// Test that shared element transitions through multiple fragments work together
@Test
fun intermediateFragment() {
@@ -364,7 +354,6 @@
verifyPopTransition(2, fragment3, fragment1, fragment2)
}
- @Ignore // b/326237469
// Adding/removing the same fragment multiple times shouldn't mess anything up
@Test
fun removeAdded() {
@@ -418,7 +407,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
// Make sure that shared elements on two different fragment containers don't interact
@Test
fun crossContainer() {
@@ -458,7 +446,6 @@
verifyCrossTransition(true, fragment1, fragment2)
}
- @Ignore // b/326237469
// Make sure that onSharedElementStart and onSharedElementEnd are called
@Suppress("UNCHECKED_CAST")
@Test
@@ -519,7 +506,6 @@
assertThat(snapshots.value).isNull()
}
- @Ignore // b/326237469
// Make sure that onMapSharedElement works to change the shared element going out
@Test
fun onMapSharedElementOut() {
@@ -592,7 +578,6 @@
}
}
- @Ignore // b/326237469
// Make sure that onMapSharedElement works to change the shared element target
@Test
fun onMapSharedElementIn() {
@@ -664,7 +649,6 @@
}
}
- @Ignore // b/326237469
// Ensure that shared element transitions that have targets properly target the views
@Test
fun complexSharedElementTransition() {
@@ -727,7 +711,6 @@
}
}
- @Ignore // b/326237469
// Ensure that after transitions have executed that they don't have any targets or other
// unfortunate modifications.
@Test
@@ -756,7 +739,6 @@
assertThat(fragment2.reenterTransition.epicenterCallback).isNull()
}
- @Ignore // b/326237469
// Ensure that transitions are done when a fragment is shown and hidden
@Test
fun showHideTransition() {
@@ -825,7 +807,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
// Test that setting allowEnterTransitionOverlap to false correctly delays
// the enter transition until after the exit transition finishes
@Test
@@ -877,7 +858,6 @@
.isFalse()
}
- @Ignore // b/326237469
// Ensure that transitions are done when a fragment is attached and detached
@Test
fun attachDetachTransition() {
@@ -927,7 +907,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
// Ensure that shared element without matching transition name doesn't error out
@Test
fun sharedElementMismatch() {
@@ -964,7 +943,6 @@
verifyNoOtherTransitions(fragment2)
}
- @Ignore // b/326237469
// Ensure that using the same source or target shared element results in an exception.
@Test
fun sharedDuplicateTargetNames() {
@@ -998,7 +976,6 @@
}
}
- @Ignore // b/326237469
// Test that invisible fragment views don't participate in transitions
@Test
fun invisibleNoTransitions() {
@@ -1037,7 +1014,6 @@
verifyNoOtherTransitions(fragment)
}
- @Ignore // b/326237469
// No crash when transitioning a shared element and there is no shared element transition.
@Test
fun noSharedElementTransition() {
@@ -1120,7 +1096,6 @@
}
}
- @Ignore // b/326237469
// No crash when there is no shared element transition and transitioning a shared element after
// a pop
@Test
@@ -1185,7 +1160,6 @@
}
}
- @Ignore // b/326237469
// When there is no matching shared element, the transition name should not be changed
@Test
fun noMatchingSharedElementRetainName() {
@@ -1233,7 +1207,6 @@
assertThat(endGreen.transitionName).isEqualTo("greenSquare")
}
- @Ignore // b/326237469
@Test
fun ignoreWhenViewNotAttached() {
with(ActivityScenario.launch(AddTransitionFragmentInActivity::class.java)) {
diff --git a/transition/transition/src/main/java/androidx/transition/DeleteMe.kt b/transition/transition/src/main/java/androidx/transition/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/transition/transition/src/main/java/androidx/transition/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index bb1ae47..69b4909 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -39,15 +39,16 @@
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
- api("androidx.annotation:annotation:1.6.0")
- api("androidx.compose.animation:animation:1.6.0")
- api("androidx.compose.foundation:foundation:1.6.0")
- api("androidx.compose.foundation:foundation-layout:1.6.0")
- api("androidx.compose.runtime:runtime:1.6.0")
- api("androidx.compose.ui:ui-util:1.6.0")
- api("androidx.compose.ui:ui:1.6.0")
- api("androidx.compose.ui:ui-graphics:1.6.0")
- api("androidx.compose.ui:ui-text:1.6.0")
+ def composeVersion = "1.6.3"
+ api("androidx.annotation:annotation:$composeVersion")
+ api("androidx.compose.animation:animation:$composeVersion")
+ api("androidx.compose.foundation:foundation:$composeVersion")
+ api("androidx.compose.foundation:foundation-layout:$composeVersion")
+ api("androidx.compose.runtime:runtime:$composeVersion")
+ api("androidx.compose.ui:ui-util:$composeVersion")
+ api("androidx.compose.ui:ui:$composeVersion")
+ api("androidx.compose.ui:ui-graphics:$composeVersion")
+ api("androidx.compose.ui:ui-text:$composeVersion")
androidTestImplementation(libs.truth)
androidTestImplementation(project(":compose:runtime:runtime"))
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 6bdb7e9..db20405 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -76,13 +76,13 @@
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardBorder {
+ @androidx.compose.runtime.Immutable public final class CardBorder {
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardColors {
+ @androidx.compose.runtime.Immutable public final class CardColors {
}
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardDefaults {
+ public final class CardDefaults {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors compactCardColors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
@@ -97,31 +97,31 @@
field public static final float VerticalImageAspectRatio = 0.6666667f;
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardGlow {
+ @androidx.compose.runtime.Immutable public final class CardGlow {
}
public final class CardKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void CompactCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.ui.graphics.Brush scrimBrush, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void CompactCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.ui.graphics.Brush scrimBrush, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutColors {
+ @androidx.compose.runtime.Immutable public final class CardLayoutColors {
}
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutDefaults {
+ public final class CardLayoutDefaults {
method @androidx.compose.runtime.Composable public void ImageCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardLayoutColors contentColor(optional long contentColor, optional long focusedContentColor, optional long pressedContentColor);
field public static final androidx.tv.material3.CardLayoutDefaults INSTANCE;
}
public final class CardLayoutKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardScale {
+ @androidx.compose.runtime.Immutable public final class CardScale {
field public static final androidx.tv.material3.CardScale.Companion Companion;
}
@@ -130,7 +130,7 @@
property public final androidx.tv.material3.CardScale None;
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardShape {
+ @androidx.compose.runtime.Immutable public final class CardShape {
}
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 6bdb7e9..db20405 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -76,13 +76,13 @@
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardBorder {
+ @androidx.compose.runtime.Immutable public final class CardBorder {
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardColors {
+ @androidx.compose.runtime.Immutable public final class CardColors {
}
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardDefaults {
+ public final class CardDefaults {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors compactCardColors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
@@ -97,31 +97,31 @@
field public static final float VerticalImageAspectRatio = 0.6666667f;
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardGlow {
+ @androidx.compose.runtime.Immutable public final class CardGlow {
}
public final class CardKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void CompactCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.ui.graphics.Brush scrimBrush, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void CompactCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.ui.graphics.Brush scrimBrush, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutColors {
+ @androidx.compose.runtime.Immutable public final class CardLayoutColors {
}
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutDefaults {
+ public final class CardLayoutDefaults {
method @androidx.compose.runtime.Composable public void ImageCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardLayoutColors contentColor(optional long contentColor, optional long focusedContentColor, optional long pressedContentColor);
field public static final androidx.tv.material3.CardLayoutDefaults INSTANCE;
}
public final class CardLayoutKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardScale {
+ @androidx.compose.runtime.Immutable public final class CardScale {
field public static final androidx.tv.material3.CardScale.Companion Companion;
}
@@ -130,7 +130,7 @@
property public final androidx.tv.material3.CardScale None;
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardShape {
+ @androidx.compose.runtime.Immutable public final class CardShape {
}
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 258c875..a53a48d 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -32,9 +32,13 @@
dependencies {
api(libs.kotlinStdlib)
- api("androidx.compose.animation:animation:1.5.3")
- api(project(":compose:foundation:foundation"))
- api("androidx.compose.material:material-icons-core:1.5.3")
+
+ def composeVersion = "1.6.3"
+ api("androidx.compose.animation:animation:$composeVersion")
+ api("androidx.compose.foundation:foundation:$composeVersion")
+ api("androidx.compose.foundation:foundation-layout:$composeVersion")
+ api("androidx.compose.material:material-icons-core:$composeVersion")
+
api(project(":tv:tv-foundation"))
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
index dab410f..0465e8a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
@@ -66,7 +66,6 @@
* still happen internally.
* @param content defines the [Composable] content inside the Card.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun Card(
onClick: () -> Unit,
@@ -129,7 +128,6 @@
* or preview the card in different states. Note that if `null` is provided, interactions will
* still happen internally.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun ClassicCard(
onClick: () -> Unit,
@@ -212,7 +210,6 @@
* or preview the card in different states. Note that if `null` is provided, interactions will
* still happen internally.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun CompactCard(
onClick: () -> Unit,
@@ -298,7 +295,6 @@
* or preview the card in different states. Note that if `null` is provided, interactions will
* still happen internally.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun WideClassicCard(
onClick: () -> Unit,
@@ -369,7 +365,6 @@
/**
* Contains the default values used by all card types.
*/
-@ExperimentalTvMaterial3Api
object CardDefaults {
internal val ContentImageAlignment = Alignment.Center
@@ -547,7 +542,6 @@
private const val SubtitleAlpha = 0.6f
private const val DescriptionAlpha = 0.8f
-@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardColors.toClickableSurfaceColors() =
ClickableSurfaceColors(
containerColor = containerColor,
@@ -560,7 +554,6 @@
disabledContentColor = contentColor
)
-@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardShape.toClickableSurfaceShape() =
ClickableSurfaceShape(
shape = shape,
@@ -570,7 +563,6 @@
focusedDisabledShape = shape
)
-@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardScale.toClickableSurfaceScale() =
ClickableSurfaceScale(
scale = scale,
@@ -580,7 +572,6 @@
focusedDisabledScale = scale
)
-@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardBorder.toClickableSurfaceBorder() =
ClickableSurfaceBorder(
border = border,
@@ -590,7 +581,6 @@
focusedDisabledBorder = border
)
-@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardGlow.toClickableSurfaceGlow() =
ClickableSurfaceGlow(
glow = glow,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
index c4273ac..85521bb 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
@@ -55,7 +55,6 @@
* [Interaction]s for this CardLayout.
* This interaction source param would also be forwarded to be used with the `imageCard` composable.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun StandardCardLayout(
imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
@@ -117,7 +116,6 @@
* [Interaction]s for this CardLayout.
* This interaction source param would also be forwarded to be used with the `imageCard` composable.
*/
-@ExperimentalTvMaterial3Api
@Composable
fun WideCardLayout(
imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
@@ -165,7 +163,6 @@
}
}
-@ExperimentalTvMaterial3Api
object CardLayoutDefaults {
/**
* Creates [CardLayoutColors] that represents the default content colors used in a
@@ -243,7 +240,6 @@
/**
* Represents the [Color] of content in a CardLayout for different interaction states.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardLayoutColors internal constructor(
internal val contentColor: Color,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CardStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CardStyles.kt
index 6856416..6bc81e6 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CardStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CardStyles.kt
@@ -24,7 +24,6 @@
/**
* Represents the [Color] of Card in different interaction states.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardColors internal constructor(
internal val containerColor: Color,
@@ -75,7 +74,6 @@
/**
* Represents the [Shape] of Card in different interaction states.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardShape internal constructor(
internal val shape: Shape,
@@ -112,7 +110,6 @@
* Represents the scaleFactor of Card in different interaction states.
* Note: This scaleFactor must always be a non-negative float.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardScale internal constructor(
@FloatRange(from = 0.0) internal val scale: Float,
@@ -159,7 +156,6 @@
/**
* Represents the [Border] of Card in different interaction states.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardBorder internal constructor(
internal val border: Border,
@@ -196,7 +192,6 @@
/**
* Represents the [Glow] of Card in different interaction states.
*/
-@ExperimentalTvMaterial3Api
@Immutable
class CardGlow internal constructor(
internal val glow: Glow,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
index d642e52..338b752 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
@@ -146,7 +146,8 @@
onClick = onClick,
enabled = enabled,
role = Role.Checkbox,
- interactionSource = interactionSource,
+ // TODO: remove the optional argument once we update to compose 1.7.x
+ interactionSource = interactionSource ?: remember { MutableInteractionSource() },
indication = null
)
} else {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
index c3c3285..97477e2 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
@@ -29,6 +29,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -85,7 +86,8 @@
onClick = onClick,
enabled = enabled,
role = Role.RadioButton,
- interactionSource = interactionSource,
+ // TODO: remove the optional argument once we update to compose 1.7.x
+ interactionSource = interactionSource ?: remember { MutableInteractionSource() },
indication = null
)
} else {
diff --git a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/DeleteMe.kt b/viewpager2/viewpager2/src/main/java/androidx/viewpager2/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 5784324..324e62f 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 19
- versionName "1.19"
+ versionCode 21
+ versionName "1.21"
}
buildTypes {
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/DeleteMe.kt b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/wear/watchface/watchface-complications-data-source-ktx/api/current.txt b/wear/watchface/watchface-complications-data-source-ktx/api/current.txt
index 55d30d5..621eb48 100644
--- a/wear/watchface/watchface-complications-data-source-ktx/api/current.txt
+++ b/wear/watchface/watchface-complications-data-source-ktx/api/current.txt
@@ -4,13 +4,13 @@
public abstract class SuspendingComplicationDataSourceService extends androidx.wear.watchface.complications.datasource.ComplicationDataSourceService {
ctor public SuspendingComplicationDataSourceService();
method public final void onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener listener);
- method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData>);
+ method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData?>);
}
public abstract class SuspendingTimelineComplicationDataSourceService extends androidx.wear.watchface.complications.datasource.ComplicationDataSourceService {
ctor public SuspendingTimelineComplicationDataSourceService();
method public final void onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener listener);
- method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.datasource.ComplicationDataTimeline>);
+ method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.datasource.ComplicationDataTimeline?>);
}
}
diff --git a/wear/watchface/watchface-complications-data-source-ktx/api/restricted_current.txt b/wear/watchface/watchface-complications-data-source-ktx/api/restricted_current.txt
index 55d30d5..621eb48 100644
--- a/wear/watchface/watchface-complications-data-source-ktx/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data-source-ktx/api/restricted_current.txt
@@ -4,13 +4,13 @@
public abstract class SuspendingComplicationDataSourceService extends androidx.wear.watchface.complications.datasource.ComplicationDataSourceService {
ctor public SuspendingComplicationDataSourceService();
method public final void onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener listener);
- method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData>);
+ method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData?>);
}
public abstract class SuspendingTimelineComplicationDataSourceService extends androidx.wear.watchface.complications.datasource.ComplicationDataSourceService {
ctor public SuspendingTimelineComplicationDataSourceService();
method public final void onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener listener);
- method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.datasource.ComplicationDataTimeline>);
+ method @UiThread public abstract suspend Object? onComplicationRequest(androidx.wear.watchface.complications.datasource.ComplicationRequest request, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.datasource.ComplicationDataTimeline?>);
}
}
diff --git a/wear/watchface/watchface-complications/api/current.txt b/wear/watchface/watchface-complications/api/current.txt
index 5bbb240..46dbdf8 100644
--- a/wear/watchface/watchface-complications/api/current.txt
+++ b/wear/watchface/watchface-complications/api/current.txt
@@ -20,8 +20,8 @@
public final class ComplicationDataSourceInfoRetriever implements java.lang.AutoCloseable {
ctor public ComplicationDataSourceInfoRetriever(android.content.Context context);
method public void close();
- method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveComplicationDataSourceInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.Result[]>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
- method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName complicationDataSourceComponent, androidx.wear.watchface.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
+ method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveComplicationDataSourceInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.Result[]?>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName complicationDataSourceComponent, androidx.wear.watchface.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData?>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
}
public static final class ComplicationDataSourceInfoRetriever.Result {
diff --git a/wear/watchface/watchface-complications/api/restricted_current.txt b/wear/watchface/watchface-complications/api/restricted_current.txt
index 5bbb240..46dbdf8 100644
--- a/wear/watchface/watchface-complications/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications/api/restricted_current.txt
@@ -20,8 +20,8 @@
public final class ComplicationDataSourceInfoRetriever implements java.lang.AutoCloseable {
ctor public ComplicationDataSourceInfoRetriever(android.content.Context context);
method public void close();
- method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveComplicationDataSourceInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.Result[]>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
- method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName complicationDataSourceComponent, androidx.wear.watchface.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
+ method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveComplicationDataSourceInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.Result[]?>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName complicationDataSourceComponent, androidx.wear.watchface.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.watchface.complications.data.ComplicationData?>) throws androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.ServiceDisconnectedException;
}
public static final class ComplicationDataSourceInfoRetriever.Result {
diff --git a/wear/watchface/watchface-editor-guava/api/current.txt b/wear/watchface/watchface-editor-guava/api/current.txt
index 1230171..6a33f4c 100644
--- a/wear/watchface/watchface-editor-guava/api/current.txt
+++ b/wear/watchface/watchface-editor-guava/api/current.txt
@@ -18,7 +18,7 @@
method public boolean isCommitChangesOnClose();
method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession?> listenableCreateOnWatchEditorSession(androidx.activity.ComponentActivity activity);
method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ChosenComplicationDataSource?> listenableOpenComplicationDataSourceChooser(int complicationSlotId);
- method public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource>);
+ method public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource?>);
method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, java.time.Instant instant, java.util.Map<java.lang.Integer,? extends androidx.wear.watchface.complications.data.ComplicationData>? slotIdToComplicationData);
method public void setCommitChangesOnClose(boolean);
property public Integer? backgroundComplicationSlotId;
diff --git a/wear/watchface/watchface-editor-guava/api/restricted_current.txt b/wear/watchface/watchface-editor-guava/api/restricted_current.txt
index 1230171..6a33f4c 100644
--- a/wear/watchface/watchface-editor-guava/api/restricted_current.txt
+++ b/wear/watchface/watchface-editor-guava/api/restricted_current.txt
@@ -18,7 +18,7 @@
method public boolean isCommitChangesOnClose();
method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession?> listenableCreateOnWatchEditorSession(androidx.activity.ComponentActivity activity);
method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ChosenComplicationDataSource?> listenableOpenComplicationDataSourceChooser(int complicationSlotId);
- method public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource>);
+ method public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource?>);
method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, java.time.Instant instant, java.util.Map<java.lang.Integer,? extends androidx.wear.watchface.complications.data.ComplicationData>? slotIdToComplicationData);
method public void setCommitChangesOnClose(boolean);
property public Integer? backgroundComplicationSlotId;
diff --git a/wear/watchface/watchface-editor/api/current.txt b/wear/watchface/watchface-editor/api/current.txt
index 10e482e..7386097 100644
--- a/wear/watchface/watchface-editor/api/current.txt
+++ b/wear/watchface/watchface-editor/api/current.txt
@@ -48,7 +48,7 @@
method public android.content.ComponentName getWatchFaceComponentName();
method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public boolean isCommitChangesOnClose();
- method @UiThread public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource>);
+ method @UiThread public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource?>);
method @UiThread public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, java.time.Instant instant, java.util.Map<java.lang.Integer,? extends androidx.wear.watchface.complications.data.ComplicationData>? slotIdToComplicationData);
method @UiThread public void setCommitChangesOnClose(boolean);
property public abstract Integer? backgroundComplicationSlotId;
diff --git a/wear/watchface/watchface-editor/api/restricted_current.txt b/wear/watchface/watchface-editor/api/restricted_current.txt
index 10e482e..7386097 100644
--- a/wear/watchface/watchface-editor/api/restricted_current.txt
+++ b/wear/watchface/watchface-editor/api/restricted_current.txt
@@ -48,7 +48,7 @@
method public android.content.ComponentName getWatchFaceComponentName();
method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public boolean isCommitChangesOnClose();
- method @UiThread public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource>);
+ method @UiThread public suspend Object? openComplicationDataSourceChooser(int complicationSlotId, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.ChosenComplicationDataSource?>);
method @UiThread public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, java.time.Instant instant, java.util.Map<java.lang.Integer,? extends androidx.wear.watchface.complications.data.ComplicationData>? slotIdToComplicationData);
method @UiThread public void setCommitChangesOnClose(boolean);
property public abstract Integer? backgroundComplicationSlotId;
diff --git a/wear/wear/src/main/java/androidx/wear/DeleteMe.kt b/wear/wear/src/main/java/androidx/wear/DeleteMe.kt
deleted file mode 100644
index 38f8b7a..0000000
--- a/wear/wear/src/main/java/androidx/wear/DeleteMe.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2022 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.
- */
-
-// This file exists to trick AGP/lint to work around b/234865137
diff --git a/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
index 8af59c5..d11a796 100644
--- a/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
+++ b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
@@ -137,7 +137,7 @@
@Test
@MediumTest
@Suppress("UNCHECKED_CAST")
- public fun cleanUpWhenSessionIsInvalid() {
+ public fun executeWhenSessionIsInvalid() {
if (Build.VERSION.SDK_INT <= 27) {
// Exclude <= API 27, from tests because it causes a SIGSEGV.
return
@@ -154,7 +154,7 @@
exception = throwable
}
assertNotNull(exception)
- verify(mClient).cleanUp()
+ assertTrue(exception!!.message!!.contains("Something bad happened"))
verify(mRunnableScheduler, atLeastOnce())
.scheduleWithDelay(anyLong(), any(Runnable::class.java))
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
index a8ba626..121fbfd 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
@@ -16,21 +16,19 @@
package androidx.work.multiprocess;
-import static android.content.Context.BIND_AUTO_CREATE;
+import static androidx.work.multiprocess.ServiceBindingKt.bindToService;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Logger;
-import androidx.work.impl.utils.futures.SettableFuture;
import com.google.common.util.concurrent.ListenableFuture;
@@ -52,7 +50,7 @@
final Executor mExecutor;
private final Object mLock;
- private Connection mConnection;
+ private Session<IListenableWorkerImpl> mConnection;
public ListenableWorkerImplClient(
@NonNull Context context,
@@ -73,22 +71,14 @@
synchronized (mLock) {
if (mConnection == null) {
Logger.get().debug(TAG,
- "Binding to " + component.getPackageName() + ", " + component.getClassName());
-
- mConnection = new Connection();
- try {
- Intent intent = new Intent();
- intent.setComponent(component);
- boolean bound = mContext.bindService(intent, mConnection, BIND_AUTO_CREATE);
- if (!bound) {
- unableToBind(mConnection,
- new RuntimeException("Unable to bind to service"));
- }
- } catch (Throwable throwable) {
- unableToBind(mConnection, throwable);
- }
+ "Binding to " + component.getPackageName() + ", "
+ + component.getClassName());
+ Intent intent = new Intent();
+ intent.setComponent(component);
+ mConnection = bindToService(mContext, intent,
+ IListenableWorkerImpl.Stub::asInterface, TAG);
}
- return mConnection.mFuture;
+ return mConnection.getConnectedFuture();
}
}
@@ -134,56 +124,7 @@
*/
@Nullable
@VisibleForTesting
- public Connection getConnection() {
+ Session<IListenableWorkerImpl> getConnection() {
return mConnection;
}
-
- /**
- * The implementation of {@link ServiceConnection} that handles changes in the connection.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class Connection implements ServiceConnection {
- private static final String TAG = Logger.tagWithPrefix("ListenableWorkerImplSession");
-
- final SettableFuture<IListenableWorkerImpl> mFuture;
-
- public Connection() {
- mFuture = SettableFuture.create();
- }
-
- @Override
- public void onServiceConnected(
- @NonNull ComponentName componentName,
- @NonNull IBinder iBinder) {
- Logger.get().debug(TAG, "Service connected");
- IListenableWorkerImpl iListenableWorkerImpl =
- IListenableWorkerImpl.Stub.asInterface(iBinder);
- mFuture.set(iListenableWorkerImpl);
- }
-
- @Override
- public void onServiceDisconnected(@NonNull ComponentName componentName) {
- Logger.get().warning(TAG, "Service disconnected");
- mFuture.setException(new RuntimeException("Service disconnected"));
- }
-
- @Override
- public void onBindingDied(@NonNull ComponentName name) {
- Logger.get().warning(TAG, "Binding died");
- mFuture.setException(new RuntimeException("Binding died"));
- }
-
- @Override
- public void onNullBinding(@NonNull ComponentName name) {
- Logger.get().error(TAG, "Unable to bind to service");
- mFuture.setException(
- new RuntimeException("Cannot bind to service " + name));
- }
- }
-
- private static void unableToBind(@NonNull Connection session, @NonNull Throwable throwable) {
- Logger.get().error(TAG, "Unable to bind to service", throwable);
- session.mFuture.setException(throwable);
- }
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
index 818566c..e70baee 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
@@ -16,17 +16,12 @@
package androidx.work.multiprocess;
-import static android.content.Context.BIND_AUTO_CREATE;
-
import static androidx.work.multiprocess.RemoteClientUtils.map;
import static androidx.work.multiprocess.RemoteClientUtils.sVoidMapper;
import android.annotation.SuppressLint;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
@@ -35,6 +30,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.work.Data;
+import androidx.work.DirectExecutor;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.ExistingWorkPolicy;
import androidx.work.ForegroundInfo;
@@ -48,7 +44,6 @@
import androidx.work.WorkRequest;
import androidx.work.impl.WorkContinuationImpl;
import androidx.work.impl.WorkManagerImpl;
-import androidx.work.impl.utils.futures.SettableFuture;
import androidx.work.multiprocess.parcelable.ParcelConverters;
import androidx.work.multiprocess.parcelable.ParcelableForegroundRequestInfo;
import androidx.work.multiprocess.parcelable.ParcelableUpdateRequest;
@@ -63,7 +58,6 @@
import java.util.Collections;
import java.util.List;
import java.util.UUID;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
/**
@@ -81,7 +75,7 @@
static final String TAG = Logger.tagWithPrefix("RemoteWorkManagerClient");
// Synthetic access
- Session mSession;
+ Session<IWorkManagerImpl> mSession;
final Context mContext;
final WorkManagerImpl mWorkManager;
@@ -341,7 +335,7 @@
* @return The current {@link Session} in use by {@link RemoteWorkManagerClient}.
*/
@Nullable
- public Session getCurrentSession() {
+ Session<IWorkManagerImpl> getCurrentSession() {
return mSession;
}
@@ -381,13 +375,6 @@
ListenableFuture<byte[]> execute(
@NonNull final ListenableFuture<IWorkManagerImpl> session,
@NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher) {
- session.addListener(() -> {
- try {
- session.get();
- } catch (ExecutionException | InterruptedException exception) {
- cleanUp();
- }
- }, mExecutor);
ListenableFuture<byte[]> future = RemoteExecuteKt.execute(mExecutor, session,
dispatcher);
future.addListener(() -> {
@@ -405,21 +392,21 @@
ListenableFuture<IWorkManagerImpl> getSession(@NonNull Intent intent) {
synchronized (mLock) {
mSessionIndex += 1;
+ ListenableFuture<IWorkManagerImpl> resultFuture;
if (mSession == null) {
- Logger.get().debug(TAG, "Creating a new session");
- mSession = new Session(this);
- try {
- boolean bound = mContext.bindService(intent, mSession, BIND_AUTO_CREATE);
- if (!bound) {
- unableToBind(mSession, new RuntimeException("Unable to bind to service"));
- }
- } catch (Throwable throwable) {
- unableToBind(mSession, throwable);
- }
+ mSession = ServiceBindingKt.bindToService(mContext, intent,
+ IWorkManagerImpl.Stub::asInterface, TAG);
+ // reading future right away, because `this::cleanUp` will synchronously
+ // set mSession to null.
+ resultFuture = mSession.getConnectedFuture();
+ mSession.getDisconnectedFuture()
+ .addListener(this::cleanUp, DirectExecutor.INSTANCE);
+ } else {
+ resultFuture = mSession.getConnectedFuture();
}
// Reset session tracker.
mRunnableScheduler.cancel(mSessionTracker);
- return mSession.mFuture;
+ return resultFuture;
}
}
@@ -434,11 +421,6 @@
}
}
- private void unableToBind(@NonNull Session session, @NonNull Throwable throwable) {
- Logger.get().error(TAG, "Unable to bind to service", throwable);
- session.mFuture.setException(throwable);
- }
-
/**
* @return the intent that is used to bind to the instance of {@link IWorkManagerImpl}.
*/
@@ -447,59 +429,6 @@
}
/**
- * The implementation of {@link ServiceConnection} that handles changes in the connection.
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class Session implements ServiceConnection {
- private static final String TAG = Logger.tagWithPrefix("RemoteWMgr.Connection");
-
- final SettableFuture<IWorkManagerImpl> mFuture;
- final RemoteWorkManagerClient mClient;
-
- public Session(@NonNull RemoteWorkManagerClient client) {
- mClient = client;
- mFuture = SettableFuture.create();
- }
-
- @Override
- public void onServiceConnected(
- @NonNull ComponentName componentName,
- @NonNull IBinder iBinder) {
- Logger.get().debug(TAG, "Service connected");
- IWorkManagerImpl iWorkManagerImpl = IWorkManagerImpl.Stub.asInterface(iBinder);
- mFuture.set(iWorkManagerImpl);
- }
-
- @Override
- public void onServiceDisconnected(@NonNull ComponentName componentName) {
- Logger.get().debug(TAG, "Service disconnected");
- mFuture.setException(new RuntimeException("Service disconnected"));
- mClient.cleanUp();
- }
-
- @Override
- public void onBindingDied(@NonNull ComponentName name) {
- onBindingDied();
- }
-
- /**
- * Clean-up client when a binding dies.
- */
- public void onBindingDied() {
- Logger.get().debug(TAG, "Binding died");
- mFuture.setException(new RuntimeException("Binding died"));
- mClient.cleanUp();
- }
-
- @Override
- public void onNullBinding(@NonNull ComponentName name) {
- Logger.get().error(TAG, "Unable to bind to service");
- mFuture.setException(
- new RuntimeException("Cannot bind to service " + name));
- }
- }
-
- /**
* A {@link Runnable} that enforces a TTL for a {@link RemoteWorkManagerClient} session.
*/
public static class SessionTracker implements Runnable {
@@ -515,7 +444,7 @@
final long preLockIndex = mClient.getSessionIndex();
synchronized (mClient.getSessionLock()) {
final long sessionIndex = mClient.getSessionIndex();
- final Session currentSession = mClient.getCurrentSession();
+ final Session<IWorkManagerImpl> currentSession = mClient.getCurrentSession();
// We check for a session index here. This is because if the index changes
// while we acquire a lock, that would mean that a new session request came through.
if (currentSession != null) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ServiceBinding.kt b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ServiceBinding.kt
new file mode 100644
index 0000000..870d0d3
--- /dev/null
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ServiceBinding.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.os.IInterface
+import androidx.concurrent.futures.SuspendToFutureAdapter.launchFuture
+import androidx.core.util.Function
+import androidx.work.Logger
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+
+internal fun <T : IInterface?> bindToService(
+ context: Context,
+ intent: Intent,
+ asInterface: Function<IBinder?, T>,
+ loggingTag: String
+): Session<T> {
+ Logger.get().debug(loggingTag, "Binding via $intent")
+
+ val session = Session(loggingTag, asInterface)
+ try {
+ val bound = context.bindService(intent, session, Context.BIND_AUTO_CREATE)
+ if (!bound) {
+ session.resolveClosedConnection(RuntimeException("Unable to bind to service"))
+ }
+ } catch (throwable: Throwable) {
+ session.resolveClosedConnection(throwable)
+ }
+ return session
+}
+
+internal class Session<T : IInterface?>(
+ private val logTag: String,
+ private val asInterface: Function<IBinder?, T>
+) : ServiceConnection {
+
+ sealed class State {
+ object Created : State()
+ class Connected(val iBinder: IBinder) : State()
+ class Disconnected(val throwable: Throwable) : State()
+ }
+
+ private val stateFlow = MutableStateFlow<State>(State.Created)
+
+ val connectedFuture = launchFuture<T>(Dispatchers.Unconfined) {
+ val state = stateFlow.first { it != State.Created }
+ if (state is State.Connected) {
+ asInterface.apply(state.iBinder)
+ } else {
+ // we can go straight to disconnected state if we failed to bind
+ throw (state as State.Disconnected).throwable
+ }
+ }
+
+ val disconnectedFuture = launchFuture<Unit> {
+ stateFlow.first { it is State.Disconnected }
+ }
+
+ override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
+ stateFlow.value = State.Connected(iBinder)
+ }
+
+ override fun onServiceDisconnected(componentName: ComponentName) {
+ Logger.get().debug(logTag, "Service disconnected")
+ resolveClosedConnection(RuntimeException("Service disconnected"))
+ }
+
+ override fun onBindingDied(name: ComponentName) {
+ onBindingDied()
+ }
+
+ /**
+ * Clean-up client when a binding dies.
+ */
+ fun onBindingDied() {
+ Logger.get().debug(logTag, "Binding died")
+ resolveClosedConnection(RuntimeException("Binding died"))
+ }
+
+ override fun onNullBinding(name: ComponentName) {
+ Logger.get().error(logTag, "Unable to bind to service")
+ resolveClosedConnection(RuntimeException("Cannot bind to service $name"))
+ }
+
+ fun resolveClosedConnection(throwable: Throwable) {
+ stateFlow.value = State.Disconnected(throwable)
+ }
+}
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 59abcf4..b681410 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -155,14 +155,14 @@
method public float[]? getFloatArray(String key);
method public int getInt(String key, int defaultValue);
method public int[]? getIntArray(String key);
- method public java.util.Map<java.lang.String,java.lang.Object> getKeyValueMap();
+ method public java.util.Map<java.lang.String,java.lang.Object?> getKeyValueMap();
method public long getLong(String key, long defaultValue);
method public long[]? getLongArray(String key);
method public String? getString(String key);
method public String[]? getStringArray(String key);
method public <T> boolean hasKeyWithValueOfType(String key, Class<T> klass);
method public byte[] toByteArray();
- property public final java.util.Map<java.lang.String,java.lang.Object> keyValueMap;
+ property public final java.util.Map<java.lang.String,java.lang.Object?> keyValueMap;
field public static final androidx.work.Data.Companion Companion;
field public static final androidx.work.Data EMPTY;
field public static final int MAX_DATA_BYTES = 10240; // 0x2800
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 59abcf4..b681410 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -155,14 +155,14 @@
method public float[]? getFloatArray(String key);
method public int getInt(String key, int defaultValue);
method public int[]? getIntArray(String key);
- method public java.util.Map<java.lang.String,java.lang.Object> getKeyValueMap();
+ method public java.util.Map<java.lang.String,java.lang.Object?> getKeyValueMap();
method public long getLong(String key, long defaultValue);
method public long[]? getLongArray(String key);
method public String? getString(String key);
method public String[]? getStringArray(String key);
method public <T> boolean hasKeyWithValueOfType(String key, Class<T> klass);
method public byte[] toByteArray();
- property public final java.util.Map<java.lang.String,java.lang.Object> keyValueMap;
+ property public final java.util.Map<java.lang.String,java.lang.Object?> keyValueMap;
field public static final androidx.work.Data.Companion Companion;
field public static final androidx.work.Data EMPTY;
field public static final int MAX_DATA_BYTES = 10240; // 0x2800
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
index 8b9be1d..eb0b885 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
@@ -86,7 +86,8 @@
.build()
val scheduler = mock(Scheduler::class.java)
workDatabase = WorkDatabase.create(
- context, taskExecutor.serialTaskExecutor, config.clock, true)
+ context, taskExecutor.serialTaskExecutor, config.clock, true
+ )
processor = spy(Processor(context, config, taskExecutor, workDatabase))
workManager = spy(
WorkManagerImpl(
@@ -100,11 +101,13 @@
)
workDatabase = workManager.workDatabase
// Initialize WorkConstraintsTracker
- tracker = WorkConstraintsTracker(Trackers(
- context = context,
- taskExecutor = taskExecutor,
- batteryChargingTracker = fakeChargingTracker
- ))
+ tracker = WorkConstraintsTracker(
+ Trackers(
+ context = context,
+ taskExecutor = taskExecutor,
+ batteryChargingTracker = fakeChargingTracker
+ )
+ )
// Initialize dispatcher
dispatcherCallback = mock(SystemForegroundDispatcher.Callback::class.java)
dispatcher = spy(SystemForegroundDispatcher(context, workManager, tracker))
@@ -118,8 +121,10 @@
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
workDatabase.workSpecDao().insertWorkSpec(request.workSpec)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.stringId, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.stringId, 0), metadata
+ )
dispatcher.onStartCommand(intent)
verify(dispatcherCallback, times(1))
.startForeground(eq(notificationId), eq(0), any<Notification>())
@@ -136,8 +141,10 @@
val notificationId = 1
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.stringId, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.stringId, 0), metadata
+ )
dispatcher.onStartCommand(intent)
verify(dispatcherCallback, times(1))
.startForeground(eq(notificationId), eq(0), any<Notification>())
@@ -172,6 +179,8 @@
val metadata = ForegroundInfo(notificationId, notification)
val intent = createNotifyIntent(context, workSpecId, metadata)
dispatcher.mCurrentForegroundId = WorkGenerationalId("anotherWorkSpecId", 0)
+ dispatcher.mForegroundInfoById[dispatcher.mCurrentForegroundId] =
+ ForegroundInfo(10, mock(Notification::class.java))
dispatcher.onStartCommand(intent)
verify(dispatcherCallback, times(1))
.notify(eq(notificationId), any<Notification>())
@@ -385,14 +394,18 @@
val notificationId = 1
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.stringId, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.stringId, 0), metadata
+ )
dispatcher.onStartCommand(intent)
assertThat(fakeChargingTracker.isTracking, `is`(true))
fakeChargingTracker.state = false
- verify(workManager, times(1)).stopForegroundWork(eq(
- WorkGenerationalId(request.workSpec.id, 0)
- ))
+ verify(workManager, times(1)).stopForegroundWork(
+ eq(
+ WorkGenerationalId(request.workSpec.id, 0)
+ )
+ )
}
@Test
@@ -406,8 +419,10 @@
val notificationId = 1
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.workSpec.id, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.workSpec.id, 0), metadata
+ )
dispatcher.onStartCommand(intent)
assertThat(fakeChargingTracker.isTracking, `is`(true))
val stopIntent = createCancelWorkIntent(context, request.stringId)
@@ -426,8 +441,10 @@
val notificationId = 1
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.stringId, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.stringId, 0), metadata
+ )
dispatcher.onStartCommand(intent)
processor.stopWork(StartStopToken(WorkGenerationalId(request.stringId, 0)), 0)
val state = workDatabase.workSpecDao().getState(request.stringId)
@@ -452,8 +469,10 @@
val notificationId = 1
val notification = mock(Notification::class.java)
val metadata = ForegroundInfo(notificationId, notification)
- val intent = createStartForegroundIntent(context,
- WorkGenerationalId(request.stringId, 0), metadata)
+ val intent = createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.stringId, 0), metadata
+ )
dispatcher.onStartCommand(intent)
assertThat(fakeChargingTracker.isTracking, `is`(true))
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index 99422cc..de9b1e8 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -210,6 +210,7 @@
mCallback = callback;
}
+ @MainThread
void onStartCommand(@NonNull Intent intent) {
String action = intent.getAction();
if (ACTION_START_FOREGROUND.equals(action)) {
@@ -264,6 +265,9 @@
@SuppressWarnings("deprecation")
@MainThread
private void handleNotify(@NonNull Intent intent) {
+ if (mCallback == null) {
+ throw new IllegalStateException("handleNotify was called on the destroyed dispatcher");
+ }
int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);
int notificationType = intent.getIntExtra(KEY_FOREGROUND_SERVICE_TYPE, 0);
String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
@@ -275,42 +279,41 @@
"Notifying with (id:" + notificationId
+ ", workSpecId: " + workSpecId
+ ", notificationType :" + notificationType + ")");
+ if (notification == null) {
+ throw new IllegalArgumentException("Notification passed in the intent was null.");
+ }
- if (notification != null && mCallback != null) {
- // Keep track of this ForegroundInfo
- ForegroundInfo info = new ForegroundInfo(
- notificationId, notification, notificationType);
-
- mForegroundInfoById.put(workId, info);
- if (mCurrentForegroundId == null) {
- // This is the current workSpecId which owns the Foreground lifecycle.
- mCurrentForegroundId = workId;
- mCallback.startForeground(notificationId, notificationType, notification);
- } else {
- // Update notification
- mCallback.notify(notificationId, notification);
- // Update the notification in the foreground such that it's the union of
- // all current foreground service types if necessary.
- if (notificationType != FOREGROUND_SERVICE_TYPE_NONE
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- int foregroundServiceType = FOREGROUND_SERVICE_TYPE_NONE;
- for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry
- : mForegroundInfoById.entrySet()) {
- ForegroundInfo foregroundInfo = entry.getValue();
- foregroundServiceType |= foregroundInfo.getForegroundServiceType();
- }
- ForegroundInfo currentInfo =
- mForegroundInfoById.get(mCurrentForegroundId);
- if (currentInfo != null) {
- mCallback.startForeground(
- currentInfo.getNotificationId(),
- foregroundServiceType,
- currentInfo.getNotification()
- );
- }
+ // Keep track of this ForegroundInfo
+ ForegroundInfo info = new ForegroundInfo(notificationId, notification, notificationType);
+ mForegroundInfoById.put(workId, info);
+ ForegroundInfo currentInfo = mForegroundInfoById.get(mCurrentForegroundId);
+ ForegroundInfo resultInfo;
+ if (currentInfo == null) {
+ // This is the current workSpecId which owns the Foreground lifecycle.
+ mCurrentForegroundId = workId;
+ resultInfo = info;
+ } else {
+ // Update notification
+ mCallback.notify(notificationId, notification);
+ // Update the notification in the foreground such that it's the union of
+ // all current foreground service types if necessary.
+ // Before Q startForeground didn't receive foregroundServiceType, so no need to
+ // calculate it.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ int foregroundServiceType = FOREGROUND_SERVICE_TYPE_NONE;
+ for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry
+ : mForegroundInfoById.entrySet()) {
+ ForegroundInfo foregroundInfo = entry.getValue();
+ foregroundServiceType |= foregroundInfo.getForegroundServiceType();
}
+ resultInfo = new ForegroundInfo(currentInfo.getNotificationId(),
+ currentInfo.getNotification(), foregroundServiceType);
+ } else {
+ resultInfo = currentInfo;
}
}
+ mCallback.startForeground(resultInfo.getNotificationId(),
+ resultInfo.getForegroundServiceType(), resultInfo.getNotification());
}
@MainThread
@@ -428,6 +431,7 @@
* An implementation of this callback should call
* {@link android.app.Service#startForeground(int, Notification, int)}.
*/
+ @MainThread
void startForeground(
int notificationId,
int notificationType,
@@ -436,16 +440,19 @@
/**
* Used to update the {@link Notification}.
*/
+ @MainThread
void notify(int notificationId, @NonNull Notification notification);
/**
* Used to cancel a {@link Notification}.
*/
+ @MainThread
void cancelNotification(int notificationId);
/**
* Used to stop the {@link SystemForegroundService}.
*/
+ @MainThread
void stop();
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
index cad168e..62efc86 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
@@ -24,8 +24,6 @@
import android.content.Context;
import android.content.Intent;
import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
import androidx.annotation.DoNotInline;
import androidx.annotation.MainThread;
@@ -48,7 +46,6 @@
@Nullable
private static SystemForegroundService sForegroundService = null;
- private Handler mHandler;
private boolean mIsShutdown;
// Synthetic access
@@ -94,14 +91,12 @@
@MainThread
private void initializeDispatcher() {
- mHandler = new Handler(Looper.getMainLooper());
mNotificationManager = (NotificationManager)
getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
mDispatcher = new SystemForegroundDispatcher(getApplicationContext());
mDispatcher.setCallback(this);
}
- @SuppressWarnings("deprecation")
@MainThread
@Override
public void stop() {
@@ -116,47 +111,34 @@
stopSelf();
}
+ @MainThread
@Override
public void startForeground(
final int notificationId,
final int notificationType,
@NonNull final Notification notification) {
-
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- Api31Impl.startForeground(SystemForegroundService.this, notificationId,
- notification, notificationType);
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- Api29Impl.startForeground(SystemForegroundService.this, notificationId,
- notification, notificationType);
- } else {
- startForeground(notificationId, notification);
- }
- }
- });
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Api31Impl.startForeground(SystemForegroundService.this, notificationId,
+ notification, notificationType);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Api29Impl.startForeground(SystemForegroundService.this, notificationId,
+ notification, notificationType);
+ } else {
+ startForeground(notificationId, notification);
+ }
}
+ @MainThread
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
@Override
public void notify(final int notificationId, @NonNull final Notification notification) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mNotificationManager.notify(notificationId, notification);
- }
- });
+ mNotificationManager.notify(notificationId, notification);
}
@Override
+ @MainThread
public void cancelNotification(final int notificationId) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mNotificationManager.cancel(notificationId);
- }
- });
+ mNotificationManager.cancel(notificationId);
}
/**