Merge "Deprecate `toPublisher(lifecycle, liveData)`" into androidx-main
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a760252..733504c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,7 +4,7 @@
* @dlam @yigit
# Owners for each library group:
-/activity* @sanura-njaka @jbw0033 @ianhanniballake
+/activity* @jbw0033 @ianhanniballake
/appcompat/* @alanv
/biometric/* @jbolinger
/collection/* @dlam
@@ -12,7 +12,7 @@
/compose/runtime/* @jimgoog @lelandrichardson
/core/* @alanv
/datastore/* @rohitsat13 @yigit
-/fragment/* @sanura-njaka @jbw0033 @ianhanniballake
+/fragment/* @jbw0033 @ianhanniballake
/lifecycle/* @jbw0033 @ianhanniballake
/navigation/* @jbw0033 @ianhanniballake @claraf3
/paging/* @claraf3 @ianhanniballake
diff --git a/annotation/annotation-experimental-lint/build.gradle b/annotation/annotation-experimental-lint/build.gradle
index 4deb784..d10e6d1 100644
--- a/annotation/annotation-experimental-lint/build.gradle
+++ b/annotation/annotation-experimental-lint/build.gradle
@@ -48,7 +48,6 @@
androidx {
name = "Experimental annotation lint checks"
type = LibraryType.LINT
- mavenVersion = LibraryVersions.ANNOTATION_EXPERIMENTAL
inceptionYear = "2019"
description = "Lint checks for the Experimental annotation library. Also enforces the " +
"semantics of Kotlin @Experimental APIs from within Android Java source code."
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
index 22093b6..719a480 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
@@ -73,7 +73,6 @@
import org.jetbrains.uast.UastFacade
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getContainingUMethod
-import org.jetbrains.uast.java.JavaUAnnotation
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.toUElementOfType
import org.jetbrains.uast.tryResolve
@@ -376,7 +375,7 @@
}
if (relevantAnnotations.contains(signature)) {
- val uAnnotation = JavaUAnnotation.wrap(annotation)
+ val uAnnotation = annotation.toUElementOfType<UAnnotation>() ?: continue
// Common case: there's just one annotation; no need to create a list copy
if (length == 1) {
@@ -888,7 +887,7 @@
): Boolean = optInFqNames.any { optInFqName ->
annotations.any { annotation ->
annotation.hasQualifiedName(optInFqName) &&
- ((annotation.toUElement() as? UAnnotation)?.hasMatchingAttributeValueClass(
+ ((annotation.toUElementOfType<UAnnotation>())?.hasMatchingAttributeValueClass(
"markerClass",
annotationFqName,
) ?: false)
diff --git a/annotation/annotation-replacewith-lint/integration-tests/build.gradle b/annotation/annotation-replacewith-lint/integration-tests/build.gradle
deleted file mode 100644
index 4211c69..0000000
--- a/annotation/annotation-replacewith-lint/integration-tests/build.gradle
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.
- */
-
-import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-def isIdeBuild() {
- return project.properties['android.injected.invoked.from.ide'] == 'true'
-}
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("kotlin-android")
-}
-
-android {
- namespace "androidx.annotation.replacewith.lint.integrationtests"
-}
-
-dependencies {
- implementation(libs.kotlinStdlib)
- implementation(project(":annotation:annotation-replacewith"))
-}
-
-androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
-}
diff --git a/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
deleted file mode 100644
index 9ebd6a0..0000000
--- a/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
+++ /dev/null
@@ -1 +0,0 @@
-androidx.annotation.replacewith.lint.ReplaceWithIssueRegistry
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt b/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt
deleted file mode 100644
index 938b868..0000000
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.annotation.replacewith.lint
-
-import com.android.tools.lint.client.api.LintClient
-import com.android.tools.lint.detector.api.CURRENT_API
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ApiLintVersionsTest {
-
- @Test
- fun versionsCheck() {
- LintClient.clientName = LintClient.CLIENT_UNIT_TESTS
-
- val registry = ReplaceWithIssueRegistry()
- // We hardcode version registry.api to the version that is used to run tests.
- assertEquals("registry.api matches version used to run tests", CURRENT_API, registry.api)
- // Intentionally fails in IDE, because we use different API version in Studio and CLI.
- assertEquals("registry.minApi is set to minimum level of 10", 10, registry.minApi)
- }
-}
diff --git a/annotation/annotation-replacewith/api/current.txt b/annotation/annotation-replacewith/api/current.txt
deleted file mode 100644
index 1147234..0000000
--- a/annotation/annotation-replacewith/api/current.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 4.0
-package androidx.annotation {
-
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.CONSTRUCTOR}) public @interface ReplaceWith {
- method public abstract String expression();
- }
-
-}
-
diff --git a/annotation/annotation-replacewith/api/res-current.txt b/annotation/annotation-replacewith/api/res-current.txt
deleted file mode 100644
index e69de29..0000000
--- a/annotation/annotation-replacewith/api/res-current.txt
+++ /dev/null
diff --git a/annotation/annotation-replacewith/api/restricted_current.txt b/annotation/annotation-replacewith/api/restricted_current.txt
deleted file mode 100644
index 1147234..0000000
--- a/annotation/annotation-replacewith/api/restricted_current.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 4.0
-package androidx.annotation {
-
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.CONSTRUCTOR}) public @interface ReplaceWith {
- method public abstract String expression();
- }
-
-}
-
diff --git a/annotation/annotation-replacewith/build.gradle b/annotation/annotation-replacewith/build.gradle
deleted file mode 100644
index 97cd77b..0000000
--- a/annotation/annotation-replacewith/build.gradle
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.
- */
-
-
-import androidx.build.KotlinTarget
-import androidx.build.Publish
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- api(libs.kotlinStdlib)
- lintPublish(project(":annotation:annotation-replacewith-lint"))
-}
-
-androidx {
- name = "ReplaceWith annotation"
- publish = Publish.SNAPSHOT_AND_RELEASE
- mavenVersion = LibraryVersions.ANNOTATION_REPLACEWITH
- kotlinTarget = KotlinTarget.KOTLIN_1_7
- inceptionYear = "2019"
- description = "Java annotation for use on deprecated API surfaces with a replacement. When " +
- "used in conjunction with the ReplaceWith annotation lint checks, this annotation " +
- "provides functional parity with Kotlin's Deprecated annotation ReplaceWith feature."
- metalavaK2UastEnabled = true
-}
-
-android {
- namespace "androidx.annotation.replacewith"
-}
diff --git a/annotation/annotation/api/current.txt b/annotation/annotation/api/current.txt
index 71556c1..b5d93db 100644
--- a/annotation/annotation/api/current.txt
+++ b/annotation/annotation/api/current.txt
@@ -241,6 +241,13 @@
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
}
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+ method public abstract String expression();
+ method public abstract String[] imports();
+ property public abstract String expression;
+ property public abstract String[] imports;
+ }
+
@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
method public abstract int api() default 1;
method public abstract int value() default 1;
diff --git a/annotation/annotation/api/restricted_current.txt b/annotation/annotation/api/restricted_current.txt
index 71556c1..b5d93db 100644
--- a/annotation/annotation/api/restricted_current.txt
+++ b/annotation/annotation/api/restricted_current.txt
@@ -241,6 +241,13 @@
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
}
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+ method public abstract String expression();
+ method public abstract String[] imports();
+ property public abstract String expression;
+ property public abstract String[] imports;
+ }
+
@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
method public abstract int api() default 1;
method public abstract int value() default 1;
diff --git a/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt b/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
index 1c29d6a..c988b44 100644
--- a/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
+++ b/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
@@ -91,7 +91,7 @@
@Deprecated(
message = "Use @RestrictTo(LIBRARY_GROUP_PREFIX) instead",
replaceWith =
- ReplaceWith(
+ kotlin.ReplaceWith(
"LIBRARY_GROUP_PREFIX",
"androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX"
)
diff --git a/annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
similarity index 62%
rename from annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java
rename to annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
index a9c70b9..6b45187 100644
--- a/annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java
+++ b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
@@ -13,22 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.annotation;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-
-import java.lang.annotation.Target;
+package androidx.annotation
/**
- * Specifies a code fragment that can be used to suggest a replacement for a method in
- * conjunction with the `ReplaceWith` lint check.
- * <p>
- * The {@code expression} parameter specified the replacement expression, which is interpreted in
- * the context of the symbol being used and can reference members of the enclosing classes, etc.
- * <p>
+ * Specifies a code fragment that can be used to suggest a replacement for a method in conjunction
+ * with the `ReplaceWith` lint check.
+ *
+ * The `expression` parameter specified the replacement expression, which is interpreted in the
+ * context of the symbol being used and can reference members of the enclosing classes, etc.
+ *
* For method calls, the replacement expression may contain parameter names of the method being
* replaced, which will be substituted with actual arguments used in the call being replaced:
* <pre>
@@ -36,7 +29,9 @@
* static int getActionType(AccessibilityEvent event, int slot) { ... }
* </pre>
*/
-@Target({METHOD, FIELD, CONSTRUCTOR})
-public @interface ReplaceWith {
- String expression();
-}
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.CONSTRUCTOR
+)
+public annotation class ReplaceWith(val expression: String, vararg val imports: String = [])
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
index 8e12de3..9cf5964 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
@@ -222,7 +222,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 28, maxSdkVersion = 33) // maxSdk 33 b/322355781
@RequiresApi(28)
public void testOnApplyWindowInsetsReachesContent_withDisplayCutout() throws Throwable {
final A activity = mActivityTestRule.getActivity();
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
index e5364f8..3e8b934 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
@@ -18,6 +18,7 @@
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import org.junit.Test;
@@ -27,6 +28,7 @@
super(WindowDecorAppCompatActivity.class);
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322355781
@Test
@UiThreadTest
public void testSupportActionModeAppCompatCallbacks() {
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
index 1b9b9b1..4ce752e 100755
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
@@ -56,6 +56,7 @@
import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.Suppress;
import androidx.test.rule.ActivityTestRule;
@@ -484,6 +485,7 @@
mDrawerLayout.removeDrawerListener(mockedListener);
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321813959
@Test
@LargeTest
public void testDrawerListenerCallbacksOnClosingViaSwipes() {
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 aab26d0..ea70bdf 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
@@ -161,8 +161,8 @@
}
var applied = false
- variants.all {
- if (applied) return@all
+ variants.configureEach {
+ if (applied) return@configureEach
applied = true
// Execute all the scheduled variant blocks
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 5dfd830..ebe0a317 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
@@ -20,7 +20,7 @@
// Minimum AGP version required
internal val MIN_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 3, 0)
+internal val MAX_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 4, 0)
// Prefix for the build type baseline profile
internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt b/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
index f61a216..e9b01a2 100644
--- a/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
+++ b/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
@@ -35,3 +35,4 @@
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC log)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE dl)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC android)
+target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index a6ad725..05bf753 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -40,7 +40,7 @@
project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {
val multiplatformExtension: KotlinMultiplatformExtension =
project.extensions.getByType(it.projectExtensionClass.java)
- multiplatformExtension.targets.all { kotlinTarget ->
+ multiplatformExtension.targets.configureEach { kotlinTarget ->
if (kotlinTarget is KotlinNativeTarget) {
if (kotlinTarget.konanTarget.family.isAppleFamily) {
// We want to apply the plugin only once.
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 3800229..0bb3341 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -45,6 +45,10 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
}
+ @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
+ ctor public FrameTimingGfxInfoMetric();
+ }
+
public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
ctor public FrameTimingMetric();
}
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index f96634cf..b7caaa5 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -58,6 +58,10 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
}
+ @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
+ ctor public FrameTimingGfxInfoMetric();
+ }
+
public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
ctor public FrameTimingMetric();
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
index be313c4..e56cea9 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
@@ -18,6 +18,7 @@
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationCpuNs
+import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationFullNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationUiNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameOverrunNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.getFrameSubMetrics
@@ -26,6 +27,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
+import kotlin.test.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -82,16 +84,17 @@
assertEquals(
expected = mapOf(
- FrameDurationCpuNs to listOf(6881407L, 5648542L, 3830261L, 4343438L),
FrameDurationUiNs to listOf(2965052L, 3246407L, 1562188L, 1945469L),
- FrameOverrunNs to listOf(-5207137L, -11699862L, -14025295L, -12300155L)
+ FrameDurationCpuNs to listOf(6881407L, 5648542L, 3830261L, 4343438L),
+ FrameDurationFullNs to listOf(15292863L, 8800138L, 6474705L, 8199845L),
+ FrameOverrunNs to listOf(-5207137L, -11699862L, -14025295L, -12300155L),
),
actual = frameSubMetrics.mapValues {
it.value.subList(0, 4)
}
)
assertEquals(
- expected = List(3) { 96 },
+ expected = List(FrameTimingQuery.SubMetric.values().size) { 96 },
actual = frameSubMetrics.map { it.value.size },
message = "Expect same number of frames for each metric"
)
@@ -142,15 +145,45 @@
assertEquals(
// Note: it's correct for UI to be > CPU in cases below,
- // since UI is be sleeping after RT is done
+ // since UI is sleeping after RT is done
expected = mapOf(
- FrameDurationCpuNs to listOf(7304479L, 7567188L, 8064897L, 8434115L),
FrameDurationUiNs to listOf(4253646L, 7592761L, 8088855L, 8461876L),
- FrameOverrunNs to listOf(-9009770L, -12199949L, -11299378L, -11708522L)
+ FrameDurationCpuNs to listOf(7304479L, 7567188L, 8064897L, 8434115L),
+ FrameDurationFullNs to listOf(11490230L, 8300051L, 9200622L, 8791478L),
+ FrameOverrunNs to listOf(-9009770L, -12199949L, -11299378L, -11708522L),
),
actual = frameData.getFrameSubMetrics(captureApiLevel = 33).mapValues {
it.value.subList(0, 4)
}
)
}
+
+ @MediumTest
+ @Test
+ fun fixedTrace34_invalidExpectActual() {
+ assumeTrue(isAbiSupported())
+ val traceFile =
+ createTempFileFromAsset("api34_invalid_expect_actual", ".perfetto-trace")
+
+ val frameData = PerfettoTraceProcessor.runSingleSessionServer(
+ traceFile.absolutePath
+ ) {
+ FrameTimingQuery.getFrameData(
+ session = this,
+ captureApiLevel = 34,
+ packageName = "androidx.compose.integration.macrobenchmark.target"
+ )
+ }
+
+ assertTrue(
+ frameData
+ .getFrameSubMetrics(34)[FrameOverrunNs]!!
+ .all { it < 50_000_000 }
+ )
+ assertTrue(
+ frameData.none {
+ it.actualSlice!!.frameId == 110752 || it.expectedSlice!!.frameId == 110752
+ }
+ )
+ }
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
new file mode 100644
index 0000000..d0b11e8
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
@@ -0,0 +1,378 @@
+/*
+ * 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.benchmark.macro;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Collects jank metrics for all or a list of processes. */
+class JankCollectionHelper {
+
+ private static final String LOG_TAG = JankCollectionHelper.class.getSimpleName();
+
+ // Prefix for all output metrics that come from the gfxinfo dump.
+ @VisibleForTesting static final String GFXINFO_METRICS_PREFIX = "gfxinfo";
+ // Shell dump commands to get and reset the tracked gfxinfo metrics.
+ @VisibleForTesting static final String GFXINFO_COMMAND_GET = "dumpsys gfxinfo %s";
+ @VisibleForTesting static final String GFXINFO_COMMAND_RESET = GFXINFO_COMMAND_GET + " reset";
+ // Pattern matchers and enumerators to verify and pull gfxinfo metrics.
+ // Example: "** Graphics info for pid 853 [com.google.android.leanbacklauncher] **"
+ private static final String GFXINFO_OUTPUT_HEADER = "Graphics info for pid (\\d+) \\[(%s)\\]";
+ // Note: use the [\\s\\S]* multi-line matcher to support String#matches(). Instead of splitting
+ // the larger sections into more granular lines, we can match across all lines for simplicity.
+ private static final String MULTILINE_MATCHER = "[\\s\\S]*%s[\\s\\S]*";
+
+ public enum GfxInfoMetric {
+ // Example: "Total frames rendered: 20391"
+ TOTAL_FRAMES(
+ Pattern.compile(".*Total frames rendered: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "total_frames"),
+ // Example: "Janky frames: 785 (3.85%)"
+ JANKY_FRAMES_COUNT(
+ Pattern.compile(
+ ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
+ 1,
+ "janky_frames_count"),
+ // Example: "Janky frames: 785 (3.85%)"
+ JANKY_FRAMES_PRCNT(
+ Pattern.compile(
+ ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
+ 2,
+ "janky_frames_percent"),
+ // Example: "Janky frames (legacy): 785 (3.85%)"
+ JANKY_FRAMES_LEGACY_COUNT(
+ Pattern.compile(
+ ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+ Pattern.DOTALL),
+ 1,
+ "janky_frames_legacy_count"),
+ // Example: "Janky frames (legacy): 785 (3.85%)"
+ JANKY_FRAMES_LEGACY_PRCNT(
+ Pattern.compile(
+ ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+ Pattern.DOTALL),
+ 2,
+ "janky_frames_legacy_percent"),
+ // Example: "50th percentile: 9ms"
+ FRAME_TIME_50TH(
+ Pattern.compile(".*50th percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "frame_render_time_percentile_50"),
+ // Example: "90th percentile: 9ms"
+ FRAME_TIME_90TH(
+ Pattern.compile(".*90th percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "frame_render_time_percentile_90"),
+ // Example: "95th percentile: 9ms"
+ FRAME_TIME_95TH(
+ Pattern.compile(".*95th percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "frame_render_time_percentile_95"),
+ // Example: "99th percentile: 9ms"
+ FRAME_TIME_99TH(
+ Pattern.compile(".*99th percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "frame_render_time_percentile_99"),
+ // Example: "Number Missed Vsync: 0"
+ NUM_MISSED_VSYNC(
+ Pattern.compile(".*Number Missed Vsync: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "missed_vsync"),
+ // Example: "Number High input latency: 0"
+ NUM_HIGH_INPUT_LATENCY(
+ Pattern.compile(".*Number High input latency: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "high_input_latency"),
+ // Example: "Number Slow UI thread: 0"
+ NUM_SLOW_UI_THREAD(
+ Pattern.compile(".*Number Slow UI thread: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "slow_ui_thread"),
+ // Example: "Number Slow bitmap uploads: 0"
+ NUM_SLOW_BITMAP_UPLOADS(
+ Pattern.compile(".*Number Slow bitmap uploads: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "slow_bmp_upload"),
+ // Example: "Number Slow issue draw commands: 0"
+ NUM_SLOW_DRAW(
+ Pattern.compile(".*Number Slow issue draw commands: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "slow_issue_draw_cmds"),
+ // Example: "Number Frame deadline missed: 0"
+ NUM_FRAME_DEADLINE_MISSED(
+ Pattern.compile(".*Number Frame deadline missed: (\\d+).*", Pattern.DOTALL),
+ 1,
+ "deadline_missed"),
+ // Number Frame deadline missed (legacy): 0
+ NUM_FRAME_DEADLINE_MISSED_LEGACY(
+ Pattern.compile(
+ ".*Number Frame deadline missed \\(legacy\\): (\\d+).*", Pattern.DOTALL),
+ 1,
+ "deadline_missed_legacy"),
+ // Example: "50th gpu percentile: 9ms"
+ GPU_FRAME_TIME_50TH(
+ Pattern.compile(".*50th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "gpu_frame_render_time_percentile_50"),
+ // Example: "90th gpu percentile: 9ms"
+ GPU_FRAME_TIME_90TH(
+ Pattern.compile(".*90th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "gpu_frame_render_time_percentile_90"),
+ // Example: "95th gpu percentile: 9ms"
+ GPU_FRAME_TIME_95TH(
+ Pattern.compile(".*95th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "gpu_frame_render_time_percentile_95"),
+ // Example: "99th gpu percentile: 9ms"
+ GPU_FRAME_TIME_99TH(
+ Pattern.compile(".*99th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+ 1,
+ "gpu_frame_render_time_percentile_99");
+
+ private final Pattern mPattern;
+ private final int mGroupIndex;
+ private final String mMetricId;
+
+ GfxInfoMetric(Pattern pattern, int groupIndex, String metricId) {
+ mPattern = pattern;
+ mGroupIndex = groupIndex;
+ mMetricId = metricId;
+ }
+
+ @Nullable
+ public Double parse(@NonNull String lines) {
+ Matcher matcher = mPattern.matcher(lines);
+ if (matcher.matches()) {
+ return Double.valueOf(matcher.group(mGroupIndex));
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ public String getMetricId() {
+ return mMetricId;
+ }
+ }
+
+ private final Set<String> mTrackedPackages = new HashSet<>();
+ private UiDevice mDevice;
+
+ /** Clear existing jank metrics, unless explicitly configured. */
+ public boolean startCollecting() {
+ if (mTrackedPackages.isEmpty()) {
+ clearGfxInfo();
+ } else {
+ int exceptionCount = 0;
+ Exception lastException = null;
+ for (String pkg : mTrackedPackages) {
+ try {
+ clearGfxInfo(pkg);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Encountered exception resetting gfxinfo.", e);
+ lastException = e;
+ exceptionCount++;
+ }
+ }
+ // Throw exceptions after to not quit on a single failure.
+ if (exceptionCount > 1) {
+ throw new RuntimeException(
+ "Multiple exceptions were encountered resetting gfxinfo. Reporting the last"
+ + " one only; others are visible in logs.",
+ lastException);
+ } else if (exceptionCount == 1) {
+ throw new RuntimeException(
+ "Encountered exception resetting gfxinfo.", lastException);
+ }
+ }
+ // No exceptions denotes success.
+ return true;
+ }
+
+ /** Collect the {@code gfxinfo} metrics for tracked processes (or all, if unspecified). */
+ @NonNull
+ public Map<String, Double> getMetrics() {
+ Map<String, Double> result = new HashMap<>();
+ if (mTrackedPackages.isEmpty()) {
+ result.putAll(getGfxInfoMetrics());
+ } else {
+ int exceptionCount = 0;
+ Exception lastException = null;
+ for (String pkg : mTrackedPackages) {
+ try {
+ result.putAll(getGfxInfoMetrics(pkg));
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Encountered exception getting gfxinfo.", e);
+ lastException = e;
+ exceptionCount++;
+ }
+ }
+ // Throw exceptions after to ensure all failures are reported. The metrics will still
+ // not be collected at this point, but it will possibly make the issue cause clearer.
+ if (exceptionCount > 1) {
+ throw new RuntimeException(
+ "Multiple exceptions were encountered getting gfxinfo. Reporting the last"
+ + " one only; others are visible in logs.",
+ lastException);
+ } else if (exceptionCount == 1) {
+ throw new RuntimeException("Encountered exception getting gfxinfo.", lastException);
+ }
+ }
+ return result;
+ }
+
+ /** Do nothing, because nothing is needed to disable jank. */
+ public boolean stopCollecting() {
+ return true;
+ }
+
+ /** Add a package or list of packages to be tracked. */
+ public void addTrackedPackages(@NonNull String... packages) {
+ Collections.addAll(mTrackedPackages, packages);
+ }
+
+ /** Clear the {@code gfxinfo} for all packages. */
+ @VisibleForTesting
+ void clearGfxInfo() {
+ // Not specifying a package will clear everything.
+ clearGfxInfo("");
+ }
+
+ /** Clear the {@code gfxinfo} for the {@code pkg} specified. */
+ @VisibleForTesting
+ void clearGfxInfo(String pkg) {
+ try {
+ if (pkg.isEmpty()) {
+ String command = String.format(GFXINFO_COMMAND_RESET, "--");
+ String output = getDevice().executeShellCommand(command);
+ // Success if any header (set by passing an empty-string) exists in the output.
+ verifyMatches(output, getHeaderMatcher(""), "No package headers in output.");
+ Log.v(LOG_TAG, "Cleared all gfxinfo.");
+ } else {
+ String command = String.format(GFXINFO_COMMAND_RESET, pkg);
+ String output = getDevice().executeShellCommand(command);
+ // Success if the specified package header exists in the output.
+ verifyMatches(output, getHeaderMatcher(pkg), "No package header in output.");
+ Log.v(LOG_TAG, String.format("Cleared %s gfxinfo.", pkg));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to clear gfxinfo.", e);
+ }
+ }
+
+ /** Return a {@code Map<String, Double>} of {@code gfxinfo} metrics for all processes. */
+ @VisibleForTesting
+ Map<String, Double> getGfxInfoMetrics() {
+ return getGfxInfoMetrics("");
+ }
+
+ /** Return a {@code Map<String, Double>} of {@code gfxinfo} metrics for {@code pkg}. */
+ @VisibleForTesting
+ @SuppressWarnings("StringSplitter")
+ Map<String, Double> getGfxInfoMetrics(String pkg) {
+ try {
+ String command = String.format(GFXINFO_COMMAND_GET, pkg);
+ String output = getDevice().executeShellCommand(command);
+ verifyMatches(output, getHeaderMatcher(pkg), "Missing package header.");
+ // Split each new section starting with two asterisks '**', and then query and append
+ // all metrics. This method supports both single-package and multi-package outputs.
+ String[] pkgMetricSections = output.split("\n\\*\\*");
+ Map<String, Double> result = new HashMap<>();
+ // Skip the 1st section, which contains only header information.
+ for (int i = 1; i < pkgMetricSections.length; i++) {
+ result.putAll(parseGfxInfoMetrics(pkgMetricSections[i]));
+ }
+ return result;
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to get gfxinfo.", e);
+ }
+ }
+
+ /** Parse the {@code output} of {@code gfxinfo} to a {@code Map<String, Double>} of metrics. */
+ private Map<String, Double> parseGfxInfoMetrics(String output) {
+ Matcher header = Pattern.compile(getHeaderMatcher("")).matcher(output);
+ if (!header.matches()) {
+ throw new RuntimeException("Failed to parse package from gfxinfo output.");
+ }
+ // Package name is the only required field.
+ String packageName = header.group(2);
+ Log.v(LOG_TAG, String.format("Collecting metrics for: %s", packageName));
+ // Parse each metric from the results via a common pattern.
+ Map<String, Double> results = new HashMap<String, Double>();
+ for (GfxInfoMetric metric : GfxInfoMetric.values()) {
+ String metricKey =
+ constructKey(GFXINFO_METRICS_PREFIX, packageName, metric.getMetricId());
+ // Find the metric or log that it's missing.
+ Double value = metric.parse(output);
+ if (value == null) {
+ Log.d(LOG_TAG, String.format("Did not find %s from %s", metricKey, packageName));
+ } else {
+ results.put(metricKey, value);
+ }
+ }
+ return results;
+ }
+
+ private String constructKey(@NonNull String ...tokens) {
+ return TextUtils.join("_", tokens);
+ }
+
+ /**
+ * Returns a matcher {@code String} for {@code pkg}'s {@code gfxinfo} headers.
+ *
+ * <p>Note: {@code pkg} may be empty.
+ */
+ private String getHeaderMatcher(String pkg) {
+ return String.format(
+ MULTILINE_MATCHER,
+ String.format(GFXINFO_OUTPUT_HEADER, (pkg.isEmpty() ? ".*" : pkg)));
+ }
+
+ /** Verify the {@code output} matches {@code match}, or throw if not. */
+ private void verifyMatches(String output, String match, String message, Object... args) {
+ if (!output.matches(match)) {
+ throw new IllegalStateException(String.format(message, args));
+ }
+ }
+
+ /** Returns the {@link UiDevice} under test. */
+ @NonNull
+ @VisibleForTesting
+ protected UiDevice getDevice() {
+ if (mDevice == null) {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+ return mDevice;
+ }
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 7423f68..260e340 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -16,13 +16,13 @@
package androidx.benchmark.macro
-import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.Shell
import androidx.benchmark.macro.BatteryCharge.hasMinimumCharge
import androidx.benchmark.macro.PowerMetric.Type
import androidx.benchmark.macro.PowerRail.hasMetrics
+import androidx.benchmark.macro.TraceSectionMetric.Mode
import androidx.benchmark.macro.perfetto.BatteryDischargeQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
@@ -151,10 +151,10 @@
): List<Measurement> {
return FrameTimingQuery.getFrameData(
session = traceSession,
- captureApiLevel = Build.VERSION.SDK_INT,
+ captureApiLevel = captureInfo.apiLevel,
packageName = captureInfo.targetPackageName
)
- .getFrameSubMetrics(Build.VERSION.SDK_INT)
+ .getFrameSubMetrics(captureInfo.apiLevel)
.filterKeys { it == SubMetric.FrameDurationCpuNs || it == SubMetric.FrameOverrunNs }
.map {
Measurement(
@@ -170,6 +170,116 @@
}
/**
+ * Version of FrameTimingMetric based on 'dumpsys gfxinfo' instead of trace data.
+ *
+ * Added for experimentation in contrast to FrameTimingMetric, as the platform accounting of frame
+ * drops currently behaves differently from that of FrameTimingMetric.
+ *
+ * Likely to be removed when differences in jank behavior are reconciled between this class, and
+ * [FrameTimingMetric].
+ *
+ * Note that output metrics do not match perfectly to FrameTimingMetric, as individual frame times
+ * are not available, only high level, millisecond-precision statistics.
+ */
+@ExperimentalMetricApi
+class FrameTimingGfxInfoMetric : Metric() {
+ private lateinit var packageName: String
+ private val helper = JankCollectionHelper()
+ private var metrics = mutableMapOf<String, Double>()
+
+ override fun configure(packageName: String) {
+ this.packageName = packageName
+ helper.addTrackedPackages(packageName)
+ }
+
+ override fun start() {
+ try {
+ helper.startCollecting()
+ } catch (exception: RuntimeException) {
+ // Ignore the exception that might result from trying to clear GfxInfo
+ // The current implementation of JankCollectionHelper throws a RuntimeException
+ // when that happens. This is safe to ignore because the app being benchmarked
+ // is not showing any UI when this happens typically.
+
+ // Once the MacroBenchmarkRule has the ability to setup the app in the right state via
+ // a designated setup block, we can get rid of this.
+ if (!Shell.isPackageAlive(packageName)) {
+ error(exception.message ?: "Assertion error, $packageName not running")
+ }
+ }
+ }
+
+ override fun stop() {
+ helper.stopCollecting()
+
+ // save metrics on stop to attempt to more closely match perfetto based metrics
+ metrics.clear()
+ metrics.putAll(helper.metrics)
+ }
+
+ /**
+ * Used to convert keys from platform to JSON format.
+ *
+ * This both converts `snake_case_format` to `camelCaseFormat`, and renames for clarity.
+ *
+ * Note that these will still output to inst results in snake_case, with `MetricNameUtils`
+ * via [androidx.benchmark.MetricResult.putInBundle].
+ */
+ private val keyRenameMap = mapOf(
+ "frame_render_time_percentile_50" to "gfxFrameTime50thPercentileMs",
+ "frame_render_time_percentile_90" to "gfxFrameTime90thPercentileMs",
+ "frame_render_time_percentile_95" to "gfxFrameTime95thPercentileMs",
+ "frame_render_time_percentile_99" to "gfxFrameTime99thPercentileMs",
+ "gpu_frame_render_time_percentile_50" to "gpuFrameTime50thPercentileMs",
+ "gpu_frame_render_time_percentile_90" to "gpuFrameTime90thPercentileMs",
+ "gpu_frame_render_time_percentile_95" to "gpuFrameTime95thPercentileMs",
+ "gpu_frame_render_time_percentile_99" to "gpuFrameTime99thPercentileMs",
+ "missed_vsync" to "vsyncMissedFrameCount",
+ "deadline_missed" to "deadlineMissedFrameCount",
+ "deadline_missed_legacy" to "deadlineMissedFrameCountLegacy",
+ "janky_frames_count" to "jankyFrameCount",
+ "janky_frames_legacy_count" to "jankyFrameCountLegacy",
+ "high_input_latency" to "highInputLatencyFrameCount",
+ "slow_ui_thread" to "slowUiThreadFrameCount",
+ "slow_bmp_upload" to "slowBitmapUploadFrameCount",
+ "slow_issue_draw_cmds" to "slowIssueDrawCommandsFrameCount",
+ "total_frames" to "gfxFrameTotalCount",
+ "janky_frames_percent" to "gfxFrameJankPercent",
+ "janky_frames_legacy_percent" to "jankyFramePercentLegacy"
+ )
+
+ /**
+ * Filters output to only frameTimeXXthPercentileMs and totalFrameCount
+ */
+ private val keyAllowList = setOf(
+ "gfxFrameTime50thPercentileMs",
+ "gfxFrameTime90thPercentileMs",
+ "gfxFrameTime95thPercentileMs",
+ "gfxFrameTime99thPercentileMs",
+ "gfxFrameTotalCount",
+ "gfxFrameJankPercent",
+ )
+
+ override fun getResult(
+ captureInfo: CaptureInfo,
+ traceSession: PerfettoTraceProcessor.Session
+ ): List<Measurement> {
+ return metrics
+ .map {
+ val prefix = "gfxinfo_${packageName}_"
+ val keyWithoutPrefix = it.key.removePrefix(prefix)
+
+ if (keyWithoutPrefix != it.key && keyRenameMap.containsKey(keyWithoutPrefix)) {
+ Measurement(keyRenameMap[keyWithoutPrefix]!!, it.value)
+ } else {
+ throw IllegalStateException("Unexpected key ${it.key}")
+ }
+ }
+ .filter { keyAllowList.contains(it.name) }
+ }
+}
+
+/**
* Captures app startup timing metrics.
*
* This outputs the following measurements:
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
index 7cec6f9..77deebb 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
@@ -63,12 +63,17 @@
""".trimIndent()
enum class SubMetric {
+ // Duration of UI thread
FrameDurationCpuNs,
+ // Total duration from UI through RT slice
FrameDurationUiNs,
- FrameOverrunNs;
+ // How much longer did frame take than expected
+ FrameOverrunNs,
+ // Total duration from expected frame start through true end of frame
+ FrameDurationFullNs;
fun supportedOnApiLevel(apiLevel: Int): Boolean {
- return apiLevel >= 31 || this != FrameOverrunNs
+ return apiLevel >= 31 || this != FrameOverrunNs && this != FrameDurationFullNs
}
}
@@ -98,6 +103,10 @@
// workaround b/279088460, where actual slice ends too early
maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.endTs
}
+ SubMetric.FrameDurationFullNs -> {
+ // workaround b/279088460, where actual slice ends too early
+ maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.ts
+ }
}
}
companion object {
@@ -209,13 +218,19 @@
// the complete end of frame is present, and we want to discard those. This
// doesn't happen at front of trace, since we find actuals from the end.
if (uiSlice != null) {
- // Use fixed offset since synthetic tracepoint for actual may start after the
- // actual UI slice (have observed 2us in practice)
- val actualSlice = actualSlicesPool.lastOrNull { it.ts < uiSlice.ts + 50_000 }
+ val actualSlice = actualSlicesPool.lastOrNull {
+ // Use fixed offset since synthetic tracepoint for actual may start after the
+ // actual UI slice (have observed 2us in practice)
+ it.ts < uiSlice.ts + 50_000 &&
+ // ensure there's some overlap - if actual doesn't contain ui, may just
+ // be "abandoned" slice at beginning of trace
+ it.contains(uiSlice.ts + (uiSlice.dur / 2))
+ }
actualSlicesPool.remove(actualSlice)
val expectedSlice = actualSlice?.frameId?.run {
expectedSlices.binarySearchFrameId(this)
}
+
FrameData.tryCreate31(
uiSlice = uiSlice,
rtSlice = rtSlice,
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index 5661083..b2e0efe 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -133,10 +133,10 @@
)
}
- // NOTE: .all here is a Gradle API, which will run the callback passed to it after the
- // extension variants have been resolved.
+ // NOTE: .configureEach here is a Gradle API, which will run the callback passed to it after
+ // the extension variants have been resolved.
var applied = false
- extensionVariants.all {
+ extensionVariants.configureEach {
if (!applied) {
applied = true
diff --git a/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt b/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
index 2209002b..77e83fd 100644
--- a/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
@@ -22,6 +22,7 @@
import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
+import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.junit.Before
import org.junit.Test
@@ -114,6 +115,26 @@
assertThat(strings).contains("Hello, World!")
// should link with libc
assertThat(strings).contains("libc")
+
+ // verify shared lib files are aligned to 16Kb boundary for Android targets
+ if (sharedLibraryParameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
+ val alignment = ProcessBuilder("objdump", "-p", outputFile.path)
+ .start()
+ .inputStream
+ .bufferedReader()
+ .useLines { lines ->
+ lines.filter {
+ it.contains("LOAD")
+ }.map {
+ it.split(" ").last()
+ }.firstOrNull()
+ }
+ assertThat(
+ alignment
+ ).isEqualTo(
+ "2**14"
+ )
+ }
}
@Test
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index bec1d87..06824dc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -29,23 +29,16 @@
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.attributes.Attribute
import org.gradle.api.plugins.ExtraPropertiesExtension
-import org.gradle.api.tasks.ClasspathNormalizer
import org.gradle.api.tasks.bundling.Zip
import org.gradle.kotlin.dsl.create
+import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-const val composeSourceOption =
- "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
-const val composeMetricsOption =
- "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination"
-const val composeReportsOption =
- "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination"
const val zipComposeReportsTaskName = "zipComposeCompilerReports"
const val zipComposeMetricsTaskName = "zipComposeCompilerMetrics"
-const val composeStrongSkippingOption =
- "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping"
/** Plugin to apply common configuration for Compose projects. */
class AndroidXComposeImplPlugin : Plugin<Project> {
@@ -232,7 +225,7 @@
"androidx.compose.compiler:compiler:$versionToUse"
)
- val kotlinPlugin =
+ val kotlinPluginProvider = project.provider {
configuration.incoming
.artifactView { view ->
view.attributes { attributes ->
@@ -243,6 +236,7 @@
}
}
.files
+ }
val enableMetrics = project.enableComposeCompilerMetrics()
val enableReports = project.enableComposeCompilerReports()
@@ -250,64 +244,55 @@
val compileTasks = project.tasks.withType(KotlinCompile::class.java)
compileTasks.configureEach { compile ->
- // Append inputs to KotlinCompile so tasks get invalidated if any of these values change
- compile.inputs
- .files({ kotlinPlugin })
- .withPropertyName("composeCompilerExtension")
- .withNormalizer(ClasspathNormalizer::class.java)
compile.inputs.property("composeMetricsEnabled", enableMetrics)
compile.inputs.property("composeReportsEnabled", enableReports)
- // Gradle hack ahead, we use of absolute paths, but is OK here because we do it in
- // doFirst which happens after Gradle task input snapshotting. AGP does the same.
- compile.doFirst {
- compile.kotlinOptions.freeCompilerArgs += "-Xplugin=${kotlinPlugin.first()}"
+ compile.pluginClasspath.from(kotlinPluginProvider.get())
+ compile.addPluginOption(ComposeCompileOptions.StrongSkippingOption, "true")
- // Enable Compose strong skipping mode
- compile.kotlinOptions.freeCompilerArgs +=
- listOf("-P", "$composeStrongSkippingOption=true")
-
- if (shouldPublish) {
- compile.kotlinOptions.freeCompilerArgs += listOf("-P", composeSourceOption)
- }
+ if (shouldPublish) {
+ compile.addPluginOption(ComposeCompileOptions.SourceOption, "true")
}
}
if (enableMetrics) {
- project.rootProject.tasks.named(zipComposeMetricsTaskName).configure({ zipTask ->
+ project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->
zipTask.dependsOn(compileTasks)
- })
+ }
val metricsIntermediateDir = project.compilerMetricsIntermediatesDir()
compileTasks.configureEach { compile ->
- compile.doFirst {
- compile.kotlinOptions.freeCompilerArgs +=
- listOf(
- "-P",
- "$composeMetricsOption=$metricsIntermediateDir"
- )
- }
+ compile.addPluginOption(
+ ComposeCompileOptions.MetricsOption, metricsIntermediateDir.path)
}
}
if (enableReports) {
- project.rootProject.tasks.named(zipComposeReportsTaskName).configure({ zipTask ->
+ project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->
zipTask.dependsOn(compileTasks)
- })
+ }
val reportsIntermediateDir = project.compilerReportsIntermediatesDir()
compileTasks.configureEach { compile ->
- compile.doFirst {
- compile.kotlinOptions.freeCompilerArgs +=
- listOf(
- "-P",
- "$composeReportsOption=$reportsIntermediateDir"
- )
- }
+ compile.addPluginOption(
+ ComposeCompileOptions.ReportsOption,
+ reportsIntermediateDir.path
+ )
}
}
}
}
+private fun KotlinCompile.addPluginOption(
+ composeCompileOptions: ComposeCompileOptions,
+ value: String
+) =
+ pluginOptions.add(CompilerPluginConfig().apply {
+ addPluginArgument(
+ composeCompileOptions.pluginId,
+ SubpluginOption(composeCompileOptions.key, value))
+ }
+)
+
public fun Project.zipComposeCompilerMetrics() {
if (project.enableComposeCompilerMetrics()) {
val zipComposeMetrics = project.tasks.register(zipComposeMetricsTaskName, Zip::class.java) {
@@ -347,3 +332,10 @@
fun Project.composeCompilerDataDir(): File {
return File(getDistributionDirectory(), "compose-compiler-data")
}
+
+private enum class ComposeCompileOptions(val pluginId: String, val key: String) {
+ SourceOption("androidx.compose.compiler.plugins.kotlin", "sourceInformation"),
+ MetricsOption("androidx.compose.compiler.plugins.kotlin", "metricsDestination"),
+ ReportsOption("androidx.compose.compiler.plugins.kotlin", "reportsDestination"),
+ StrongSkippingOption("androidx.compose.compiler.plugins.kotlin", "experimentalStrongSkipping");
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 9735b71..a10d912 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -123,6 +123,11 @@
const val HIGH_MEMORY = "androidx.highMemory"
/**
+ * Negates the HIGH_MEMORY flag
+ */
+const val LOW_MEMORY = "androidx.lowMemory"
+
+/**
* If true, don't require lint-checks project to exist. This should only be set in integration
* tests, to allow them to save time by not configuring extra projects.
*/
@@ -163,6 +168,7 @@
DISPLAY_TEST_OUTPUT,
ENABLE_DOCUMENTATION,
HIGH_MEMORY,
+ LOW_MEMORY,
STUDIO_TYPE,
SUMMARIZE_STANDARD_ERROR,
USE_MAX_DEP_VERSIONS,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 9a59c99..010d52d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -927,6 +927,11 @@
@Suppress("DEPRECATION")
externalNativeBuild.cmake.buildStagingDirectory =
File(project.buildDir, "../nativeBuildStaging")
+
+ // Align the ELF region of native shared libs 16kb boundary
+ defaultConfig.externalNativeBuild.cmake.arguments.add(
+ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384"
+ )
}
private fun KotlinMultiplatformAndroidTarget.configureAndroidBaseOptions(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 5a4ad81..6ed3943 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -228,19 +228,27 @@
project.tasks.withType<LintModelWriterTask>().configureEach { it.variantInputs.addSourceSets() }
}
+private fun Project.findLintProject(path: String): Project? {
+ return project.rootProject.findProject(path)
+ ?: if (allowMissingLintProject()) {
+ null
+ } else {
+ throw GradleException("Project $path does not exist")
+ }
+}
+
private fun Project.configureLint(lint: Lint, isLibrary: Boolean) {
val extension = project.androidXExtension
val isMultiplatform = project.multiplatformExtension != null
- val lintChecksProject =
- project.rootProject.findProject(":lint-checks")
- ?: if (allowMissingLintProject()) {
- return
- } else {
- throw GradleException("Project :lint-checks does not exist")
- }
-
+ val lintChecksProject = findLintProject(":lint-checks") ?: return
project.dependencies.add("lintChecks", lintChecksProject)
+ if (extension.type == LibraryType.GRADLE_PLUGIN) {
+ project.rootProject.findProject(":lint:lint-gradle")?.let {
+ project.dependencies.add("lintChecks", it)
+ }
+ }
+
afterEvaluate { addSourceSetsForAndroidMultiplatformAfterEvaluate() }
// The purpose of this specific project is to test that lint is running, so
@@ -359,6 +367,9 @@
disable.add("IllegalExperimentalApiUsage")
}
+ fatal.add("UastImplementation") // go/hide-uast-impl
+ fatal.add("KotlincFE10") // b/239982263
+
// If the project has not overridden the lint config, set the default one.
if (lintConfig == null) {
val lintXmlPath =
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
index 5d3c3aa..6e74707 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
@@ -33,6 +33,7 @@
import org.gradle.process.ExecSpec
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader
+import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.LinkerOutputKind
import org.jetbrains.kotlin.konan.target.Platform
import org.jetbrains.kotlin.konan.target.PlatformManager
@@ -124,13 +125,19 @@
outputFile.parentFile.mkdirs()
val platform = getPlatform(parameters.konanTarget)
+
+ // Specify max-page-size to align ELF regions to 16kb
+ val linkerFlags = if (parameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
+ listOf("-z", "max-page-size=16384")
+ } else emptyList()
+
val objectFiles = parameters.objectFiles.regularFilePaths()
val linkedObjectFiles = parameters.linkedObjects.regularFilePaths()
val linkCommands = platform.linker.finalLinkCommands(
objectFiles = objectFiles,
executable = outputFile.canonicalPath,
libraries = linkedObjectFiles,
- linkerArgs = emptyList(),
+ linkerArgs = linkerFlags,
optimize = true,
debug = false,
kind = LinkerOutputKind.DYNAMIC_LIBRARY,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e51c3ac..32d2d2a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -112,9 +112,12 @@
@get:[InputFile PathSensitive(PathSensitivity.NONE)]
abstract val libraryMetadataFile: RegularFileProperty
- // The base URL to create source links for classes, as a format string with placeholders for the
- // file path and qualified class name.
+ // The base URLs to create source links for classes, functions, and properties, respectively, as
+ // format strings with placeholders for the file path and qualified class name, function name,
+ // or property name.
@get:Input abstract val baseSourceLink: Property<String>
+ @get:Input abstract val baseFunctionSourceLink: Property<String>
+ @get:Input abstract val basePropertySourceLink: Property<String>
private fun sourceSets(): List<DokkaInputModels.SourceSet> {
val externalDocs =
@@ -216,6 +219,8 @@
"libraryMetadataFilename" to
libraryMetadataFile.get().toString(),
"baseSourceLink" to baseSourceLink.get(),
+ "baseFunctionSourceLink" to baseFunctionSourceLink.get(),
+ "basePropertySourceLink" to basePropertySourceLink.get(),
"annotationsNotToDisplay" to annotationsNotToDisplay.get(),
"annotationsNotToDisplayJava" to
annotationsNotToDisplayJava.get(),
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index f22d4af..c516f58 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -533,8 +533,12 @@
excludedPackagesForKotlin.set(emptySet())
libraryMetadataFile.set(getMetadataRegularFile(project))
projectStructureMetadataFile.set(mergedProjectMetadata)
- // See go/dackka-source-link for details on this link.
- baseSourceLink.set("https://cs.android.com/search?" + "q=file:%s+class:%s")
+ // See go/dackka-source-link for details on these links.
+ baseSourceLink.set("https://cs.android.com/search?q=file:%s+class:%s")
+ baseFunctionSourceLink.set(
+ "https://cs.android.com/search?q=file:%s+function:%s"
+ )
+ basePropertySourceLink.set("https://cs.android.com/search?q=file:%s+symbol:%s")
annotationsNotToDisplay.set(hiddenAnnotations)
annotationsNotToDisplayJava.set(hiddenAnnotationsJava)
annotationsNotToDisplayKotlin.set(hiddenAnnotationsKotlin)
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
index f056d56..447a6f1 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
@@ -81,24 +81,6 @@
}
}
-/** @return [File] representing the path stored in [envValue] if it exists, `null` otherwise. */
-private fun getPathFromEnvironmentVariableOrNull(envVar: String): File? {
- val envValue = System.getenv(envVar)
- if (envValue != null) {
- val dir = File(envValue)
- if (dir.isDirectory) {
- return dir
- }
- }
-
- return null
-}
-
-private fun fileIfExistsOrNull(parent: File, child: String): File? {
- val file = File(parent, child)
- return if (file.exists()) file else null
-}
-
private fun getSdkPathFromEnvironmentVariable(): File {
// check for environment variables, in the order AGP checks
listOf("ANDROID_HOME", "ANDROID_SDK_ROOT").forEach {
@@ -132,12 +114,6 @@
return extension.get("supportRootFolder") as File
}
-/** Returns whether the path to the canonical root project directory has been set. */
-fun Project.hasSupportRootFolder(): Boolean {
- val extension = project.rootProject.property("ext") as ExtraPropertiesExtension
- return extension.has("supportRootFolder")
-}
-
/**
* Returns the path to the checkout's root directory, e.g. where {@code repo init} was run.
*
diff --git a/busytown/androidx_multiplatform_mac.sh b/busytown/androidx_multiplatform_mac.sh
index f34ad7a..9bf3d6b 100755
--- a/busytown/androidx_multiplatform_mac.sh
+++ b/busytown/androidx_multiplatform_mac.sh
@@ -12,13 +12,14 @@
# disable GCP cache, these machines don't have credentials.
export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
+sharedArgs="--no-configuration-cache -Pandroidx.constraints=true $*"
# Setup simulators
impl/androidx-native-mac-simulator-setup.sh
-impl/build.sh buildOnServer listTaskOutputs --no-configuration-cache createAllArchives -Pandroidx.constraints=true
+impl/build.sh buildOnServer listTaskOutputs createAllArchives $sharedArgs
# run a separate createAllArchives task to prepare a repository
# folder in DIST.
# This cannot be merged with the buildOnServer run because
# snapshot version is not a proper release version.
-DIST_DIR=$DIST_DIR/snapshots SNAPSHOT=true impl/build.sh createAllArchives --no-configuration-cache -Pandroidx.constraints=true
+DIST_DIR=$DIST_DIR/snapshots SNAPSHOT=true impl/build.sh createAllArchives $sharedArgs
diff --git a/busytown/androidx_multiplatform_mac_arm64.sh b/busytown/androidx_multiplatform_mac_arm64.sh
new file mode 100755
index 0000000..abb492b
--- /dev/null
+++ b/busytown/androidx_multiplatform_mac_arm64.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
+$SCRIPT_DIR/androidx_multiplatform_mac.sh -Pandroidx.lowMemory
diff --git a/busytown/androidx_multiplatform_mac_host_tests.sh b/busytown/androidx_multiplatform_mac_host_tests.sh
index 9e2fcdf..6ee8947 100755
--- a/busytown/androidx_multiplatform_mac_host_tests.sh
+++ b/busytown/androidx_multiplatform_mac_host_tests.sh
@@ -8,9 +8,6 @@
export ANDROIDX_PROJECTS=INFRAROGUE # TODO: Switch from `INFRAROGUE` to `KMP`
-# disable GCP cache, these machines don't have credentials.
-export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
-
echo "Starting $0 at $(date)"
cd "$(dirname $0)"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt
deleted file mode 100644
index 083412f..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2023 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.impl
-
-import android.hardware.camera2.CaptureFailure
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestFailure
-import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.UnsafeWrapper
-import kotlin.reflect.KClass
-
-/**
- * This class implements the [RequestFailure] interface by passing the package-private
- * [CaptureFailure] object.
- */
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal class AndroidCaptureFailure(
- override val requestMetadata: RequestMetadata,
- override val captureFailure: CaptureFailure
-) : RequestFailure, UnsafeWrapper {
- override val frameNumber: FrameNumber = FrameNumber(captureFailure.frameNumber)
- override val reason: Int = captureFailure.reason
- override val wasImageCaptured: Boolean = captureFailure.wasImageCaptured()
-
- @Suppress("UNCHECKED_CAST")
- override fun <T : Any> unwrapAs(type: KClass<T>): T? =
- when (type) {
- CaptureFailure::class -> captureFailure as T?
- else -> null
- }
-}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
index 1c71936..10ac90e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
@@ -138,7 +139,7 @@
val session: CameraCaptureSession? =
requestMetadata.unwrapAs(CameraCaptureSession::class)
val request: CaptureRequest? = requestMetadata.unwrapAs(CaptureRequest::class)
- val captureFailure = requestFailure.captureFailure
+ val captureFailure = requestFailure.unwrapAs(CaptureFailure::class)
if (session != null && request != null && captureFailure != null) {
executor.execute {
callback.captureCallback.onCaptureFailed(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt
deleted file mode 100644
index d7d309b6..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2023 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.impl
-
-import android.hardware.camera2.CaptureFailure
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestFailure
-import androidx.camera.camera2.pipe.RequestMetadata
-
-/**
- * This class implements the [RequestFailure] interface by extracting the fields of
- * the package-private [CaptureFailure] object.
- */
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal data class FakeCaptureFailure(
- override val requestMetadata: RequestMetadata,
- override val wasImageCaptured: Boolean,
- override val frameNumber: FrameNumber,
- override val reason: Int,
- override val captureFailure: CaptureFailure?
-) : RequestFailure
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 255514d..2b3445c 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
@@ -20,7 +20,6 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
import android.hardware.camera2.CaptureResult
@@ -60,6 +59,7 @@
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
@@ -92,7 +92,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
import org.robolectric.shadows.StreamConfigurationMapBuilder
@@ -658,12 +657,13 @@
// Callback capture fail immediately.
request.listeners.forEach {
val requestMetadata = FakeRequestMetadata()
+ val frameNumber = FrameNumber(100L)
it.onFailed(
requestMetadata = requestMetadata,
- frameNumber = FrameNumber(100L),
- requestFailure = AndroidCaptureFailure(
+ frameNumber = frameNumber,
+ requestFailure = FakeRequestFailure(
requestMetadata,
- mock(CaptureFailure::class.java)
+ frameNumber
)
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index d26b2a8..34973a8 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -16,7 +16,6 @@
package androidx.camera.camera2.pipe.integration.impl
-import android.hardware.camera2.CaptureFailure
import android.os.Build
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.StreamId
@@ -32,6 +31,7 @@
import androidx.camera.camera2.pipe.integration.testing.FakeSurface
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
@@ -209,16 +209,12 @@
fakeCameraGraphSession.submittedRequests.first().let { request ->
request.listeners.forEach { listener ->
- @Suppress("DEPRECATION")
listener.onFailed(
fakeRequestMetadata,
frameNumber,
- FakeCaptureFailure(
+ FakeRequestFailure(
fakeRequestMetadata,
- false,
- frameNumber,
- CaptureFailure.REASON_ERROR,
- null
+ frameNumber
)
)
}
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 cd6027d..35d338b 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
@@ -29,11 +29,11 @@
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.integration.impl.FakeCaptureFailure
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.ABORTED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.FAILED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.TOTAL_CAPTURE_DONE
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import java.util.concurrent.Semaphore
import kotlinx.coroutines.CompletableDeferred
@@ -180,12 +180,9 @@
FAILED -> listener.onFailed(
requestMetadata,
FrameNumber(0),
- FakeCaptureFailure(
+ FakeRequestFailure(
requestMetadata,
- false,
- FrameNumber(0),
- CaptureFailure.REASON_ERROR,
- null
+ FrameNumber(0)
)
)
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt
new file mode 100644
index 0000000..5660d9b
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.testing
+
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestFailure
+import androidx.camera.camera2.pipe.RequestMetadata
+import kotlin.reflect.KClass
+
+/**
+ * Utility class for testing code that depends on [RequestFailure] with reasonable defaults.
+ */
+class FakeRequestFailure(
+ override val requestMetadata: RequestMetadata,
+ override val frameNumber: FrameNumber,
+ override val reason: Int = 0,
+ override val wasImageCaptured: Boolean = false,
+) : RequestFailure {
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+ // Fake objects cannot be unwrapped.
+ return null
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
index 3e52406..9cc329c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
@@ -171,11 +171,11 @@
value class FrameId(val value: Long)
/**
- * Represents the status of an output from the camera.
+ * Represents the status of an output from the camera with enum-like values.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmInline
-value class OutputStatus(val value: Int) {
+value class OutputStatus internal constructor(val value: Int) {
companion object {
/** Output is not yet available. */
val PENDING = OutputStatus(0)
@@ -258,7 +258,7 @@
/**
* Get the status of the pending [Frame].
*/
- val frameStatus: OutputStatus
+ val status: OutputStatus
/**
* Get or suspend until the [Frame] that will be produced by the camera for this [request] is
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index db5f5d6..76585d3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -261,16 +261,22 @@
* constructor prevents directly creating an instance of it.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-interface RequestFailure {
+interface RequestFailure : UnsafeWrapper {
+ /** Metadata about the request that has failed. */
val requestMetadata: RequestMetadata
+ /** The Camera [FrameNumber] for the request that has failed. */
val frameNumber: FrameNumber
+ /** Indicates the reason the particular request failed, see [CaptureFailure] for details. */
val reason: Int
+ /**
+ * Indicates if images were still captured for this request. If this is true, the camera should
+ * invoke [Request.Listener.onBufferLost] individually for each output that failed. If this is
+ * false, these outputs will never arrive, and the individual callbacks will not be invoked.
+ */
val wasImageCaptured: Boolean
-
- val captureFailure: CaptureFailure?
}
/**
@@ -296,14 +302,12 @@
/**
* The intended use for this class is to submit the input needed for a reprocessing request, the
- * [InputStream], [ImageWrapper] and [FrameMetadata]. Both values are non-nullable because
+ * [ImageWrapper] and [FrameInfo]. Both values are non-nullable because
* both values are needed for reprocessing.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class InputRequest(
- val inputStreamId: InputStreamId,
val image: ImageWrapper,
- val frameMetadata: FrameMetadata,
val frameInfo: FrameInfo
)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
index 9cdf28fc..ae96642 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
@@ -24,7 +24,6 @@
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestFailure
import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.UnsafeWrapper
import kotlin.reflect.KClass
/**
@@ -34,8 +33,8 @@
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AndroidCaptureFailure(
override val requestMetadata: RequestMetadata,
- override val captureFailure: CaptureFailure
-) : RequestFailure, UnsafeWrapper {
+ private val captureFailure: CaptureFailure
+) : RequestFailure {
override val frameNumber: FrameNumber = FrameNumber(captureFailure.frameNumber)
override val reason: Int = captureFailure.reason
override val wasImageCaptured: Boolean = captureFailure.wasImageCaptured()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 49cb8ce..5fd7fcc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -194,17 +194,16 @@
// Load the request and throw if we are not able to find an associated request. Under
// normal circumstances this should never happen.
- val request = readRequestMetadata(requestNumber)
+ val requestMetadata = readRequestMetadata(requestNumber)
- val simpleCaptureFailure = SimpleCaptureFailure(
- request,
+ val extensionRequestFailure = ExtensionRequestFailure(
+ requestMetadata,
false,
frameNumber,
- CaptureFailure.REASON_ERROR,
- null
+ CaptureFailure.REASON_ERROR
)
- invokeCaptureFailure(request, frameNumber, simpleCaptureFailure)
+ invokeCaptureFailure(requestMetadata, frameNumber, extensionRequestFailure)
}
override fun onCaptureBufferLost(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
similarity index 88%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
index 2d43ff8..ee372c5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
@@ -24,16 +24,20 @@
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestFailure
import androidx.camera.camera2.pipe.RequestMetadata
+import kotlin.reflect.KClass
/**
* This class implements the [RequestFailure] interface by extracting the fields of
* the package-private [CaptureFailure] object.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal data class SimpleCaptureFailure(
+internal data class ExtensionRequestFailure(
override val requestMetadata: RequestMetadata,
override val wasImageCaptured: Boolean,
override val frameNumber: FrameNumber,
override val reason: Int,
- override val captureFailure: CaptureFailure?
-) : RequestFailure
+) : RequestFailure {
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+ return null
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt
new file mode 100644
index 0000000..32c889d
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.internal
+
+import android.os.Build
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.config.CameraGraphScope
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
+import javax.inject.Inject
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * FrameCaptureQueue manages the list of requests that are expected to produce a [Frame] that needs
+ * to be returned when the [Frame] that matches the [Request] is started.
+ */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+@CameraGraphScope
+internal class FrameCaptureQueue @Inject constructor() : AutoCloseable {
+ private val lock = Any()
+
+ @GuardedBy("lock")
+ private val queue = ArrayDeque<FrameCaptureImpl>()
+
+ @GuardedBy("lock")
+ private var closed = false
+
+ fun remove(request: Request): FrameCaptureImpl? =
+ synchronized(lock) {
+ if (closed) return null
+
+ // If an item matching this request exists, remove it from the queue and return it.
+ queue.firstOrNull {
+ it.request == request
+ }?.also { queue.remove(it) }
+ }
+
+ /**
+ * Tell the [FrameDistributor] that a specific request will be submitted to the camera and
+ * create a placeholder that will be completed when that specific request starts exposing.
+ */
+ fun enqueue(request: Request): FrameCaptureImpl =
+ synchronized(lock) {
+ FrameCaptureImpl(request).also {
+ if (!closed) {
+ queue.add(it)
+ } else {
+ it.close()
+ }
+ }
+ }
+
+ /**
+ * Tell the [FrameDistributor] that a specific list of requests will be submitted to the
+ * camera and to create placeholders.
+ */
+ fun enqueue(requests: List<Request>): List<FrameCapture> =
+ synchronized(lock) {
+ requests.map { FrameCaptureImpl(it) }.also {
+ if (!closed) {
+ queue.addAll(it)
+ } else {
+ for (result in it) {
+ result.close()
+ }
+ }
+ }
+ }
+
+ override fun close() {
+ synchronized(lock) {
+ if (closed) return
+ closed = true
+ }
+
+ // Note: This happens outside the synchronized block, but is safe since all modifications
+ // above happen within the synchronized block, and all modifications check the closed value
+ // before modifying the list.
+ for (pendingOutputFrame in queue) {
+
+ // Any pending frame in the queue is guaranteed to not hold a real result.
+ pendingOutputFrame.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
+ }
+ queue.clear()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ internal inner class FrameCaptureImpl(override val request: Request) : FrameCapture {
+ private val closed = atomic(false)
+ private val result = CompletableDeferred<OutputResult<Frame>>()
+
+ @GuardedBy("this")
+ private var frameListeners: MutableList<Frame.Listener>? = mutableListOf()
+
+ /** Complete this [FrameCapture] with the provide [Frame]. */
+ fun completeWith(frame: Frame) {
+ if (!result.completeWithOutput(frame)) {
+ // Close the frame if the result was non-null and we failed to complete this frame
+ frame.close()
+ } else {
+ val listeners: List<Frame.Listener>?
+ synchronized(this) {
+ listeners = frameListeners
+ frameListeners = null
+ }
+
+ if (listeners != null) {
+ for (i in listeners.indices) {
+ frame.addListener(listeners[i])
+ }
+ }
+ }
+ }
+
+ /** Cancel this [FrameCapture] with a specific [OutputStatus]. */
+ fun completeWithFailure(failureStatus: OutputStatus) {
+ if (result.completeWithFailure(failureStatus)) {
+ val listeners: List<Frame.Listener>?
+ synchronized(this) {
+ listeners = frameListeners
+ frameListeners = null
+ }
+
+ // Ensure listeners always receive the onFrameCompleted event, since it will not be
+ // attached to a real frame.
+ if (listeners != null) {
+ for (i in listeners.indices) {
+ listeners[i].onFrameComplete()
+ }
+ }
+ }
+ }
+
+ override fun getFrame(): Frame? {
+ if (closed.value) return null
+ return result.outputOrNull()?.tryAcquire()
+ }
+
+ override val status: OutputStatus
+ get() {
+ if (closed.value) return OutputStatus.UNAVAILABLE
+ return result.outputStatus()
+ }
+
+ override suspend fun awaitFrame(): Frame? {
+ if (closed.value) return null
+ return result.await().output?.tryAcquire()
+ }
+
+ override fun addListener(listener: Frame.Listener) {
+ val success = synchronized(this) {
+ frameListeners?.add(listener) == true
+ }
+ // If the list of listeners is null, then we've already completed this deferred output
+ // frame.
+ if (!success) {
+ val frame = result.outputOrNull()
+ if (frame != null) {
+ frame.addListener(listener)
+ } else {
+ listener.onFrameComplete()
+ }
+ }
+ }
+
+ override fun close() {
+ if (closed.compareAndSet(expect = false, update = true)) {
+ completeWithFailure(OutputStatus.UNAVAILABLE)
+ result.outputOrNull()?.close()
+
+ // We should close all of the object if we successfully remove it from the list.
+ // Otherwise, this operation is a no-op.
+ synchronized(lock) {
+ queue.remove(this)
+ }
+ }
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt
new file mode 100644
index 0000000..e88f31e5
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.internal
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.FrameReference
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestFailure
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.media.ClosingFinalizer
+import androidx.camera.camera2.pipe.media.ImageSource
+import androidx.camera.camera2.pipe.media.NoOpFinalizer
+import androidx.camera.camera2.pipe.media.OutputImage
+
+/**
+ * A FrameDistributor is responsible for listening to events from each [Request] as well as images
+ * that are produced by [ImageSources][ImageSource] in order to group them into [Frames][Frame] and
+ * distribute these frames to downstream consumers.
+ *
+ * Frames can be safely held until needed, passed to other consumers, or closed at any point in
+ * time while correctly handling the underlying resource counts and error handling, which is a
+ * non-trivial problem to solve correctly and efficiently. For optimal behavior, an instance of this
+ * class should be attached as a listener to the callbacks of *all* [Requests][Request] sent to the
+ * Camera.
+ *
+ * Frames are distributed during [onStarted] in two primary ways:
+ *
+ * 1. To [FrameCapture] for non-repeating capture requests in the [frameCaptureQueue]
+ * 2. To the [frameStartedListener] as a [FrameReference], which must call [Frame.tryAcquire] if
+ * it would like to hold onto it beyond the lifetime of the method call.
+ *
+ * The remaining callbacks are used to distribute specific error and failure conditions to frames
+ * that were previously started.
+ */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+internal class FrameDistributor(
+ imageSources: Map<StreamId, ImageSource>,
+ private val frameCaptureQueue: FrameCaptureQueue,
+ private val frameStartedListener: FrameStartedListener
+) : AutoCloseable, Request.Listener {
+ /**
+ * Listener to observe new [FrameReferences][FrameReference] as they are started by the camera.
+ */
+ fun interface FrameStartedListener {
+ /**
+ * Invoked when a new [Frame] has started. Implementations must synchronously invoke
+ * [Frame.tryAcquire] if they want to maintain a valid reference to the [Frame]
+ */
+ fun onFrameStarted(frameReference: FrameReference)
+ }
+
+ private val frameInfoDistributor = OutputDistributor<FrameInfo>(outputFinalizer = NoOpFinalizer)
+
+ // This is an example CameraGraph configuration a camera configured with both capture and
+ // non capture output streams, as well as physical outputs:
+ //
+ // Camera-0 (Logical)
+ // Stream-1 Output-1 Viewfinder (No Images)
+ // Stream-2 Output-2 YUV_420
+ // Stream-3 Output-3 RAW10 [Camera-4]
+ // Output-4 RAW10 [Camera-5]
+ // Output-5 RAW10 [Camera-6]
+ //
+ // In this scenario the FrameDistributor will handle distributing images to Stream-2 and
+ // to Stream-3 if they are configured with an ImageSource. Each of these streams is
+ // associated with its own OutputDistributor for error handling and grouping.
+ private val imageDistributors = imageSources.mapValues { (_, imageSource) ->
+ val imageDistributor =
+ OutputDistributor<OutputImage>(outputFinalizer = ClosingFinalizer)
+
+ // Bind the listener on the ImageSource to the imageDistributor. This listener
+ // and the imageDistributor may be invoked on a different thread.
+ imageSource.setListener { imageStreamId, imageOutputId, outputTimestamp, image ->
+ if (image != null) {
+ imageDistributor.onOutputResult(
+ outputTimestamp,
+ OutputResult.from(OutputImage.from(imageStreamId, imageOutputId, image))
+ )
+ } else {
+ imageDistributor.onOutputResult(
+ outputTimestamp,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+ )
+ }
+ }
+
+ imageDistributor
+ }
+ private val imageStreams: Set<StreamId> = imageDistributors.keys
+
+ /**
+ * Create and distribute a [Frame] to the pending [FrameCapture] (If one has been registered
+ * for this request), and to the [FrameStartedListener]
+ */
+ override fun onStarted(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ timestamp: CameraTimestamp
+ ) {
+ // When the camera begins exposing a frame, create a placeholder for all of the outputs that
+ // will be produced, and tell each of the image distributors to expect results for this
+ // frameNumber and timestamp.
+ val frameState = FrameState(
+ requestMetadata,
+ frameNumber,
+ timestamp,
+ imageStreams
+ )
+
+ // Tell the frameInfo distributor to expect FrameInfo at the provided FrameNumber
+ frameInfoDistributor.onOutputStarted(
+ cameraFrameNumber = frameNumber,
+ cameraTimestamp = timestamp,
+ outputNumber = frameNumber.value, // Number to match output against
+ outputListener = frameState.frameInfoOutput
+ )
+
+ // Tell each imageDistributor to expect an Image at the provided CameraTimestamp.
+ for (i in frameState.imageOutputs.indices) {
+ val imageOutput = frameState.imageOutputs[i]
+ val imageDistributor = imageDistributors[imageOutput.streamId]!!
+
+ // Images are matched to the frame based on the cameraTimestamp.
+ imageDistributor.onOutputStarted(
+ cameraFrameNumber = frameNumber,
+ cameraTimestamp = timestamp,
+ outputNumber = timestamp.value, // Number to match output against
+ outputListener = imageOutput
+ )
+
+ if (!requestMetadata.streams.keys.contains(imageOutput.streamId)) {
+ // Edge case: It's possible that a CaptureRequest submitted to the camera
+ // is different than the Request used to create it in a few scenarios (such
+ // as the surface being unavailable or invalid). If this happens, tell the
+ // imageDistributor that the output has failed for this specific frame.
+ imageDistributor.onOutputFailure(frameState.frameNumber)
+ }
+ }
+
+ // Create a Frame, and offer it
+ val frame = FrameImpl(frameState)
+ frameStartedListener.onFrameStarted(frame)
+
+ // If there is an explicit capture request associated with this request, pass it to the
+ // FrameCapture.
+ if (!requestMetadata.repeating) {
+ val frameCapture = frameCaptureQueue.remove(requestMetadata.request)
+ if (frameCapture != null) {
+ frameCapture.completeWith(frame)
+ return
+ }
+ }
+
+ // Close the frame. This releases the reference we are holding.
+ frame.close()
+ }
+
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo
+ ) {
+ // Tell the frameInfo distributor that the metadata for this exposure has been computed and
+ // can be distributed.
+ frameInfoDistributor.onOutputResult(frameNumber.value, OutputResult.from(result))
+ }
+
+ override fun onBufferLost(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ stream: StreamId
+ ) {
+ // Tell the specific image distributor for this stream that the output has failed and will
+ // not arrive for this frame. When onBufferLost occurs, other images and metadata may still
+ // complete successfully.
+ imageDistributors[stream]?.onOutputFailure(frameNumber)
+ }
+
+ override fun onFailed(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ requestFailure: RequestFailure
+ ) {
+ // Metadata will not arrive for this frame:
+ frameInfoDistributor.onOutputResult(
+ frameNumber.value,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_FAILED)
+ )
+
+ // There are two scenarios:
+ // 1. Images were captured: In this case, camera2 will send individual failures for each
+ // failed image via the onBufferLost callback.
+ // 2. Images were *not* captured: (This case), if images were not captured, all outputs have
+ // failed, and camera2 will not invoke onBufferLost. We are responsible for marking all
+ // outputs as failed.
+ if (!requestFailure.wasImageCaptured) {
+ // Note: The actual streams used by camera2 are specified in requestMetadata.streams and
+ // may be different than requestMetadata.request.streams if one of the surfaces was
+ // not ready or available. Make sure we iterate over `requestMetadata.streams`
+ for (stream in requestMetadata.streams.keys) {
+ imageDistributors[stream]?.onOutputFailure(frameNumber)
+ }
+ }
+ }
+
+ override fun onAborted(request: Request) {
+ // When a request is aborted, it may (or may not) be in some stage of capture, and it might
+ // not have started yet. The only thing we know for sure is:
+ //
+ // 1. Only single requests can be aborted (repeating requests are never guaranteed to
+ // produce outputs, so they never get aborted calls)
+ // 2. If a request is already started, the camera should handle the failures for aborted
+ // captures as a separate set of events.
+ frameCaptureQueue.remove(request)?.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
+ }
+
+ override fun close() {
+ // Closing the captureQueue aborts all pending and future capture requests.
+ frameCaptureQueue.close()
+
+ // Stop distributing FrameInfo
+ frameInfoDistributor.close()
+
+ // Stop distributing Images
+ for (imageDistributor in imageDistributors.values) {
+ imageDistributor.close()
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
index 3408acb..7229043 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
@@ -141,7 +141,7 @@
override fun getFrameInfo(): FrameInfo? {
if (closed.value) return null
- return frameState.frameInfoOutput.getCompletedOrNull()
+ return frameState.frameInfoOutput.outputOrNull()
}
override suspend fun awaitImage(streamId: StreamId): OutputImage? {
@@ -155,7 +155,7 @@
if (closed.value) return null
if (!imageStreams.contains(streamId)) return null
val output = frameState.imageOutputs.firstOrNull { it.streamId == streamId }
- return output?.getCompletedOrNull()
+ return output?.outputOrNull()
}
override fun imageStatus(streamId: StreamId): OutputStatus {
@@ -170,9 +170,4 @@
}
override fun toString(): String = frameState.toString()
-
- companion object {
- private val frameIds = atomic(0L)
- internal fun nextFrameId(): FrameId = FrameId(frameIds.incrementAndGet())
- }
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
index ba4603a..c0b30a7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
@@ -30,14 +30,17 @@
import androidx.camera.camera2.pipe.internal.FrameState.State.FRAME_INFO_COMPLETE
import androidx.camera.camera2.pipe.internal.FrameState.State.STARTED
import androidx.camera.camera2.pipe.internal.FrameState.State.STREAM_RESULTS_COMPLETE
-import androidx.camera.camera2.pipe.media.OutputDistributor
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
import androidx.camera.camera2.pipe.media.OutputImage
import androidx.camera.camera2.pipe.media.SharedOutputImage
import java.util.concurrent.CopyOnWriteArrayList
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.updateAndGet
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Deferred
/**
* This class represents a successfully started frame from the camera, and placeholders for the
@@ -46,11 +49,11 @@
@RequiresApi(21)
internal class FrameState(
val requestMetadata: RequestMetadata,
- val frameId: FrameId,
val frameNumber: FrameNumber,
val frameTimestamp: CameraTimestamp,
imageStreams: Set<StreamId>
) {
+ val frameId = nextFrameId()
val frameInfoOutput: FrameInfoOutput = FrameInfoOutput()
val imageOutputs: List<ImageOutput> = buildList {
for (streamId in requestMetadata.streams.keys) {
@@ -152,7 +155,6 @@
* [FrameOutput] handles the logic and reference counting that is required to safely handle a
* shared `CompletableDeferred` instance that may contain an expensive closable resource.
*/
- @OptIn(ExperimentalCoroutinesApi::class)
internal abstract class FrameOutput<T : Any> {
private val count = atomic(1)
@@ -161,7 +163,8 @@
* object of type T OR the [OutputStatus] that was passed down when this output was
* completed.
*/
- val result = CompletableDeferred<Any>()
+ protected val internalResult = CompletableDeferred<OutputResult<T>>()
+ val result: Deferred<OutputResult<T>> get() = internalResult
fun increment(): Boolean {
val current =
@@ -177,17 +180,10 @@
fun decrement() {
if (count.decrementAndGet() == 0) {
- result.cancel()
- try {
- if (!result.isCancelled) {
- // If we call cancel(), but the end state is not canceled, it means that
- // the result was previously completed successfully. In this case, we need
- // to release the underlying reference this Frame is holding on to.
- result.getCompleted().asOutput { release(it) }
- }
- } catch (ignored: IllegalStateException) {
- // NoOp, this should never happen.
- }
+ // UNAVAILABLE is used to indicate outputs that have been closed or released during
+ // normal operation.
+ internalResult.completeWithFailure(OutputStatus.UNAVAILABLE)
+ release()
}
}
@@ -195,53 +191,16 @@
get() {
// A result of `isCancelled` indicates the frame was closed before the Output
// arrived.
- if (count.value == 0 || result.isCancelled) {
+ if (count.value == 0) {
return OutputStatus.UNAVAILABLE
}
-
- // If the result is not canceled, and not completed, then we are waiting for the
- // output to arrive.
- if (!result.isCompleted) {
- return OutputStatus.PENDING
- }
-
- // This is guaranteed to be completed.
- val result = result.getCompleted()
- if (result is OutputStatus) {
- return result
- }
- return OutputStatus.AVAILABLE
+ return internalResult.outputStatus()
}
- fun getCompletedOrNull(): T? =
- if (!result.isCompleted || result.isCancelled) {
- null
- } else {
- result.getCompleted().asOutput { acquire(it) }
- }
+ abstract fun outputOrNull(): T?
+ abstract suspend fun await(): T?
- protected fun completeWith(output: T?, outputResult: OutputStatus): Boolean {
- val result = if (output == null) {
- result.complete(outputResult)
- } else {
- result.complete(output)
- }
- return result
- }
-
- private inline fun <R> Any.asOutput(block: (T) -> R): R? {
- if (this is OutputStatus) return null
- @Suppress("UNCHECKED_CAST")
- return block(this as T)
- }
-
- suspend fun await(): T? = result.await().asOutput { acquire(it) }
-
- /** Invoked to acquire the underlying resource. */
- protected abstract fun acquire(value: T): T?
-
- /** Invoked when the underlying resource is no longer referenced and should be released. */
- protected abstract fun release(value: T)
+ protected abstract fun release()
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@@ -253,22 +212,23 @@
cameraTimestamp: CameraTimestamp,
outputSequence: Long,
outputNumber: Long,
- outputStatus: OutputStatus,
- output: FrameInfo?
+ outputResult: OutputResult<FrameInfo>
) {
+ val output = outputResult.output
check(output == null || output.frameNumber.value == outputNumber) {
"Unexpected FrameInfo: $output " +
"Expected ${output?.frameNumber?.value} to match $outputNumber!"
}
- completeWith(output, outputStatus)
+ internalResult.complete(outputResult)
onFrameInfoComplete()
}
- override fun acquire(value: FrameInfo): FrameInfo = value
+ override suspend fun await(): FrameInfo? = result.await().output
+ override fun outputOrNull() = result.outputOrNull()
- override fun release(value: FrameInfo) {
- // Ignored: FrameInfo is not closable.
+ override fun release() {
+ // NoOp
}
}
@@ -281,23 +241,36 @@
cameraTimestamp: CameraTimestamp,
outputSequence: Long,
outputNumber: Long,
- outputStatus: OutputStatus,
- output: OutputImage?
+ outputResult: OutputResult<OutputImage>
) {
- check(output == null || output.timestamp == outputNumber) {
- "Unexpected Image: $output, expected ${output?.timestamp} to match $outputNumber!"
+ val output = outputResult.output
+ if (output != null) {
+ check(output.timestamp == outputNumber) {
+ "Unexpected image: $output! Expected ${output.timestamp} " +
+ "to match $outputNumber!"
+ }
+ val sharedImage = SharedOutputImage.from(output)
+ if (!internalResult.completeWithOutput(sharedImage)) {
+ sharedImage.close()
+ }
+ } else {
+ internalResult.completeWithFailure(outputResult.status)
}
- val sharedImage = output?.let { SharedOutputImage.from(it) }
- if (!completeWith(sharedImage, outputStatus)) {
- sharedImage?.close()
- }
+
onStreamResultComplete(streamId)
}
- override fun release(value: SharedOutputImage) {
- value.close()
- }
+ override fun outputOrNull(): SharedOutputImage? = result.outputOrNull()?.acquireOrNull()
- override fun acquire(value: SharedOutputImage): SharedOutputImage? = value.acquireOrNull()
+ override suspend fun await(): SharedOutputImage? = result.await().output?.acquireOrNull()
+
+ override fun release() {
+ internalResult.outputOrNull()?.close()
+ }
+ }
+
+ companion object {
+ private val frameIds = atomic(0L)
+ private fun nextFrameId(): FrameId = FrameId(frameIds.incrementAndGet())
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
similarity index 78%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
index 30a68b1..91c4338 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 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,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.camera2.pipe.media
+package androidx.camera.camera2.pipe.internal
import android.os.Build
import androidx.annotation.GuardedBy
@@ -22,6 +22,7 @@
import androidx.camera.camera2.pipe.CameraTimestamp
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.media.Finalizer
import kotlinx.atomicfu.atomic
/**
@@ -29,18 +30,18 @@
*
* In addition this class must:
* 1. Track and cancel events due to skipped [onOutputStarted] events.
- * 2. Track and finalize resources due to skipped [onOutputAvailable] events.
+ * 2. Track and finalize resources due to skipped [onOutputResult] events.
* 3. Track and cancel events that match [onOutputFailure] events.
* 4. Track and handle out-of-order [onOutputStarted] events.
* 5. Finalize all resources and cancel all events during [close]
*
* This class makes several assumptions:
* 1. [onOutputStarted] events *usually* arrive in order, relative to each other.
- * 2. [onOutputAvailable] events *usually* arrive in order, relative to each other.
- * 3. [onOutputStarted] events *usually* happen before a corresponding [onOutputAvailable] event
- * 4. [onOutputStarted] events may have a large number of events (1-50) before [onOutputAvailable]
+ * 2. [onOutputResult] events *usually* arrive in order, relative to each other.
+ * 3. [onOutputStarted] events *usually* happen before a corresponding [onOutputResult] event
+ * 4. [onOutputStarted] events may have a large number of events (1-50) before [onOutputResult]
* events start coming in.
- * 5. [onOutputStarted] and [onOutputAvailable] are 1:1 under normal circumstances.
+ * 5. [onOutputStarted] and [onOutputResult] are 1:1 under normal circumstances.
*
* @param maximumCachedOutputs indicates how many available outputs this distributor will accept
* without matching [onOutputStarted] event before closing them with the [outputFinalizer].
@@ -58,16 +59,14 @@
* once per [OutputDistributor.onOutputStarted] event.
*
* On failures (The output being unavailable, the [OutputDistributor] being closed before
- * an output has arrived, or an explicit output failure event), this method will still be
- * invoked with a null [output].
+ * an output has arrived, or an explicit output failure event).
*/
fun onOutputComplete(
cameraFrameNumber: FrameNumber,
cameraTimestamp: CameraTimestamp,
outputSequence: Long,
outputNumber: Long,
- outputStatus: OutputStatus,
- output: T?,
+ outputResult: OutputResult<T>
)
}
@@ -92,7 +91,7 @@
private var lastFailedOutputNumber = Long.MIN_VALUE
private val startedOutputs = mutableListOf<StartedOutput<T>>()
- private val availableOutputs = mutableMapOf<Long, T?>()
+ private val availableOutputs = mutableMapOf<Long, OutputResult<T>>()
/**
* Indicates a camera2 output has started at a particular frameNumber and timestamp as well as
@@ -102,7 +101,7 @@
* @param cameraFrameNumber The Camera2 FrameNumber for this output
* @param cameraTimestamp The Camera2 CameraTimestamp for this output
* @param outputNumber untyped number that corresponds to the number provided by
- * [onOutputAvailable]. For Images, this will likely be the timestamp of the image (Which may
+ * [onOutputResult]. For Images, this will likely be the timestamp of the image (Which may
* be the same as the CameraTimestamp, but may also be different if the timebase of the
* the images is different), or the value of the frameNumber if this OutputDistributor is
* handling metadata.
@@ -116,10 +115,10 @@
outputNumber: Long,
outputListener: OutputListener<T>
) {
- var outputsToCancel: List<StartedOutput<T>>? = null
- var outputToComplete: T? = null
- var invokeOutputCompleteListener = false
- var outputToFinalize: T? = null
+ var missingOutputs: List<StartedOutput<T>>? = null
+ var matchingOutput: OutputResult<T>? = null
+ var invokeOutputListener = false
+ var outputToFinalize: OutputResult<T>? = null
val isClosed: Boolean
val outputSequence: Long
@@ -131,7 +130,7 @@
lastFailedOutputNumber == outputNumber
) {
outputToFinalize = availableOutputs.remove(outputNumber)
- invokeOutputCompleteListener = true
+ invokeOutputListener = true
return@synchronized
}
@@ -166,9 +165,9 @@
if (availableOutputs.containsKey(outputNumber)) {
// If we found a matching output, get and remove it from the list of
// availableOutputs.
- outputToComplete = availableOutputs.remove(outputNumber)
- invokeOutputCompleteListener = true
- outputsToCancel = removeOutputsOlderThan(
+ matchingOutput = availableOutputs.remove(outputNumber)
+ invokeOutputListener = true
+ missingOutputs = removeOutputsOlderThan(
isOutOfOrder,
outputSequence,
outputNumber
@@ -190,49 +189,42 @@
)
}
- // Invoke finalizers and listeners outside of the synchronized block to avoid holding locks.
- outputsToCancel?.let {
- val reason = if (isClosed) {
- OutputStatus.ERROR_OUTPUT_ABORTED
- } else {
- OutputStatus.ERROR_OUTPUT_MISSING
- }
- for (output in it) {
- output.completeWith(null, reason)
- }
+ // Handle missing outputs, finalizers, and listeners outside of the synchronized block.
+ missingOutputs?.forEach {
+ it.completeWith(OutputResult.failure(OutputStatus.ERROR_OUTPUT_MISSING))
}
- outputToFinalize?.let { outputFinalizer.finalize(it) }
- if (invokeOutputCompleteListener) {
+ outputToFinalize?.output?.let { outputFinalizer.finalize(it) }
+
+ if (invokeOutputListener) {
val outputResult = if (isClosed) {
- OutputStatus.ERROR_OUTPUT_ABORTED
- } else if (outputToComplete == null) {
- OutputStatus.ERROR_OUTPUT_DROPPED
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_ABORTED)
} else {
- OutputStatus.AVAILABLE
+ matchingOutput ?: OutputResult.failure(OutputStatus.ERROR_OUTPUT_FAILED)
}
outputListener.onOutputComplete(
cameraFrameNumber = cameraFrameNumber,
cameraTimestamp = cameraTimestamp,
outputSequence = outputSequence,
outputNumber = outputNumber,
- outputResult,
- outputToComplete,
+ outputResult
)
}
}
/**
- * Indicates a camera2 output has arrived for a specific [outputNumber]. outputNumber will
- * often refer to a FrameNumber for TotalCaptureResult distribution, and will often refer to a
- * nanosecond timestamp for ImageReader Image distribution.
+ * Indicates a camera2 output has arrived for a specific [outputNumber].
+ *
+ * This value is the primary keu used to match `onOutputStart` events with `onOutputResult`
+ * events. For images, these values will often refer to the nanosecond timestamp of the Image,
+ * and for TotalCaptureResults, this value will often reference the associated FrameNumber.
*/
- fun onOutputAvailable(outputNumber: Long, output: T?) {
- var outputToFinalize: T? = null
+ fun onOutputResult(outputNumber: Long, outputResult: OutputResult<T>) {
+ var outputToFinalize: OutputResult<T>? = null
var outputsToCancel: List<StartedOutput<T>>? = null
synchronized(lock) {
if (closed || lastFailedOutputNumber == outputNumber) {
- outputToFinalize = output
+ outputToFinalize = outputResult
return@synchronized
}
@@ -243,18 +235,14 @@
if (matchingOutput != null) {
outputsToCancel = removeOutputsOlderThan(matchingOutput)
- // If the output is null, then we know that the output was intentionally dropped.
- if (output == null) {
- matchingOutput.completeWith(null, OutputStatus.ERROR_OUTPUT_DROPPED)
- } else {
- matchingOutput.completeWith(output, OutputStatus.AVAILABLE)
- }
+ matchingOutput.completeWith(outputResult)
+
startedOutputs.remove(matchingOutput)
return@synchronized
}
// If there is no started output, put this output into the queue of pending outputs.
- availableOutputs[outputNumber] = output
+ availableOutputs[outputNumber] = outputResult
// If there are too many pending outputs, remove the oldest one.
if (availableOutputs.size > maximumCachedOutputs) {
@@ -265,8 +253,10 @@
}
// Invoke finalizers and listeners outside of the synchronized block to avoid holding locks.
- outputToFinalize?.let { outputFinalizer.finalize(it) }
- outputsToCancel?.forEach { it.completeWith(null, OutputStatus.ERROR_OUTPUT_MISSING) }
+ outputToFinalize?.output?.let { outputFinalizer.finalize(it) }
+ outputsToCancel?.forEach {
+ it.completeWith(OutputResult.failure(OutputStatus.ERROR_OUTPUT_MISSING))
+ }
}
/**
@@ -290,7 +280,7 @@
}
// Invoke listeners outside of the synchronized block to avoid holding locks.
- outputWithFailure?.completeWith(null, OutputStatus.ERROR_OUTPUT_FAILED)
+ outputWithFailure?.completeWithFailure(OutputStatus.ERROR_OUTPUT_FAILED)
}
@GuardedBy("lock")
@@ -316,7 +306,7 @@
}
override fun close() {
- var outputsToFinalize: List<T?>
+ var outputsToFinalize: List<OutputResult<T>>
var outputsToCancel: List<StartedOutput<T>>
synchronized(lock) {
@@ -332,10 +322,10 @@
}
for (pendingOutput in outputsToFinalize) {
- outputFinalizer.finalize(pendingOutput)
+ outputFinalizer.finalize(pendingOutput.output)
}
for (startedOutput in outputsToCancel) {
- startedOutput.completeWith(null, OutputStatus.ERROR_OUTPUT_ABORTED)
+ startedOutput.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
}
}
@@ -353,7 +343,10 @@
) {
private val complete = atomic(false)
- fun completeWith(output: T?, outputResult: OutputStatus) {
+ fun completeWithFailure(failureReason: OutputStatus) =
+ completeWith(OutputResult.failure(failureReason))
+
+ fun completeWith(outputResult: OutputResult<T>) {
check(complete.compareAndSet(expect = false, update = true)) {
"Output $outputSequence at $cameraFrameNumber for $outputNumber was completed " +
"multiple times!"
@@ -363,8 +356,7 @@
cameraTimestamp,
outputSequence,
outputNumber,
- outputResult,
- output
+ outputResult
)
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt
new file mode 100644
index 0000000..78e72c6
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.camera.camera2.pipe.internal
+
+import androidx.camera.camera2.pipe.OutputStatus
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * Inline value class that can be used in place of `Any` to represent either a valid output object
+ * OR an [OutputStatus] indicating why the result is not available.
+ */
+@JvmInline
+internal value class OutputResult<out T> private constructor(internal val result: Any?) {
+ /**
+ * Returns `true` if this instance represents a successful outcome.
+ */
+ val available: Boolean get() = !failure && result != null
+
+ /**
+ * Returns `true` if this instance represents a failed result.
+ */
+ val failure: Boolean get() = result is OutputStatus
+
+ /**
+ * Returns the value, if [from], else null.
+ */
+ @Suppress("UNCHECKED_CAST")
+ inline val output: T?
+ get() =
+ when {
+ available -> result as T
+ else -> null
+ }
+
+ /**
+ * If this OutputResult represents a failure, then return the [OutputStatus] associated with it,
+ * otherwise report [OutputStatus.AVAILABLE] for successfully cases.
+ */
+ inline val status: OutputStatus
+ get() =
+ when {
+ available -> OutputStatus.AVAILABLE
+ result == null -> OutputStatus.UNAVAILABLE
+ else -> result as OutputStatus
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ companion object {
+ /**
+ * Returns an instance that encapsulates the given [output] as successful value.
+ */
+ inline fun <T> from(output: T): OutputResult<T> =
+ OutputResult(output as Any?)
+
+ /**
+ * Returns an instance that encapsulates the given OutputStatus as a failure.
+ */
+ inline fun <T> failure(failureReason: OutputStatus): OutputResult<T> =
+ OutputResult(failureReason)
+
+ /** Utility function to complete a CompletableDeferred with a successful [OutputResult]. */
+ inline fun <T> CompletableDeferred<OutputResult<T>>.completeWithOutput(output: T): Boolean {
+ return complete(from(output))
+ }
+
+ /** Utility function to complete a CompletableDeferred with a [OutputStatus] failure */
+ inline fun <T> CompletableDeferred<OutputResult<T>>.completeWithFailure(
+ status: OutputStatus
+ ): Boolean {
+ return complete(failure(status))
+ }
+
+ /**
+ * For a [Deferred] object that contains an OutputResult, determine the status based on the
+ * state of the [Deferred] or from the actual object if this status has been completed.
+ */
+ inline fun <T> Deferred<OutputResult<T>>.outputStatus(): OutputStatus {
+ return if (!isCompleted) {
+ // If the result is not completed, then this Output is in a PENDING state.
+ OutputStatus.PENDING
+ } else if (isCancelled) {
+ // If the result was canceled for any reason, then this Output is, and will not, be
+ // available.
+ OutputStatus.UNAVAILABLE
+ } else {
+ // If we reach here, the result is A) completed, and B) not canceled. read the
+ // status from the result and return it.
+ getCompleted().status
+ }
+ }
+
+ /**
+ * Get the output from this [Deferred], if available, or null.
+ */
+ inline fun <T> Deferred<OutputResult<T>>.outputOrNull(): T? {
+ if (isCompleted && !isCancelled) {
+ return getCompleted().output
+ }
+ return null
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt
new file mode 100644
index 0000000..7362c7f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 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.internal
+
+import android.os.Build
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamId
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/** Tests for [FrameCaptureQueue] */
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class FrameCaptureQueueTest {
+ private val imageStreams = listOf(StreamId(1), StreamId(2), StreamId(3))
+ private val captureQueue = FrameCaptureQueue()
+
+ private val request1 = Request(
+ streams = imageStreams
+ )
+ private val request2 = Request(
+ streams = imageStreams
+ )
+ private val request3 = Request(
+ streams = imageStreams
+ )
+
+ @Test
+ fun outputFrameQueueHoldsDeferredFrames() {
+ val frameCapture = captureQueue.enqueue(request1)
+ assertThat(frameCapture.status).isEqualTo(OutputStatus.PENDING)
+ }
+
+ @Test
+ fun outputFrameQueueCanRemoveDeferredFrames() {
+ val deferred = captureQueue.enqueue(request1)
+
+ val deferred1 = captureQueue.remove(request1)
+ val deferred2 = captureQueue.remove(request1)
+ assertThat(deferred1).isSameInstanceAs(deferred)
+ assertThat(deferred2).isNull()
+ }
+
+ @Test
+ fun outputFrameQueueHoldsMultipleDeferredFramesInOrder() {
+ val deferred1 = captureQueue.enqueue(request1)
+ val deferred2 = captureQueue.enqueue(request1)
+ val deferred3 = captureQueue.enqueue(request1)
+
+ assertThat(deferred1).isNotSameInstanceAs(deferred2)
+ assertThat(deferred2).isNotSameInstanceAs(deferred3)
+
+ val removedFrame1 = captureQueue.remove(request1)
+ val removedFrame2 = captureQueue.remove(request1)
+ val removedFrame3 = captureQueue.remove(request1)
+ val removedFrame4 = captureQueue.remove(request1)
+
+ assertThat(removedFrame1).isSameInstanceAs(deferred1)
+ assertThat(removedFrame2).isSameInstanceAs(deferred2)
+ assertThat(removedFrame3).isSameInstanceAs(deferred3)
+ assertThat(removedFrame4).isNull()
+ }
+
+ @Test
+ fun outputFrameQueueCanRemoveIntermixedRequests() {
+ // Intermixed requests (2 request1's, 1 request2, 1 request3)
+ val frame1 = captureQueue.enqueue(request1)
+ val frame2 = captureQueue.enqueue(request2)
+ val frame3 = captureQueue.enqueue(request1)
+ val frame4 = captureQueue.enqueue(request3)
+
+ // Remove request1's first
+ val removedFrame1 = captureQueue.remove(request1)
+ val removedFrame2 = captureQueue.remove(request1)
+ val removedFrame3 = captureQueue.remove(request3)
+ val removedFrame4 = captureQueue.remove(request2)
+
+ assertThat(removedFrame1).isSameInstanceAs(frame1)
+ assertThat(removedFrame2).isSameInstanceAs(frame3)
+ assertThat(removedFrame3).isSameInstanceAs(frame4)
+ assertThat(removedFrame4).isSameInstanceAs(frame2)
+ }
+
+ @Test
+ fun closingOutputFrameRemovesItFromOutputFrameQueue() {
+ val frame1 = captureQueue.enqueue(request1)
+ val frame2 = captureQueue.enqueue(request1)
+ val frame3 = captureQueue.enqueue(request1)
+
+ frame2.close()
+
+ val removedFrame1 = captureQueue.remove(request1)
+ val removedFrame2 = captureQueue.remove(request1)
+ val removedFrame3 = captureQueue.remove(request1)
+
+ assertThat(removedFrame1).isSameInstanceAs(frame1)
+ assertThat(removedFrame2).isSameInstanceAs(frame3)
+ assertThat(removedFrame3).isNull()
+ }
+
+ @Test
+ fun closingDeferredFrameCancelsResults() {
+ val frame = captureQueue.enqueue(request1)
+ frame.close()
+
+ val frameFromQueue = captureQueue.remove(request1)
+ assertThat(frameFromQueue).isNull()
+ }
+
+ @Test
+ fun canEnqueueMultipleResults() {
+ val frames = captureQueue.enqueue(listOf(request1, request1, request1))
+ assertThat(frames.size).isEqualTo(3)
+
+ val removedFrame1 = captureQueue.remove(request1)
+ val removedFrame2 = captureQueue.remove(request1)
+ val removedFrame3 = captureQueue.remove(request1)
+
+ assertThat(removedFrame1).isSameInstanceAs(frames[0])
+ assertThat(removedFrame2).isSameInstanceAs(frames[1])
+ assertThat(removedFrame3).isSameInstanceAs(frames[2])
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt
new file mode 100644
index 0000000..1ff606c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2023 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.internal
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.Frame.Companion.isFrameInfoAvailable
+import androidx.camera.camera2.pipe.Frame.Companion.isImageAvailable
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.FrameReference
+import androidx.camera.camera2.pipe.FrameReference.Companion.acquire
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.ImageSimulator
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/** Tests for [FrameDistributor] */
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class FrameDistributorTest {
+
+ private val stream1Config =
+ CameraStream.Config.create(
+ Size(1280, 720),
+ StreamFormat.YUV_420_888
+ )
+ private val stream2Config =
+ CameraStream.Config.create(
+ Size(1920, 1080), StreamFormat.YUV_420_888
+ )
+ private val streamConfigs = listOf(stream1Config, stream2Config)
+
+ private val imageSimulator = ImageSimulator(streamConfigs)
+ private val stream1Id = imageSimulator.streamGraph[stream1Config]!!.id
+ private val stream2Id = imageSimulator.streamGraph[stream2Config]!!.id
+ private val streams = listOf(stream1Id, stream2Id)
+
+ private val cameraId = imageSimulator.cameraMetadata.camera
+ private val cameraTimestamp = CameraTimestamp(1234L)
+ private val cameraFrameNumber = FrameNumber(420)
+
+ private val request = Request(streams = streams)
+ private val fakeRequestMetadata = FakeRequestMetadata.from(
+ request,
+ imageSimulator.streamToSurfaceMap,
+ repeating = false
+ )
+ private val fakeFrameInfo = FakeFrameInfo(
+ metadata = FakeFrameMetadata(
+ camera = cameraId,
+ frameNumber = cameraFrameNumber
+ ),
+ requestMetadata = fakeRequestMetadata
+ )
+
+ private val fakeFrameBuffer = FakeFrameBuffer()
+ private val frameCaptureQueue = FrameCaptureQueue()
+ private val frameDistributor =
+ FrameDistributor(imageSimulator.imageSources, frameCaptureQueue, fakeFrameBuffer)
+
+ @Test
+ fun frameDistributorSetupVerification() {
+ assertThat(imageSimulator.imageSources.keys).containsExactly(stream1Id, stream2Id)
+ assertThat(imageSimulator.streamToSurfaceMap.keys).containsExactly(stream1Id, stream2Id)
+ }
+
+ @Test
+ fun framesAreAddedToFrameBuffer() {
+ frameDistributor.onStarted(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ cameraTimestamp
+ )
+
+ assertThat(fakeFrameBuffer.frames.size).isEqualTo(1)
+
+ val frame = fakeFrameBuffer.frames[0]
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStreams).containsExactly(stream1Id, stream2Id)
+
+ // Closing should cause all outputs to be closed, since this should be the only frame.
+ frame.close()
+
+ assertThat(frame.imageStreams).containsExactly(stream1Id, stream2Id)
+
+ assertThat(frame.getFrameInfo()).isEqualTo(null)
+ assertThat(frame.getImage(stream1Id)).isEqualTo(null)
+ assertThat(frame.getImage(stream2Id)).isEqualTo(null)
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.UNAVAILABLE)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.UNAVAILABLE)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.UNAVAILABLE)
+ }
+
+ @Test
+ fun outputsAreDistributedToFrame() {
+ frameDistributor.onStarted(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ cameraTimestamp
+ )
+
+ assertThat(fakeFrameBuffer.frames.size).isEqualTo(1)
+ val frame = fakeFrameBuffer.frames[0]
+
+ val image1 = imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+ assertThat(frame.isImageAvailable(stream1Id)).isTrue()
+ assertThat(frame.isImageAvailable(stream2Id)).isFalse()
+ assertThat(frame.isFrameInfoAvailable).isFalse()
+ assertThat(image1.isClosed).isFalse()
+
+ val image2 = imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+ assertThat(frame.isImageAvailable(stream2Id)).isTrue()
+ assertThat(frame.isFrameInfoAvailable).isFalse()
+ assertThat(image2.isClosed).isFalse()
+
+ frameDistributor.onComplete(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ fakeFrameInfo
+ )
+ assertThat(frame.isFrameInfoAvailable).isTrue()
+
+ // Now close the frame (without acquiring images)
+ frame.close()
+
+ // Assert that the images are closed
+ assertThat(image1.isClosed).isTrue()
+ assertThat(image2.isClosed).isTrue()
+ }
+
+ @Test
+ fun onStartedCausesFrameCaptureToBeAvailable() {
+ val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request) as FrameCapture
+ assertThat(frameCapture.status).isEqualTo(OutputStatus.PENDING)
+
+ frameDistributor.onStarted(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ cameraTimestamp
+ )
+
+ assertThat(frameCapture.status).isEqualTo(OutputStatus.AVAILABLE)
+ val frame = frameCapture.getFrame()
+ assertThat(frame).isNotNull()
+ frame?.close()
+ }
+
+ @Test
+ fun abortedRequestsCauseFramesToBeAborted() {
+ val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+ frameDistributor.onAborted(
+ fakeRequestMetadata.request
+ )
+ assertThat(frameCapture.status).isEqualTo(OutputStatus.ERROR_OUTPUT_ABORTED)
+ assertThat(frameCapture.getFrame()).isNull()
+ }
+
+ @Test
+ fun onFailureCausesFrameInfoToBeLost() {
+ val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+ frameDistributor.onStarted(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ cameraTimestamp
+ )
+ val frame = frameCapture.getFrame()!!
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+ frameDistributor.onFailed(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ FakeRequestFailure(fakeRequestMetadata, cameraFrameNumber, wasImageCaptured = true)
+ )
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+ // Images are still delivered, even after onFailed
+ imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+ imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+ assertThat(frame.isImageAvailable(stream1Id)).isEqualTo(true)
+ assertThat(frame.isImageAvailable(stream2Id)).isEqualTo(true)
+ }
+
+ @Test
+ fun onFailureWithImageLossAllOutputsToFail() {
+ val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+ frameDistributor.onStarted(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ cameraTimestamp
+ )
+ val frame = frameCapture.getFrame()!!
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+ frameDistributor.onFailed(
+ fakeRequestMetadata,
+ cameraFrameNumber,
+ FakeRequestFailure(fakeRequestMetadata, cameraFrameNumber, wasImageCaptured = false)
+ )
+
+ assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+ assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+ assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+
+ // Images are still delivered, even after onFailed
+ val fakeImage1 = imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+ val fakeImage2 = imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+
+ assertThat(fakeImage1.isClosed).isTrue()
+ assertThat(fakeImage2.isClosed).isTrue()
+ }
+
+ @After
+ fun cleanup() {
+ imageSimulator.close()
+ }
+
+ private class FakeFrameBuffer : FrameDistributor.FrameStartedListener,
+ AutoCloseable {
+ private val lock = Any()
+ private var closed = false
+ private val _frames = mutableListOf<Frame>()
+ val frames: List<Frame>
+ get() = synchronized(lock) { _frames.toList() }
+
+ override fun onFrameStarted(frameReference: FrameReference) {
+ synchronized(lock) {
+ if (!closed) {
+ _frames.add(frameReference.acquire())
+ }
+ }
+ }
+
+ override fun close() {
+ val shouldClose: Boolean
+ synchronized(lock) {
+ shouldClose = !closed
+ closed = true
+ }
+
+ if (shouldClose) {
+ for (outputFrame in _frames) {
+ outputFrame.close()
+ }
+ _frames.clear()
+ }
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
index f157e96..cccea05 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
@@ -21,7 +21,6 @@
import androidx.camera.camera2.pipe.CameraTimestamp
import androidx.camera.camera2.pipe.Frame.Companion.isFrameInfoAvailable
import androidx.camera.camera2.pipe.Frame.Companion.isImageAvailable
-import androidx.camera.camera2.pipe.FrameId
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.OutputId
import androidx.camera.camera2.pipe.OutputStatus
@@ -57,7 +56,6 @@
private val stream2Surface = fakeSurfaces.createFakeSurface(Size(640, 480))
private val streamToSurfaceMap = mapOf(stream1Id to stream1Surface, stream2Id to stream2Surface)
- private val frameId = FrameId(240)
private val frameNumber = FrameNumber(420)
private val frameTimestampNs = 1234L
private val frameTimestamp = CameraTimestamp(frameTimestampNs)
@@ -72,7 +70,6 @@
private val frameState = FrameState(
requestMetadata = fakeRequestMetadata,
- frameId = frameId,
frameNumber = frameNumber,
frameTimestamp = frameTimestamp,
imageStreams
@@ -311,8 +308,7 @@
frameTimestamp,
42,
frameTimestamp.value,
- OutputStatus.ERROR_OUTPUT_DROPPED,
- null
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
)
assertThat(sharedOutputFrame.imageStatus(stream1Id))
@@ -338,8 +334,7 @@
frameTimestamp,
42,
frameTimestamp.value,
- OutputStatus.AVAILABLE,
- stream1OutputImage
+ OutputResult.from(stream1OutputImage)
)
// Complete streamResult2 with stream2Output3Image
@@ -348,8 +343,7 @@
frameTimestamp,
42,
frameTimestamp.value,
- OutputStatus.AVAILABLE,
- stream2OutputImage
+ OutputResult.from(stream2OutputImage)
)
// Complete frameInfoResult
@@ -358,8 +352,7 @@
frameTimestamp,
42,
frameNumber.value,
- OutputStatus.AVAILABLE,
- fakeFrameInfo
+ OutputResult.from(fakeFrameInfo)
)
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
index 6d28f9e..77e6629 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
@@ -18,7 +18,6 @@
import android.os.Build
import androidx.camera.camera2.pipe.CameraTimestamp
-import androidx.camera.camera2.pipe.FrameId
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.OutputId
import androidx.camera.camera2.pipe.OutputStatus
@@ -46,7 +45,6 @@
private val output1Id = OutputId(1)
- private val frameId = FrameId(240)
private val frameNumber = FrameNumber(420)
private val frameTimestampNs = 1234L
private val frameTimestamp = CameraTimestamp(frameTimestampNs)
@@ -75,7 +73,6 @@
private val frameState = FrameState(
requestMetadata = fakeRequestMetadata,
- frameId = frameId,
frameNumber = frameNumber,
frameTimestamp = frameTimestamp,
imageStreams
@@ -117,8 +114,7 @@
frameTimestamp,
64L,
frameTimestampNs,
- OutputStatus.AVAILABLE,
- outputImage
+ OutputResult.from(outputImage)
)
assertThat(fakeImage.isClosed).isFalse()
@@ -129,7 +125,7 @@
assertThat(fakeImage.isClosed).isTrue()
assertThat(imageResult1.status).isEqualTo(OutputStatus.UNAVAILABLE)
- val result = imageResult1.getCompletedOrNull()
+ val result = imageResult1.outputOrNull()
assertThat(result).isNull()
}
@@ -142,12 +138,11 @@
frameTimestamp,
64L,
frameTimestampNs,
- OutputStatus.AVAILABLE,
- outputImage
+ OutputResult.from(outputImage)
)
assertThat(fakeImage.isClosed).isTrue()
- assertThat(imageResult1.getCompletedOrNull()).isNull()
+ assertThat(imageResult1.outputOrNull()).isNull()
}
@Test
@@ -157,11 +152,10 @@
frameTimestamp,
64L,
frameTimestampNs,
- OutputStatus.AVAILABLE,
- outputImage
+ OutputResult.from(outputImage)
)
- val imageCopy1 = imageResult1.getCompletedOrNull()
- val imageCopy2 = imageResult1.getCompletedOrNull()
+ val imageCopy1 = imageResult1.outputOrNull()
+ val imageCopy2 = imageResult1.outputOrNull()
assertThat(imageCopy1).isNotNull()
assertThat(imageCopy2).isNotNull()
@@ -185,12 +179,11 @@
frameTimestamp,
10,
frameNumber.value,
- OutputStatus.AVAILABLE,
- fakeFrameInfo
+ OutputResult.from(fakeFrameInfo)
)
assertThat(frameState.frameInfoOutput.status).isEqualTo(OutputStatus.AVAILABLE)
- assertThat(frameState.frameInfoOutput.getCompletedOrNull()).isSameInstanceAs(fakeFrameInfo)
+ assertThat(frameState.frameInfoOutput.outputOrNull()).isSameInstanceAs(fakeFrameInfo)
}
@Test
@@ -201,11 +194,10 @@
frameTimestamp,
10,
frameNumber.value,
- OutputStatus.AVAILABLE,
- fakeFrameInfo
+ OutputResult.from(fakeFrameInfo)
)
assertThat(frameState.frameInfoOutput.status).isEqualTo(OutputStatus.UNAVAILABLE)
- assertThat(frameState.frameInfoOutput.getCompletedOrNull()).isNull()
+ assertThat(frameState.frameInfoOutput.outputOrNull()).isNull()
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
similarity index 81%
rename from camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt
rename to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
index af11146..42bf6cc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 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,13 +14,14 @@
* limitations under the License.
*/
-package androidx.camera.camera2.pipe.media
+package androidx.camera.camera2.pipe.internal
import android.os.Build
import androidx.camera.camera2.pipe.CameraTimestamp
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.OutputStatus
-import androidx.camera.camera2.pipe.media.OutputDistributor.OutputListener
+import androidx.camera.camera2.pipe.internal.OutputDistributor.OutputListener
+import androidx.camera.camera2.pipe.media.Finalizer
import com.google.common.truth.Truth.assertThat
import kotlinx.atomicfu.atomic
import org.junit.Test
@@ -61,7 +62,7 @@
@Test
fun onOutputAvailableDoesNotFinalizeOutputs() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
// When an output becomes available, ensure it is not immediately finalized.
assertThat(fakeOutput1.finalized).isFalse()
@@ -69,10 +70,10 @@
@Test
fun onOutputAvailableEvictsAndFinalizesPreviousOutputs() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
- outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+ outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+ outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
// outputDistributor will only cache up to three outputs without matching start events.
@@ -87,10 +88,13 @@
@Test
fun onOutputAvailableEvictsAndFinalizesOutputsInSequence() {
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
- outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1) // Out of order
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+ outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+ outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
+ outputDistributor.onOutputResult(
+ fakeOutput1.outputNumber,
+ OutputResult.from(fakeOutput1)
+ ) // Out of order
// FIFO Order for outputs, regardless of the output number.
// Note: Outputs are provided as [2, 3, 4, *1*]
@@ -102,13 +106,22 @@
@Test
fun onOutputAvailableWithNullEvictsAndFinalizesOutputs() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+ outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
- outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, null)
- outputDistributor.onOutputAvailable(fakeOutput5.outputNumber, null)
- outputDistributor.onOutputAvailable(fakeOutput6.outputNumber, null)
+ outputDistributor.onOutputResult(
+ fakeOutput4.outputNumber,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+ )
+ outputDistributor.onOutputResult(
+ fakeOutput5.outputNumber,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+ )
+ outputDistributor.onOutputResult(
+ fakeOutput6.outputNumber,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+ )
// Dropped outputs (null) still evict old outputs.
assertThat(fakeOutput1.finalized).isTrue()
@@ -118,8 +131,8 @@
@Test
fun closingOutputDistributorFinalizesCachedOutputs() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
// Outputs that have not been matched with started events must be closed when the
// outputDistributor is closed.
@@ -134,8 +147,8 @@
outputDistributor.close()
// Outputs that occur after close must always be finalized immediately.
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
assertThat(fakeOutput1.finalized).isTrue()
assertThat(fakeOutput2.finalized).isTrue()
@@ -146,7 +159,7 @@
// When a a start event occurs and an output is also available, ensure the callback
// is correctly invoked.
outputDistributor.startWith(pendingOutput1)
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
assertThat(pendingOutput1.isComplete).isTrue()
assertThat(pendingOutput1.output).isEqualTo(fakeOutput1)
@@ -165,7 +178,10 @@
@Test
fun pendingResultsAreMatchedWithNullOutputs() {
outputDistributor.startWith(pendingOutput1)
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, null)
+ outputDistributor.onOutputResult(
+ fakeOutput1.outputNumber,
+ OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+ )
assertThat(pendingOutput1.isComplete).isTrue()
assertThat(pendingOutput1.output).isNull()
@@ -179,7 +195,10 @@
outputDistributor.startWith(pendingOutput3)
outputDistributor.startWith(pendingOutput4)
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3) // Match 3
+ outputDistributor.onOutputResult(
+ fakeOutput3.outputNumber,
+ OutputResult.from(fakeOutput3)
+ ) // Match 3
assertThat(pendingOutput1.isComplete).isTrue() // #1 is Canceled
assertThat(pendingOutput2.isComplete).isTrue() // #2 is Canceled
@@ -231,8 +250,8 @@
@Test
fun availableOutputsAreNotDistributedToStartedOutputsAfterClose() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
outputDistributor.close()
outputDistributor.startWith(pendingOutput1) // Note: Would normally match fakeOutput1
outputDistributor.startWith(pendingOutput2) // Note: Would normally match fakeOutput2
@@ -258,8 +277,8 @@
outputDistributor.startWith(pendingOutput1) // Note: Would normally match fakeOutput1
outputDistributor.startWith(pendingOutput2) // Note: Would normally match fakeOutput2
outputDistributor.close()
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
// If we have valid start events, but then receive close, and then receive matching outputs,
// ensure all outputs are still considered dropped.
@@ -285,7 +304,7 @@
outputDistributor.startWith(pendingOutput1) // Note! Out of order start event
// Complete Output 1
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
assertThat(pendingOutput1.isComplete).isTrue()
assertThat(pendingOutput2.isComplete).isFalse() // Since 1 was out of order, do not cancel
@@ -307,7 +326,7 @@
outputDistributor.startWith(pendingOutput3) // Out of order (relative to 4)
// Complete output 3!
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
+ outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
assertThat(pendingOutput1.isComplete).isTrue() // Cancelled. 1 < 3
assertThat(pendingOutput2.isComplete).isTrue() // Cancelled. 2 < 3
@@ -331,12 +350,12 @@
outputDistributor.startWith(pendingOutput4) // Normal (> 3)
// Normal outputs complete
- outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
- outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
- outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
+ outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+ outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+ outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
// Then the out of order event completes
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
// All of the outputs are correctly distributed:
assertThat(pendingOutput1.isComplete).isTrue()
@@ -408,12 +427,12 @@
assertThat(pendingOutput3.isComplete).isFalse()
assertThat(pendingOutput2.output).isNull()
- assertThat(pendingOutput2.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+ assertThat(pendingOutput2.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
}
@Test
fun previouslyAddedOutputIsClosedAfterFailure() {
- outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+ outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
outputDistributor.onOutputFailure(pendingOutput1.cameraFrameNumber)
// Output cannot be matched with frameNumber
@@ -425,7 +444,7 @@
assertThat(fakeOutput1.finalized).isTrue()
assertThat(pendingOutput1.isComplete).isTrue()
assertThat(pendingOutput1.output).isNull()
- assertThat(pendingOutput1.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+ assertThat(pendingOutput1.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
}
/**
@@ -450,8 +469,7 @@
cameraTimestamp: CameraTimestamp,
outputSequence: Long,
outputNumber: Long,
- outputStatus: OutputStatus,
- output: FakeOutput?
+ outputResult: OutputResult<FakeOutput>
) {
// Assert that this callback has only been invoked once.
assertThat(_complete.compareAndSet(expect = false, update = true)).isTrue()
@@ -463,8 +481,8 @@
// Record the actual output and outputSequence for future checks.
this.outputSequence = outputSequence
- this.outputStatus = outputStatus
- this.output = output
+ this.outputStatus = outputResult.status
+ this.output = outputResult.output
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt
new file mode 100644
index 0000000..df25eb1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.internal
+
+import android.os.Build
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class OutputResultTest {
+
+ @Test
+ fun outputResultCanBeCreatedWithObjects() {
+ val value = Any()
+ val result = OutputResult.from(value)
+
+ assertThat(result.available).isTrue()
+ assertThat(result.output).isSameInstanceAs(value)
+ assertThat(result.status).isEqualTo(OutputStatus.AVAILABLE)
+ }
+
+ @Test
+ fun outputResultsCanFail() {
+ val result = OutputResult.failure<Any>(OutputStatus.ERROR_OUTPUT_ABORTED)
+
+ assertThat(result.available).isFalse()
+ assertThat(result.output).isNull()
+ assertThat(result.status).isEqualTo(OutputStatus.ERROR_OUTPUT_ABORTED)
+ }
+
+ @Test
+ fun outputResultWorkWithIntegers() {
+ // Check to make sure this works with integers since OutputStatus is an inline value class.
+ val value = 42
+ val result = OutputResult.from(value)
+
+ assertThat(result.available).isTrue()
+ assertThat(result.output).isSameInstanceAs(value)
+ assertThat(result.status).isEqualTo(OutputStatus.AVAILABLE)
+
+ val failed = OutputResult.failure<Int>(OutputStatus.ERROR_OUTPUT_DROPPED)
+
+ assertThat(failed.available).isFalse()
+ assertThat(failed.output).isNull()
+ assertThat(failed.status).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+ }
+
+ @Test
+ fun outputResultWithNullableTypesAccuratelyHandleFailure() {
+ val result = OutputResult.from(null)
+
+ assertThat(result.available).isFalse()
+ assertThat(result.failure).isFalse()
+ assertThat(result.status).isEqualTo(OutputStatus.UNAVAILABLE)
+ assertThat<Any?>(result.output).isNull()
+ }
+
+ @Test
+ fun deferredWithOutputResultCompleteWithRealValue() {
+ val value = 42
+ val deferred = CompletableDeferred<OutputResult<Int>>()
+
+ assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.PENDING)
+ assertThat(deferred.outputOrNull()).isNull()
+
+ deferred.completeWithOutput(value)
+
+ assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.AVAILABLE)
+ assertThat(deferred.outputOrNull()).isEqualTo(value)
+ }
+
+ @Test
+ fun deferredWithOutputResultCanBeCanceled() {
+ val deferred = CompletableDeferred<OutputResult<Int>>()
+ deferred.cancel()
+
+ assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.UNAVAILABLE)
+ assertThat(deferred.outputOrNull()).isNull()
+ }
+
+ @Test
+ fun deferredWithOutputResultCanBeFailedWithStatus() {
+ val deferred = CompletableDeferred<OutputResult<Int>>()
+ deferred.completeWithFailure(OutputStatus.ERROR_OUTPUT_DROPPED)
+
+ assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+ assertThat(deferred.outputOrNull()).isNull()
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.kt
new file mode 100644
index 0000000..e72bdc1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 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.testing
+
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.media.ImageSource
+import androidx.camera.camera2.pipe.media.ImageSourceListener
+import kotlin.reflect.KClass
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class FakeImageSource(
+ private val streamId: StreamId,
+ private val streamFormat: StreamFormat,
+ private val outputSizes: Map<OutputId, Size>,
+) : ImageSource {
+ private var listener: ImageSourceListener? = null
+ private val fakeSurface = FakeSurfaces.create(outputSizes.values.first())
+ override val surface: Surface
+ get() = fakeSurface
+
+ fun simulateImage(
+ timestamp: Long,
+ outputId: OutputId? = null,
+ ): FakeImage {
+ val id = outputId ?: outputSizes.keys.single()
+ val size = outputSizes[id]!!
+ val fakeImage = FakeImage(size.width, size.height, streamFormat.value, timestamp)
+ listener?.onImage(streamId, id, timestamp, fakeImage)
+ return fakeImage
+ }
+
+ fun simulateMissingImage(timestamp: Long, outputId: OutputId? = null) {
+ val id = outputId ?: outputSizes.keys.single()
+ listener?.onImage(streamId, id, timestamp, null)
+ }
+
+ override fun setListener(listener: ImageSourceListener) {
+ this.listener = listener
+ }
+
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
+
+ override fun close() {
+ listener = null
+ fakeSurface.release()
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt
new file mode 100644
index 0000000..332daf1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 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.testing
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.StreamGraph
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.graph.StreamGraphImpl
+import androidx.camera.camera2.pipe.media.ImageSource
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class ImageSimulator(
+ streamConfigs: List<CameraStream.Config>,
+ imageStreams: Set<CameraStream.Config>? = null,
+ defaultCameraMetadata: CameraMetadata? = null,
+ defaultStreamGraph: StreamGraph? = null
+) : AutoCloseable {
+ private val fakeSurfaces = FakeSurfaces()
+
+ val cameraMetadata = defaultCameraMetadata ?: FakeCameraMetadata()
+ val graphConfig = CameraGraph.Config(camera = cameraMetadata.camera, streams = streamConfigs)
+ val streamGraph = defaultStreamGraph ?: StreamGraphImpl(cameraMetadata, graphConfig)
+
+ private val fakeImageSources = buildMap {
+ for (config in graphConfig.streams) {
+ if (imageStreams != null && !imageStreams.contains(config)) continue
+ val cameraStream = streamGraph[config]!!
+ val fakeImageSource = FakeImageSource(
+ cameraStream.id,
+ config.outputs.first().format,
+ cameraStream.outputs.associate { it.id to it.size }
+ )
+ check(this[cameraStream.id] == null)
+ this[cameraStream.id] = fakeImageSource
+ }
+ }
+
+ val imageSources: Map<StreamId, ImageSource> = fakeImageSources
+ val imageStreams = imageSources.keys
+ val streamToSurfaceMap = buildMap {
+ for (config in graphConfig.streams) {
+ val cameraStream = streamGraph[config]!!
+ this[cameraStream.id] =
+ imageSources[cameraStream.id]?.surface ?: fakeSurfaces.createFakeSurface(
+ cameraStream.outputs.first().size
+ )
+ }
+ }
+
+ fun simulateImage(streamId: StreamId, timestamp: Long, outputId: OutputId? = null): FakeImage {
+ return fakeImageSources[streamId]!!.simulateImage(timestamp, outputId)
+ }
+
+ override fun close() {
+ for (imageSource in fakeImageSources.values) {
+ imageSource.close()
+ }
+ fakeSurfaces.close()
+ }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index b38cbf3..0a7ca49 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -1155,7 +1155,7 @@
((Camera2CameraCaptureResult) captor.getValue()).getCaptureResult();
assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
- .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG);
+ .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isFalse();
diff --git a/camera/camera-core/src/main/cpp/CMakeLists.txt b/camera/camera-core/src/main/cpp/CMakeLists.txt
index eeddf30..8293c0f 100644
--- a/camera/camera-core/src/main/cpp/CMakeLists.txt
+++ b/camera/camera-core/src/main/cpp/CMakeLists.txt
@@ -28,4 +28,8 @@
find_package(libyuv REQUIRED)
target_link_libraries(image_processing_util_jni PRIVATE ${log-lib} ${android-lib} ${jnigraphics-lib} libyuv::yuv)
-
+target_link_options(
+ image_processing_util_jni
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 4d78895..9c7be2d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -157,6 +157,48 @@
}
/**
+ * Checks if the matrix contains a mirroring.
+ *
+ * <p>This is mostly for testing if a sensor-to-buffer transformation. This method returns true
+ * if the image has been mirrored by the pipeline.
+ */
+ public static boolean isMirrored(@NonNull Matrix matrix) {
+ // We create 2 vectors, (0, 1) and (1, 0) with -90 degrees angle between them. Then we map
+ // the vectors with the matrix. If the angle changes to positive(90 degrees), we know that
+ // the matrix contains a mirroring.
+ float[] vectors = new float[]{0, 1, 1, 0};
+ matrix.mapVectors(vectors);
+ return calculateSignedAngle(vectors[0], vectors[1], vectors[2], vectors[3]) > 0;
+ }
+
+ /**
+ * Calculates the clockwise angle between 2 vectors.
+ */
+ public static float calculateSignedAngle(float v1x, float v1y, float v2x, float v2y) {
+ // Calculate the dot product
+ float dotProduct = v1x * v2x + v1y * v2y;
+
+ // Calculate the determinant (which is proportional to the sine of the angle)
+ float det = v1x * v2y - v1y * v2x;
+
+ // Calculate the magnitudes of the vectors
+ double magV1 = Math.sqrt(v1x * v1x + v1y * v1y);
+ double magV2 = Math.sqrt(v2x * v2x + v2y * v2y);
+
+ // Calculate the cosine and sine of the angle
+ double cosTheta = dotProduct / (magV1 * magV2);
+ double sinTheta = det / (magV1 * magV2);
+
+ // Calculate the angle in radians using atan2 (result ranges from -π to π)
+ double angleRad = Math.atan2(sinTheta, cosTheta);
+
+ // Convert the angle to degrees, if needed
+ double angleDeg = Math.toDegrees(angleRad);
+
+ return (float) angleDeg;
+ }
+
+ /**
* Gets the size after cropping and rotating.
*
* @return rotated size
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 12c14d0..dd3088d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -137,16 +137,30 @@
// Calculate sensorToBufferTransform
android.graphics.Matrix sensorToBufferTransform =
new android.graphics.Matrix(input.getSensorToBufferTransform());
- android.graphics.Matrix imageTransform = getRectToRect(
+ android.graphics.Matrix newTransform = getRectToRect(
new RectF(cropRect),
sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
- sensorToBufferTransform.postConcat(imageTransform);
+ sensorToBufferTransform.postConcat(newTransform);
// The aspect ratio of the output must match the aspect ratio of the crop rect. Otherwise
// the output will be stretched.
Size rotatedCropSize = getRotatedSize(cropRect, rotationDegrees);
checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
+ // Calculate the transformed crop rect.
+ Rect newCropRect;
+ if (outConfig.shouldRespectInputCropRect()) {
+ checkArgument(outConfig.getCropRect().contains(input.getCropRect()),
+ String.format("Output crop rect %s must contain input crop rect %s",
+ outConfig.getCropRect(), input.getCropRect()));
+ newCropRect = new Rect();
+ RectF newCropRectF = new RectF(input.getCropRect());
+ newTransform.mapRect(newCropRectF);
+ newCropRectF.round(newCropRect);
+ } else {
+ newCropRect = sizeToRect(outConfig.getSize());
+ }
+
// Copy the stream spec from the input to the output, except for the resolution.
StreamSpec streamSpec = input.getStreamSpec().toBuilder().setResolution(
outConfig.getSize()).build();
@@ -158,8 +172,7 @@
sensorToBufferTransform,
// The Surface transform cannot be carried over during buffer copy.
/*hasCameraTransform=*/false,
- // Crop rect is always the full size.
- sizeToRect(outConfig.getSize()),
+ newCropRect,
/*rotationDegrees=*/input.getRotationDegrees() - rotationDegrees,
// Once copied, the target rotation is no longer useful.
/*targetRotation*/ ROTATION_NOT_SPECIFIED,
@@ -402,11 +415,28 @@
public abstract int getRotationDegrees();
/**
- * The whether the stream should be mirrored.
+ * Whether the stream should be mirrored.
*/
public abstract boolean isMirroring();
/**
+ * Whether the node should respect the input's crop rect.
+ *
+ * <p>If true, the output's crop rect will be calculated based
+ * {@link OutConfig#getCropRect()} AND the input's crop rect. In this case, the
+ * {@link OutConfig#getCropRect()} must contain the input's crop rect. This applies to
+ * the scenario where the input crop rect is valid but the current node cannot apply crop
+ * rect. For example, when
+ * {@link CameraEffect#TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION} option is used.
+ *
+ * <p>If false, then the node will override input's crop rect with
+ * {@link OutConfig#getCropRect()}. This mostly applies to the sharing node. For example,
+ * the children want to crop the input stream to different sizes, in which case, the
+ * input crop rect is invalid.
+ */
+ public abstract boolean shouldRespectInputCropRect();
+
+ /**
* Creates an {@link OutConfig} instance from the input edge.
*
* <p>The result is an output edge with the input's transformation applied.
@@ -423,6 +453,8 @@
/**
* Creates an {@link OutConfig} instance with custom transformations.
+ *
+ * // TODO: remove this method and make the shouldRespectInputCropRect bit explicit.
*/
@NonNull
public static OutConfig of(@CameraEffect.Targets int targets,
@@ -431,8 +463,23 @@
@NonNull Size size,
int rotationDegrees,
boolean mirroring) {
+ return of(targets, format, cropRect, size, rotationDegrees, mirroring,
+ /*shouldRespectInputCropRect=*/false);
+ }
+
+ /**
+ * Creates an {@link OutConfig} instance with custom transformations.
+ */
+ @NonNull
+ public static OutConfig of(@CameraEffect.Targets int targets,
+ @CameraEffect.Formats int format,
+ @NonNull Rect cropRect,
+ @NonNull Size size,
+ int rotationDegrees,
+ boolean mirroring,
+ boolean shouldRespectInputCropRect) {
return new AutoValue_SurfaceProcessorNode_OutConfig(randomUUID(), targets, format,
- cropRect, size, rotationDegrees, mirroring);
+ cropRect, size, rotationDegrees, mirroring, shouldRespectInputCropRect);
}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 149cefe..687a41b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -25,6 +25,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
+import static androidx.camera.core.impl.utils.TransformUtils.sizeToRect;
import static androidx.core.util.Preconditions.checkNotNull;
import static java.util.Collections.singletonList;
@@ -321,15 +322,16 @@
// Transform the camera edge to get the input edge.
mEffectNode = new SurfaceProcessorNode(camera,
getEffect().createSurfaceProcessorInternal());
- // Effect does not apply rotation.
int rotationAppliedByEffect = getRotationAppliedByEffect();
+ Rect cropRectAppliedByEffect = getCropRectAppliedByEffect(cameraEdge);
SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(
cameraEdge.getTargets(),
cameraEdge.getFormat(),
- cameraEdge.getCropRect(),
- getRotatedSize(cameraEdge.getCropRect(), rotationAppliedByEffect),
+ cropRectAppliedByEffect,
+ getRotatedSize(cropRectAppliedByEffect, rotationAppliedByEffect),
rotationAppliedByEffect,
- /*mirroring=*/false); // Effects does not mirror.
+ getMirroringAppliedByEffect(),
+ /*shouldRespectInputCropRect=*/true);
SurfaceProcessorNode.In in = SurfaceProcessorNode.In.of(cameraEdge,
singletonList(outConfig));
SurfaceProcessorNode.Out out = mEffectNode.transform(in);
@@ -348,6 +350,34 @@
}
}
+ private boolean getMirroringAppliedByEffect() {
+ CameraEffect effect = checkNotNull(getEffect());
+ if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
+ // TODO: handle this option in VideoCapture.
+ // For a Surface that connects to the front camera directly, the texture
+ // transformation contains mirroring bit which will be applied by libraries using the
+ // TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION option.
+ CameraInternal camera = checkNotNull(getCamera());
+ return camera.isFrontFacing() && camera.getHasTransform();
+ } else {
+ // By default, the effect node does not apply any mirroring.
+ return false;
+ }
+ }
+
+ private Rect getCropRectAppliedByEffect(SurfaceEdge cameraEdge) {
+ CameraEffect effect = checkNotNull(getEffect());
+ if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
+ // TODO: handle this option in VideoCapture.
+ // Do not apply the crop rect if the effect is configured to do so.
+ Size parentSize = cameraEdge.getStreamSpec().getResolution();
+ return sizeToRect(parentSize);
+ } else {
+ // By default, the effect node does not apply any crop rect.
+ return cameraEdge.getCropRect();
+ }
+ }
+
private void addCameraErrorListener(
@NonNull SessionConfig.Builder sessionConfigBuilder,
@NonNull String cameraId,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
index 66cff98b..36c58e1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
@@ -26,6 +26,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.getRotationDegrees;
+import static androidx.camera.core.impl.utils.TransformUtils.isMirrored;
import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.camera.core.streamsharing.DynamicRangeUtils.resolveDynamicRange;
@@ -211,8 +212,11 @@
Map<UseCase, OutConfig> getChildrenOutConfigs(@NonNull SurfaceEdge sharingInputEdge,
@ImageOutputConfig.RotationValue int parentTargetRotation, boolean isViewportSet) {
Map<UseCase, OutConfig> outConfigs = new HashMap<>();
+ // TODO: we might be able to extract parent rotation degrees from the input edge's
+ // sensor-to-buffer matrix and the mirroring bit.
int parentRotationDegrees = mParentCamera.getCameraInfo().getSensorRotationDegrees(
parentTargetRotation);
+ boolean parentIsMirrored = isMirrored(sharingInputEdge.getSensorToBufferTransform());
for (UseCase useCase : mChildren) {
Pair<Rect, Size> preferredSizePair = mResolutionsMerger.getPreferredChildSizePair(
requireNonNull(mChildrenConfigsMap.get(useCase)),
@@ -234,7 +238,8 @@
cropRectBeforeScaling,
rotateSize(childSizeToScale, childParentDelta),
childParentDelta,
- useCase.isMirroringRequired(mParentCamera)));
+ // Only mirror if the parent and the child disagrees.
+ useCase.isMirroringRequired(mParentCamera) ^ parentIsMirrored));
}
return outConfigs;
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
index a1f5fa1..2aad05f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
@@ -16,6 +16,7 @@
package androidx.camera.core.impl.utils;
+import static androidx.camera.core.impl.utils.TransformUtils.calculateSignedAngle;
import static androidx.camera.core.impl.utils.TransformUtils.getExifTransform;
import static androidx.camera.core.impl.utils.TransformUtils.getRotationDegrees;
import static androidx.camera.core.impl.utils.TransformUtils.rectToVertices;
@@ -54,6 +55,48 @@
}
@Test
+ public void calculateSignedAngles() {
+ assertThat(calculateSignedAngle(0f, 1f, 1f, 0f)).isWithin(1e-3f).of(-90);
+ assertThat(calculateSignedAngle(1f, 0f, 0f, 1f)).isWithin(1e-3f).of(90);
+ }
+
+ @Test
+ public void mirrorHorizontally_isMirrored() {
+ // Arrange.
+ Matrix matrix = new Matrix();
+ // Act.
+ matrix.postScale(1, -1);
+ // Assert.
+ assertThat(TransformUtils.isMirrored(matrix)).isTrue();
+ }
+
+ @Test
+ public void mirrorVertically_isMirrored() {
+ // Arrange.
+ Matrix matrix = new Matrix();
+ // Act.
+ matrix.postScale(-1, 1);
+ // Assert.
+ assertThat(TransformUtils.isMirrored(matrix)).isTrue();
+ }
+
+ @Test
+ public void newMatrix_isNotMirrored() {
+ assertThat(TransformUtils.isMirrored(new Matrix())).isFalse();
+ }
+
+ @Test
+ public void mirrorHorizontallyAndVertically_isNotMirrored() {
+ // Arrange.
+ Matrix matrix = new Matrix();
+ // Act.
+ matrix.postScale(1, -1);
+ matrix.postScale(-1, 1);
+ // Assert.
+ assertThat(TransformUtils.isMirrored(matrix)).isFalse();
+ }
+
+ @Test
public void rotateSize_multipleOf90() {
Size size = new Size(WIDTH, HEIGHT);
//noinspection SuspiciousNameCombination
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 30b170b..8549b34 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -37,6 +37,7 @@
import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.TransformUtils
import androidx.camera.core.impl.utils.TransformUtils.getRectToRect
+import androidx.camera.core.impl.utils.TransformUtils.getRotatedSize
import androidx.camera.core.impl.utils.TransformUtils.is90or270
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
@@ -119,6 +120,69 @@
}
@Test
+ fun respectInputCropRect_outputCropRectIsBasedOnInput() {
+ // Arrange: create a input edge and a out config. The out config's crop rect contains the
+ // input edge's crop rect.
+ val inputEdge = SurfaceEdge(
+ PREVIEW,
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ StreamSpec.builder(INPUT_SIZE).build(),
+ Matrix(),
+ true,
+ Rect(160, 120, 480, 360), // 320 x 240 crop rect in the center
+ 0,
+ ROTATION_NOT_SPECIFIED,
+ true
+ )
+ val outCropRect = Rect(80, 60, 560, 420)
+ val outConfig = OutConfig.of(
+ inputEdge.targets,
+ inputEdge.format,
+ outCropRect,
+ rectToSize(outCropRect),
+ inputEdge.rotationDegrees,
+ inputEdge.isMirroring,
+ true
+ )
+ createSurfaceProcessorNode()
+ // Act: transform input.
+ val out = node.transform(SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig)))
+ // Assert: output crop rect is based on input crop rect AND the OutConfig crop rect.
+ assertThat(out[outConfig]!!.cropRect).isEqualTo(Rect(80, 60, 400, 300))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun outConfigCropRectDoesNotContainInput_throwException() {
+ // Arrange: create a input edge and a out config. The out config's crop rect does not
+ // contain the input edge's crop rect.
+ val inputEdge = SurfaceEdge(
+ PREVIEW,
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ StreamSpec.builder(INPUT_SIZE).build(),
+ Matrix(),
+ true,
+ Rect(160, 120, 480, 360), // 320 x 240 crop rect in the center
+ 0,
+ ROTATION_NOT_SPECIFIED,
+ true
+ )
+ // A crop rect that is smaller than the input's.
+ val smallCropRect = Rect(170, 120, 480, 360)
+ val outConfig = OutConfig.of(
+ inputEdge.targets,
+ inputEdge.format,
+ smallCropRect,
+ rectToSize(smallCropRect),
+ inputEdge.rotationDegrees,
+ inputEdge.isMirroring,
+ true
+ )
+ createSurfaceProcessorNode()
+ // Act: transform input which throws exception.
+ node.transform(SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig)))
+ }
+
+ @Test
fun transformInput_getCorrectSensorToBufferMatrix() {
// Arrange.
createSurfaceProcessorNode()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index bfeac2b..e427b91 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.graphics.ImageFormat
+import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
@@ -112,6 +113,7 @@
FakeCamera(null, FakeCameraInfoInternal(SENSOR_ROTATION, LENS_FACING_FRONT))
private lateinit var streamSharing: StreamSharing
private val size = Size(800, 600)
+ private val cropRect = Rect(150, 100, 750, 500)
private lateinit var defaultConfig: UseCaseConfig<*>
private lateinit var effectProcessor: FakeSurfaceProcessorInternal
private lateinit var sharingProcessor: FakeSurfaceProcessorInternal
@@ -145,26 +147,32 @@
}
@Test
- fun effectHandleRotation_remainingRotationIs0() {
+ fun effectHandleRotationAndMirroring_remainingTransformationIsEmpty() {
// Arrange: create an effect that handles rotation.
effect = FakeSurfaceEffect(
PREVIEW or VIDEO_CAPTURE,
CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION,
effectProcessor
)
- streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+ streamSharing = StreamSharing(frontCamera, setOf(child1), useCaseConfigFactory)
+ streamSharing.setViewPortCropRect(cropRect)
streamSharing.effect = effect
// Act: Bind effect and get sharing input edge.
streamSharing.bindToCamera(frontCamera, null, defaultConfig)
streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
// Assert: no remaining rotation because it's handled by the effect.
assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(0)
+ assertThat(streamSharing.sharingInputEdge!!.cropRect).isEqualTo(
+ Rect(100, 50, 500, 650)
+ )
+ assertThat(streamSharing.sharingInputEdge!!.isMirroring).isEqualTo(false)
}
@Test
- fun effectDoNotHandleRotation_remainingRotationIsNot0() {
+ fun effectDoNotHandleRotationAndMirroring_remainingTransformationIsNotEmpty() {
// Arrange: create an effect that does not handle rotation.
streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+ streamSharing.setViewPortCropRect(cropRect)
streamSharing.effect = effect
// Act: bind effect.
streamSharing.bindToCamera(frontCamera, null, defaultConfig)
@@ -172,6 +180,8 @@
// Assert: the remaining rotation still exists because the effect doesn't handle it. It will
// be handled by downstream pipeline.
assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(SENSOR_ROTATION)
+ assertThat(streamSharing.sharingInputEdge!!.cropRect).isEqualTo(Rect(0, 0, 600, 400))
+ assertThat(streamSharing.sharingInputEdge!!.isMirroring).isEqualTo(true)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
index e25e507..9582f46 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
@@ -75,7 +75,7 @@
private val CROP_RECT = Rect(0, 0, 800, 600)
// Arbitrary transform to test that the transform is propagated.
- private val SENSOR_TO_BUFFER = Matrix().apply { setScale(1f, -1f) }
+ private val SENSOR_TO_BUFFER = Matrix().apply { setRotate(90F) }
private var receivedSessionConfigError: SessionConfig.SessionError? = null
private val SESSION_CONFIG_WITH_SURFACE = SessionConfig.Builder()
.addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE))
@@ -87,9 +87,8 @@
private val surfaceEdgesToClose = mutableListOf<SurfaceEdge>()
private val parentCamera = FakeCamera()
private val child1 = FakeUseCaseConfig.Builder().setTargetRotation(Surface.ROTATION_0).build()
- private val child2 = FakeUseCaseConfig.Builder()
- .setMirrorMode(MIRROR_MODE_ON)
- .build()
+ private val child2 = FakeUseCaseConfig.Builder().setMirrorMode(MIRROR_MODE_ON).build()
+
private val childrenEdges = mapOf(
Pair(child1 as UseCase, createSurfaceEdge()),
Pair(child2 as UseCase, createSurfaceEdge())
@@ -249,6 +248,20 @@
}
@Test
+ fun parentHasMirroring_clientDoNotApplyMirroring() {
+ // Arrange: create an edge that has mirrored the input in the past.
+ val inputEdge = createSurfaceEdge(matrix = Matrix().apply { setScale(-1f, 1f) })
+ // Act: get the children's out configs.
+ val outConfigs = adapter.getChildrenOutConfigs(inputEdge, Surface.ROTATION_90, true)
+ // Assert: child1 needs additional mirroring because the parent mirrors the input while the
+ // child doesn't mirror.
+ assertThat(outConfigs[child1]!!.isMirroring).isTrue()
+ // Assert: child2 does not need additional mirroring because both the parent and the child
+ // mirrors the input.
+ assertThat(outConfigs[child2]!!.isMirroring).isFalse()
+ }
+
+ @Test
fun getChildrenOutConfigs() {
// Arrange.
val cropRect = Rect(10, 10, 410, 310)
diff --git a/camera/camera-testing/src/main/cpp/CMakeLists.txt b/camera/camera-testing/src/main/cpp/CMakeLists.txt
index b0c17f4..af959ea 100644
--- a/camera/camera-testing/src/main/cpp/CMakeLists.txt
+++ b/camera/camera-testing/src/main/cpp/CMakeLists.txt
@@ -27,4 +27,9 @@
find_library(android-lib android)
-target_link_libraries(testing_surface_format_jni ${android-lib})
\ No newline at end of file
+target_link_libraries(testing_surface_format_jni ${android-lib})
+target_link_options(
+ testing_surface_format_jni
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/camera-viewfinder-core/api/current.txt b/camera/camera-viewfinder-core/api/current.txt
index db64093..e6c2771 100644
--- a/camera/camera-viewfinder-core/api/current.txt
+++ b/camera/camera-viewfinder-core/api/current.txt
@@ -80,5 +80,9 @@
public static final class ViewfinderSurfaceRequest.Result.Companion {
}
+ public final class ViewfinderSurfaceRequestUtil {
+ method @RequiresApi(21) public static androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+ }
+
}
diff --git a/camera/camera-viewfinder-core/api/restricted_current.txt b/camera/camera-viewfinder-core/api/restricted_current.txt
index db64093..e6c2771 100644
--- a/camera/camera-viewfinder-core/api/restricted_current.txt
+++ b/camera/camera-viewfinder-core/api/restricted_current.txt
@@ -80,5 +80,9 @@
public static final class ViewfinderSurfaceRequest.Result.Companion {
}
+ public final class ViewfinderSurfaceRequestUtil {
+ method @RequiresApi(21) public static androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+ }
+
}
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.kt
new file mode 100644
index 0000000..945abdb
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 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:JvmName("ViewfinderSurfaceRequestUtil")
+
+package androidx.camera.viewfinder.surface
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import androidx.annotation.RequiresApi
+
+/**
+ * Populates [ViewfinderSurfaceRequest.Builder] from [CameraCharacteristics].
+ *
+ * The [CameraCharacteristics] will be used to populate information including lens facing,
+ * sensor orientation and [ImplementationMode].
+ * If the hardware level is legacy, the [ImplementationMode] will be set to
+ * [ImplementationMode.COMPATIBLE].
+ */
+@SuppressLint("ClassVerificationFailure")
+@RequiresApi(21)
+fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
+ cameraCharacteristics: CameraCharacteristics
+): ViewfinderSurfaceRequest.Builder {
+ setLensFacing(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)!!)
+ setSensorOrientation(
+ cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!)
+ if (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+ == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
+ setImplementationMode(androidx.camera.viewfinder.surface.ImplementationMode.COMPATIBLE)
+ }
+ return this
+}
diff --git a/camera/camera-viewfinder/api/current.txt b/camera/camera-viewfinder/api/current.txt
index a7333d7..82a5cb5 100644
--- a/camera/camera-viewfinder/api/current.txt
+++ b/camera/camera-viewfinder/api/current.txt
@@ -7,15 +7,17 @@
ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
method @UiThread public android.graphics.Bitmap? getBitmap();
- method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
+ method @Deprecated @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
- method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+ method @UiThread public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode();
+ method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest);
+ method @Deprecated @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
}
- @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
- enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
- enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
+ @Deprecated @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
+ enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
+ enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
}
@RequiresApi(21) public enum CameraViewfinder.ScaleType {
@@ -28,30 +30,31 @@
}
@RequiresApi(21) public final class CameraViewfinderExt {
- method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ method @Deprecated public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
}
- @RequiresApi(21) public class ViewfinderSurfaceRequest {
- method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
- method public int getLensFacing();
- method public android.util.Size getResolution();
- method public int getSensorOrientation();
- method public void markSurfaceSafeToRelease();
+ @Deprecated @RequiresApi(21) public class ViewfinderSurfaceRequest {
+ method @Deprecated public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+ method @Deprecated public int getLensFacing();
+ method @Deprecated public android.util.Size getResolution();
+ method @Deprecated public int getSensorOrientation();
+ method @Deprecated public void markSurfaceSafeToRelease();
}
- public static final class ViewfinderSurfaceRequest.Builder {
- ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
- ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
- ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+ @Deprecated public static final class ViewfinderSurfaceRequest.Builder {
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(android.util.Size);
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
}
public final class ViewfinderSurfaceRequestUtil {
- method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+ method @Deprecated @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
}
}
diff --git a/camera/camera-viewfinder/api/restricted_current.txt b/camera/camera-viewfinder/api/restricted_current.txt
index a7333d7..82a5cb5 100644
--- a/camera/camera-viewfinder/api/restricted_current.txt
+++ b/camera/camera-viewfinder/api/restricted_current.txt
@@ -7,15 +7,17 @@
ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
method @UiThread public android.graphics.Bitmap? getBitmap();
- method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
+ method @Deprecated @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
- method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+ method @UiThread public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode();
+ method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest);
+ method @Deprecated @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
}
- @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
- enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
- enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
+ @Deprecated @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
+ enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
+ enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
}
@RequiresApi(21) public enum CameraViewfinder.ScaleType {
@@ -28,30 +30,31 @@
}
@RequiresApi(21) public final class CameraViewfinderExt {
- method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ method @Deprecated public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
}
- @RequiresApi(21) public class ViewfinderSurfaceRequest {
- method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
- method public int getLensFacing();
- method public android.util.Size getResolution();
- method public int getSensorOrientation();
- method public void markSurfaceSafeToRelease();
+ @Deprecated @RequiresApi(21) public class ViewfinderSurfaceRequest {
+ method @Deprecated public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+ method @Deprecated public int getLensFacing();
+ method @Deprecated public android.util.Size getResolution();
+ method @Deprecated public int getSensorOrientation();
+ method @Deprecated public void markSurfaceSafeToRelease();
}
- public static final class ViewfinderSurfaceRequest.Builder {
- ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
- ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
- ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
- method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+ @Deprecated public static final class ViewfinderSurfaceRequest.Builder {
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(android.util.Size);
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+ ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+ method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
}
public final class ViewfinderSurfaceRequestUtil {
- method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+ method @Deprecated @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
}
}
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index 7ab055e..738e308 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -32,6 +32,7 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
+ api(project(':camera:camera-viewfinder-core'))
implementation("androidx.annotation:annotation-experimental:1.4.0")
implementation(libs.guavaListenableFuture)
implementation("androidx.core:core:1.7.0")
@@ -42,7 +43,6 @@
implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
implementation(libs.kotlinCoroutinesCore)
implementation(libs.kotlinCoroutinesAndroid)
- implementation project(':camera:camera-viewfinder-core')
annotationProcessor(libs.autoValue)
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
index 20d606e..e222d09 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
@@ -24,6 +24,8 @@
import androidx.camera.impl.utils.futures.FutureCallback
import androidx.camera.impl.utils.futures.Futures
import androidx.camera.viewfinder.CameraViewfinder.ScaleType.FILL_CENTER
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
import androidx.camera.viewfinder.utils.CoreAppTestUtil
import androidx.camera.viewfinder.utils.FakeActivity
import androidx.core.content.ContextCompat
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
index 4074c5e..c9e189e 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
@@ -21,6 +21,8 @@
import android.util.Size
import android.view.View
import android.widget.FrameLayout
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
import androidx.camera.viewfinder.utils.CoreAppTestUtil
import androidx.camera.viewfinder.utils.FakeActivity
import androidx.test.core.app.ActivityScenario
@@ -81,7 +83,7 @@
@After
fun tearDown() {
if (::mSurfaceRequest.isInitialized) {
- mSurfaceRequest.viewfinderSurface.close()
+ mSurfaceRequest.getSurface().close()
}
}
@@ -93,8 +95,8 @@
mImplementation.onSurfaceRequested(mSurfaceRequest)
}
- mSurfaceRequest.viewfinderSurface.surface.get(1000, TimeUnit.MILLISECONDS)
- mSurfaceRequest.viewfinderSurface.close()
+ mSurfaceRequest.getSurface().getSurfaceAsync().get(1000, TimeUnit.MILLISECONDS)
+ mSurfaceRequest.getSurface().close()
}
@Throws(Throwable::class)
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
index cf9b4aa..d80ef56 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
@@ -22,6 +22,8 @@
import android.util.Size
import android.view.TextureView
import android.widget.FrameLayout
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -76,7 +78,7 @@
if (_surfaceRequest != null) {
_surfaceRequest!!.willNotProvideSurface()
// Ensure all successful requests have their returned future finish.
- _surfaceRequest!!.viewfinderSurface.close()
+ _surfaceRequest!!.getSurface().close()
_surfaceRequest = null
}
}
@@ -89,7 +91,7 @@
fun doNotProvideSurface_ifSurfaceTextureNotAvailableYet() {
val request = surfaceRequest
implementation!!.onSurfaceRequested(request)
- request.viewfinderSurface.surface[2, TimeUnit.SECONDS]
+ request.getSurface().getSurfaceAsync()[2, TimeUnit.SECONDS]
}
@Test
@@ -97,7 +99,7 @@
fun provideSurface_ifSurfaceTextureAvailable() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val surfaceListenableFuture = surfaceRequest.viewfinderSurface.surface
+ val surfaceListenableFuture = surfaceRequest.getSurface().getSurfaceAsync()
implementation!!.mTextureView
?.surfaceTextureListener!!
.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
@@ -110,7 +112,7 @@
fun doNotDestroySurface_whenSurfaceTextureBeingDestroyed_andCameraUsingSurface() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val surfaceListenableFuture = surfaceRequest.viewfinderSurface.surface
+ val surfaceListenableFuture = surfaceRequest.getSurface().getSurfaceAsync()
val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
surfaceListenableFuture.get()
@@ -128,8 +130,8 @@
fun destroySurface_whenSurfaceTextureBeingDestroyed_andCameraNotUsingSurface() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val deferrableSurface = surfaceRequest.viewfinderSurface
- val surfaceListenableFuture = deferrableSurface.surface
+ val deferrableSurface = surfaceRequest.getSurface()
+ val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
surfaceListenableFuture.get()
@@ -151,8 +153,8 @@
fun releaseSurfaceTexture_afterSurfaceTextureDestroyed_andCameraNoLongerUsingSurface() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val deferrableSurface = surfaceRequest.viewfinderSurface
- val surfaceListenableFuture = deferrableSurface.surface
+ val deferrableSurface = surfaceRequest.getSurface()
+ val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
surfaceListenableFuture.get()
@@ -174,7 +176,7 @@
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
// Cancel the request from the camera side
- surfaceRequest.viewfinderSurface.surface.cancel(true)
+ surfaceRequest.getSurface().getSurfaceAsync().cancel(true)
// Wait enough time for mCompleter's cancellation listener to be called
Thread.sleep(1000)
@@ -211,8 +213,8 @@
fun resetSurfaceTextureOnDetachAndAttachWindow() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val deferrableSurface = surfaceRequest.viewfinderSurface
- val surfaceListenableFuture = deferrableSurface.surface
+ val deferrableSurface = surfaceRequest.getSurface()
+ val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
surfaceListenableFuture.get()
@@ -230,8 +232,8 @@
fun releaseDetachedSurfaceTexture_whenDeferrableSurfaceClose() {
val surfaceRequest = surfaceRequest
implementation!!.onSurfaceRequested(surfaceRequest)
- val deferrableSurface = surfaceRequest.viewfinderSurface
- val surfaceListenableFuture = deferrableSurface.surface
+ val deferrableSurface = surfaceRequest.getSurface()
+ val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
surfaceListenableFuture.get()
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
index 9a438c0..dc194bd 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
@@ -45,9 +45,9 @@
import androidx.camera.viewfinder.internal.quirk.DeviceQuirks;
import androidx.camera.viewfinder.internal.quirk.SurfaceViewNotCroppedByParentQuirk;
import androidx.camera.viewfinder.internal.quirk.SurfaceViewStretchedQuirk;
-import androidx.camera.viewfinder.internal.surface.ViewfinderSurfaceProvider;
import androidx.camera.viewfinder.internal.utils.Logger;
import androidx.camera.viewfinder.internal.utils.Threads;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceProvider;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
@@ -66,7 +66,8 @@
private static final String TAG = "CameraViewFinder";
@ColorRes private static final int DEFAULT_BACKGROUND_COLOR = android.R.color.black;
- private static final ImplementationMode DEFAULT_IMPL_MODE = ImplementationMode.PERFORMANCE;
+ private static final androidx.camera.viewfinder.surface.ImplementationMode DEFAULT_IMPL_MODE =
+ androidx.camera.viewfinder.surface.ImplementationMode.PERFORMANCE;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@@ -80,7 +81,7 @@
@NonNull
private final Looper mRequiredLooper = Looper.myLooper();
- @NonNull ImplementationMode mImplementationMode;
+ @NonNull androidx.camera.viewfinder.surface.ImplementationMode mImplementationMode;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@@ -90,7 +91,7 @@
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
- ViewfinderSurfaceRequest mCurrentSurfaceRequest;
+ androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest mCurrentSurfaceRequest;
private final OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -107,7 +108,9 @@
@Override
@AnyThread
- public void onSurfaceRequested(@NonNull ViewfinderSurfaceRequest surfaceRequest) {
+ public void onSurfaceRequested(
+ @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest
+ ) {
if (!Threads.isMainThread()) {
// In short term, throwing exception to guarantee onSurfaceRequest is
// called on main thread. In long term, user should be able to specify an
@@ -182,7 +185,8 @@
int implementationModeId =
attributes.getInteger(R.styleable.Viewfinder_implementationMode,
DEFAULT_IMPL_MODE.getId());
- mImplementationMode = ImplementationMode.fromId(implementationModeId);
+ mImplementationMode = androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+ implementationModeId);
} finally {
attributes.recycle();
}
@@ -206,11 +210,40 @@
* {@link ImplementationMode}.
*
* @return The {@link ImplementationMode} for {@link CameraViewfinder}.
+ * @deprecated Use {@link #getSurfaceImplementationMode()} instead.
+ * The {@link ImplementationMode} in camera-viewfinder will be made obsolete with the
+ * introduction of camera-viewfinder-core.
*/
+ @Deprecated
@UiThread
@NonNull
public ImplementationMode getImplementationMode() {
checkUiThread();
+ return ImplementationMode.fromId(mImplementationMode.getId());
+ }
+
+ /**
+ * Returns the {@link androidx.camera.viewfinder.surface.ImplementationMode}.
+ *
+ * <p> For each {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} sent to
+ * {@link CameraViewfinder}, the
+ * {@link androidx.camera.viewfinder.surface.ImplementationMode} set in the
+ * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} will be used first.
+ * If it's not set, the {@code app:implementationMode} in the layout xml will be used. If
+ * it's not set in the layout xml, the default value
+ * {@link androidx.camera.viewfinder.surface.ImplementationMode#PERFORMANCE}
+ * will be used. Each {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} sent
+ * to {@link CameraViewfinder} can override the
+ * {@link androidx.camera.viewfinder.surface.ImplementationMode} once it has set the
+ * {@link androidx.camera.viewfinder.surface.ImplementationMode}.
+ *
+ * @return The {@link androidx.camera.viewfinder.surface.ImplementationMode} for
+ * {@link CameraViewfinder}.
+ */
+ @UiThread
+ @NonNull
+ public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode() {
+ checkUiThread();
return mImplementationMode;
}
@@ -262,7 +295,7 @@
* <p> The result is a {@link ListenableFuture} of {@link Surface}, which provides the
* functionality to attach listeners and propagate exceptions.
*
- * <pre>
+ * <pre>{@code
* ViewfinderSurfaceRequest request = new ViewfinderSurfaceRequest(
* new Size(width, height), cameraManager.getCameraCharacteristics(cameraId));
*
@@ -280,22 +313,73 @@
* {@literal @}Override
* public void onFailure(Throwable t) {}
* }, ContextCompat.getMainExecutor(getContext()));
- * </pre>
+ * }</pre>
*
* @param surfaceRequest The {@link ViewfinderSurfaceRequest} to get a surface.
* @return The requested surface.
*
* @see ViewfinderSurfaceRequest
+ * @deprecated Use
+ * {@link #requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest)}
+ * instead. The {@link ViewfinderSurfaceRequest} in camera-viewfinder will be made obsolete
+ * with the introduction of camera-viewfinder-core.
*/
+ @Deprecated
@UiThread
@NonNull
public ListenableFuture<Surface> requestSurfaceAsync(
@NonNull ViewfinderSurfaceRequest surfaceRequest) {
+ return requestSurfaceAsync(surfaceRequest.getViewfinderSurfaceRequest());
+ }
+
+ /**
+ * Requests surface by sending a
+ * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}.
+ *
+ * <p> Only one request can be handled at the same time. If requesting a surface with
+ * the same {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}, the previous
+ * requested surface will be returned. If requesting a surface with a new
+ * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}, the previous
+ * requested surface will be released and a new surface will be requested.
+ *
+ * <p> The result is a {@link ListenableFuture} of {@link Surface}, which provides the
+ * functionality to attach listeners and propagate exceptions.
+ *
+ * <pre>{@code
+ * ViewfinderSurfaceRequest request = new ViewfinderSurfaceRequest(
+ * new Size(width, height), cameraManager.getCameraCharacteristics(cameraId));
+ *
+ * ListenableFuture<Surface> surfaceListenableFuture =
+ * mCameraViewFinder.requestSurfaceAsync(request);
+ *
+ * Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
+ * {@literal @}Override
+ * public void onSuccess({@literal @}Nullable Surface surface) {
+ * if (surface != null) {
+ * createCaptureSession(surface);
+ * }
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Throwable t) {}
+ * }, ContextCompat.getMainExecutor(getContext()));
+ * }</pre>
+ *
+ * @param surfaceRequest The {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}
+ * to get a surface.
+ * @return The requested surface.
+ *
+ * @see androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+ */
+ @UiThread
+ @NonNull
+ public ListenableFuture<Surface> requestSurfaceAsync(
+ @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest) {
checkUiThread();
if (mCurrentSurfaceRequest != null
&& surfaceRequest.equals(mCurrentSurfaceRequest)) {
- return mCurrentSurfaceRequest.getViewfinderSurface().getSurface();
+ return mCurrentSurfaceRequest.getSurface().getSurfaceAsync();
}
if (mCurrentSurfaceRequest != null) {
@@ -303,7 +387,7 @@
}
ListenableFuture<Surface> surfaceListenableFuture =
- surfaceRequest.getViewfinderSurface().getSurface();
+ surfaceRequest.getSurface().getSurfaceAsync();
mCurrentSurfaceRequest = surfaceRequest;
provideSurfaceIfReady();
@@ -362,7 +446,9 @@
}
@VisibleForTesting
- static boolean shouldUseTextureView(@NonNull final ImplementationMode implementationMode) {
+ static boolean shouldUseTextureView(
+ @NonNull final androidx.camera.viewfinder.surface.ImplementationMode implementationMode
+ ) {
boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
|| DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
if (Build.VERSION.SDK_INT <= 24 || hasSurfaceViewQuirk) {
@@ -392,7 +478,8 @@
}
private boolean provideSurfaceIfReady() {
- final ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
+ final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest =
+ mCurrentSurfaceRequest;
final ViewfinderSurfaceProvider surfaceProvider = mSurfaceProvider;
if (surfaceProvider != null && surfaceRequest != null) {
surfaceProvider.onSurfaceRequested(surfaceRequest);
@@ -428,7 +515,10 @@
* {@link TextureView} is better supported by a wider range of devices. The option is used by
* {@link CameraViewfinder} to decide what is the best internal implementation given the device
* capabilities and user configurations.
+ *
+ * @deprecated Use {@link androidx.camera.viewfinder.surface.ImplementationMode} instead.
*/
+ @Deprecated
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public enum ImplementationMode {
@@ -603,7 +693,8 @@
public void onDisplayChanged(int displayId) {
Display display = getDisplay();
if (display != null && display.getDisplayId() == displayId) {
- ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
+ androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest =
+ mCurrentSurfaceRequest;
if (surfaceRequest != null) {
mViewfinderTransformation.updateTransformInfo(
createTransformInfo(surfaceRequest.getResolution(),
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
index 1193fc1..f5dfce2 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
@@ -22,11 +22,21 @@
/**
* Provides a suspending function of [CameraViewfinder.requestSurfaceAsync] to request
- * a [Surface] by sending a [ViewfinderSurfaceRequest].
+ * a [Surface] by sending a [androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest].
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
object CameraViewfinderExt {
+
+ @Suppress("DEPRECATION")
+ @Deprecated(message = "Use androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest " +
+ "as argument",
+ replaceWith = ReplaceWith("requestSurface using " +
+ "androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest"))
suspend fun CameraViewfinder.requestSurface(
viewfinderSurfaceRequest: ViewfinderSurfaceRequest
): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
+
+ suspend fun CameraViewfinder.requestSurface(
+ viewfinderSurfaceRequest: androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+ ): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
index 9b56178..986b388 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
@@ -32,6 +32,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import androidx.camera.viewfinder.internal.utils.Logger;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
import androidx.core.content.ContextCompat;
import androidx.core.util.Preconditions;
@@ -254,7 +255,7 @@
private void invalidateSurface() {
if (mSurfaceRequest != null) {
Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
- mSurfaceRequest.getViewfinderSurface().close();
+ mSurfaceRequest.getSurface().close();
}
}
}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
index e45a70d..730966f 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
@@ -29,8 +29,9 @@
import androidx.camera.impl.utils.executor.CameraExecutors;
import androidx.camera.impl.utils.futures.FutureCallback;
import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest.Result;
import androidx.camera.viewfinder.internal.utils.Logger;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
@@ -82,6 +83,7 @@
mTextureView.setLayoutParams(
new FrameLayout.LayoutParams(mResolution.getWidth(), mResolution.getHeight()));
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
+ @SuppressWarnings("ObjectToString")
@Override
public void onSurfaceTextureAvailable(@NonNull final SurfaceTexture surfaceTexture,
final int width, final int height) {
@@ -95,7 +97,7 @@
if (mSurfaceReleaseFuture != null && mSurfaceRequest != null) {
Preconditions.checkNotNull(mSurfaceRequest);
Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
- mSurfaceRequest.getViewfinderSurface().close();
+ mSurfaceRequest.getSurface().close();
} else {
tryToProvideViewfinderSurface();
}
@@ -119,7 +121,7 @@
new FutureCallback<Result>() {
@Override
public void onSuccess(Result result) {
- Preconditions.checkState(result.getResultCode()
+ Preconditions.checkState(result.getCode()
!= Result.RESULT_SURFACE_ALREADY_PROVIDED,
"Unexpected result from SurfaceRequest. Surface was "
+ "provided twice.");
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
index 326cedc..e78aff8 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
/**
* Wraps the underlying handling of the {@link android.view.Surface} used for viewfinder, which is
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
index 9eace4a..7ee6d90 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
@@ -33,27 +33,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.impl.utils.executor.CameraExecutors;
-import androidx.camera.impl.utils.futures.FutureCallback;
-import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode;
-import androidx.camera.viewfinder.internal.surface.ViewfinderSurface;
-import androidx.camera.viewfinder.internal.utils.Logger;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Consumer;
-import androidx.core.util.Preconditions;
+import androidx.camera.viewfinder.impl.surface.DeferredSurface;
-import com.google.auto.value.AutoValue;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
/**
* The request to get a {@link Surface} to display camera feed.
@@ -67,173 +53,33 @@
*
* <p> Calling {@link ViewfinderSurfaceRequest#markSurfaceSafeToRelease()} will notify the
* surface provider that the surface is not needed and related resources can be released.
+ *
+ * @deprecated Use {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} instead.
*/
+@Deprecated
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ViewfinderSurfaceRequest {
private static final String TAG = "ViewfinderSurfaceRequest";
- @NonNull private final Size mResolution;
- @NonNull private final ViewfinderSurface mInternalViewfinderSurface;
- @NonNull private final CallbackToFutureAdapter.Completer<Void> mRequestCancellationCompleter;
- @NonNull private final ListenableFuture<Void> mSessionStatusFuture;
- @NonNull private final CallbackToFutureAdapter.Completer<Surface> mSurfaceCompleter;
- @LensFacingValue private int mLensFacing;
- @SensorOrientationDegreesValue private int mSensorOrientation;
- @Nullable
- private ImplementationMode mImplementationMode;
- @SuppressWarnings("WeakerAccess") /*synthetic accessor */
- @NonNull
- final ListenableFuture<Surface> mSurfaceFuture;
+ @NonNull private androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+ mViewfinderSurfaceRequest;
/**
* Creates a new surface request with surface resolution, camera device, lens facing and
* sensor orientation information.
*
- * @param resolution The requested surface resolution. It is the output surface size
- * the camera is configured with, instead of {@link CameraViewfinder}
- * view size.
- * @param lensFacing The camera lens facing.
- * @param sensorOrientation THe camera sensor orientation.
- * @param implementationMode The {@link ImplementationMode} to apply to the viewfinder.
+ * surfaceRequest The {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}.
*/
ViewfinderSurfaceRequest(
- @NonNull Size resolution,
- @LensFacingValue int lensFacing,
- @SensorOrientationDegreesValue int sensorOrientation,
- @Nullable ImplementationMode implementationMode) {
- mResolution = resolution;
- mLensFacing = lensFacing;
- mSensorOrientation = sensorOrientation;
- mImplementationMode = implementationMode;
-
- // To ensure concurrency and ordering, operations are chained. Completion can only be
- // triggered externally by the top-level completer (mSurfaceCompleter). The other future
- // completers are only completed by callbacks set up within the constructor of this class
- // to ensure correct ordering of events.
-
- // Cancellation listener must be called last to ensure the result can be retrieved from
- // the session listener.
- String surfaceRequestString =
- "SurfaceRequest[size: " + resolution + ", id: " + this.hashCode() + "]";
- AtomicReference<CallbackToFutureAdapter.Completer<Void>> cancellationCompleterRef =
- new AtomicReference<>(null);
- ListenableFuture<Void> requestCancellationFuture =
- CallbackToFutureAdapter.getFuture(completer -> {
- cancellationCompleterRef.set(completer);
- return surfaceRequestString + "-cancellation";
- });
- CallbackToFutureAdapter.Completer<Void> requestCancellationCompleter =
- Preconditions.checkNotNull(cancellationCompleterRef.get());
- mRequestCancellationCompleter = requestCancellationCompleter;
-
- // Surface session status future completes and is responsible for finishing the
- // cancellation listener.
- AtomicReference<CallbackToFutureAdapter.Completer<Void>> sessionStatusCompleterRef =
- new AtomicReference<>(null);
- mSessionStatusFuture = CallbackToFutureAdapter.getFuture(completer -> {
- sessionStatusCompleterRef.set(completer);
- return surfaceRequestString + "-status";
- });
-
- Futures.addCallback(mSessionStatusFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- // Cancellation didn't occur, so complete the cancellation future. There
- // shouldn't ever be any standard listeners on this future, so nothing should be
- // invoked.
- Preconditions.checkState(requestCancellationCompleter.set(null));
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- if (t instanceof RequestCancelledException) {
- // Cancellation occurred. Notify listeners.
- Preconditions.checkState(requestCancellationFuture.cancel(false));
- } else {
- // Cancellation didn't occur, complete the future so cancellation listeners
- // are not invoked.
- Preconditions.checkState(requestCancellationCompleter.set(null));
- }
- }
- }, CameraExecutors.directExecutor());
-
- // Create the surface future/completer. This will be used to complete the rest of the
- // future chain and can be set externally via SurfaceRequest methods.
- CallbackToFutureAdapter.Completer<Void> sessionStatusCompleter =
- Preconditions.checkNotNull(sessionStatusCompleterRef.get());
- AtomicReference<CallbackToFutureAdapter.Completer<Surface>> surfaceCompleterRef =
- new AtomicReference<>(null);
- mSurfaceFuture = CallbackToFutureAdapter.getFuture(completer -> {
- surfaceCompleterRef.set(completer);
- return surfaceRequestString + "-Surface";
- });
- mSurfaceCompleter = Preconditions.checkNotNull(surfaceCompleterRef.get());
-
- // Create the viewfinder surface which will be used for communicating when the
- // camera and consumer are done using the surface. Note this anonymous inner class holds
- // an implicit reference to the ViewfinderSurfaceRequest. This is by design, and ensures the
- // ViewfinderSurfaceRequest and all contained future completers will not be garbage
- // collected as long as the ViewfinderSurface is referenced externally (via
- // getViewfinderSurface()).
- mInternalViewfinderSurface = new ViewfinderSurface() {
- @NonNull
- @Override
- protected ListenableFuture<Surface> provideSurfaceAsync() {
- Logger.d(TAG,
- "mInternalViewfinderSurface + " + mInternalViewfinderSurface + " "
- + "provideSurface");
- return mSurfaceFuture;
- }
- };
- ListenableFuture<Void> terminationFuture =
- mInternalViewfinderSurface.getTerminationFuture();
-
- // Propagate surface completion to the session future.
- Futures.addCallback(mSurfaceFuture, new FutureCallback<Surface>() {
- @Override
- public void onSuccess(@Nullable Surface surface) {
- // On successful setting of a surface, defer completion of the session future to
- // the ViewfinderSurface termination future. Once that future completes, then it
- // is safe to release the Surface and associated resources.
-
- Futures.propagate(terminationFuture, sessionStatusCompleter);
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- // Translate cancellation into a SurfaceRequestCancelledException. Other
- // exceptions mean either the request was completed via willNotProvideSurface() or a
- // programming error occurred. In either case, the user will never see the
- // session future (an immediate future will be returned instead), so complete the
- // future so cancellation listeners are never called.
- if (t instanceof CancellationException) {
- Preconditions.checkState(sessionStatusCompleter.setException(
- new RequestCancelledException(
- surfaceRequestString + " cancelled.", t)));
- } else {
- sessionStatusCompleter.set(null);
- }
- }
- }, CameraExecutors.directExecutor());
-
- // If the viewfinder surface is terminated, there are two cases:
- // 1. The surface has not yet been provided to the camera (or marked as 'will not
- // complete'). Treat this as if the surface request has been cancelled.
- // 2. The surface was already provided to the camera. In this case the camera is now
- // finished with the surface, so cancelling the surface future below will be a no-op.
- terminationFuture.addListener(() -> {
- Logger.d(TAG,
- "mInternalViewfinderSurface + " + mInternalViewfinderSurface + " "
- + "terminateFuture triggered");
- mSurfaceFuture.cancel(true);
- }, CameraExecutors.directExecutor());
+ @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest) {
+ mViewfinderSurfaceRequest = surfaceRequest;
}
@Override
@SuppressWarnings("GenericException") // super.finalize() throws Throwable
protected void finalize() throws Throwable {
- mInternalViewfinderSurface.close();
+ mViewfinderSurfaceRequest.getSurface().close();
super.finalize();
}
@@ -250,7 +96,7 @@
*/
@NonNull
public Size getResolution() {
- return mResolution;
+ return mViewfinderSurfaceRequest.getResolution();
}
/**
@@ -263,7 +109,7 @@
*/
@SensorOrientationDegreesValue
public int getSensorOrientation() {
- return mSensorOrientation;
+ return mViewfinderSurfaceRequest.getSensorOrientation();
}
/**
@@ -276,20 +122,30 @@
*/
@LensFacingValue
public int getLensFacing() {
- return mLensFacing;
+ return mViewfinderSurfaceRequest.getLensFacing();
}
/**
- * Returns the {@link ImplementationMode}.
+ * Returns the {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
*
- * <p>The value is set by {@link Builder#setImplementationMode(ImplementationMode)}.
+ * <p>The value is set by {@link
+ * Builder#setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode)
+ * }.
*
- * @return {@link ImplementationMode}. The value will be null if it's not set via
- * {@link Builder#setImplementationMode(ImplementationMode)}.
+ * @return {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
+ * The value will be null if it's not set via
+ * {@link Builder#setImplementationMode(
+ * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode)}.
*/
@Nullable
- public ImplementationMode getImplementationMode() {
- return mImplementationMode;
+ public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode() {
+ return androidx.camera.viewfinder.CameraViewfinder.ImplementationMode.fromId(
+ mViewfinderSurfaceRequest.getImplementationMode().getId());
+ }
+
+ @NonNull
+ androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest getViewfinderSurfaceRequest() {
+ return mViewfinderSurfaceRequest;
}
/**
@@ -299,125 +155,30 @@
* related resources can be released.
*/
public void markSurfaceSafeToRelease() {
- mInternalViewfinderSurface.close();
+ mViewfinderSurfaceRequest.markSurfaceSafeToRelease();
}
@NonNull
- ViewfinderSurface getViewfinderSurface() {
- return mInternalViewfinderSurface;
+ DeferredSurface getViewfinderSurface() {
+ return mViewfinderSurfaceRequest.getSurface();
}
@SuppressLint("PairedRegistration")
void addRequestCancellationListener(@NonNull Executor executor,
@NonNull Runnable listener) {
- mRequestCancellationCompleter.addCancellationListener(listener, executor);
- }
-
- /**
- * Completes the request for a {@link Surface} if it has not already been
- * completed or cancelled.
- *
- * <p>Once the camera no longer needs the provided surface, the {@code resultListener} will be
- * invoked with a {@link Result} containing {@link Result#RESULT_SURFACE_USED_SUCCESSFULLY}.
- * At this point it is safe to release the surface and any underlying resources. Releasing
- * the surface before receiving this signal may cause undesired behavior on lower API levels.
- *
- * <p>If the request is cancelled by the camera before successfully attaching the
- * provided surface to the camera, then the {@code resultListener} will be invoked with a
- * {@link Result} containing {@link Result#RESULT_REQUEST_CANCELLED}.
- *
- * <p>If the request was previously completed via {@link #willNotProvideSurface()}, then
- * {@code resultListener} will be invoked with a {@link Result} containing
- * {@link Result#RESULT_WILL_NOT_PROVIDE_SURFACE}.
- *
- * <p>Upon returning from this method, the surface request is guaranteed to be complete.
- * However, only the {@code resultListener} provided to the first invocation of this method
- * should be used to track when the provided {@link Surface} is no longer in use by the
- * camera, as subsequent invocations will always invoke the {@code resultListener} with a
- * {@link Result} containing {@link Result#RESULT_SURFACE_ALREADY_PROVIDED}.
- *
- * @param surface The surface which will complete the request.
- * @param executor Executor used to execute the {@code resultListener}.
- * @param resultListener Listener used to track how the surface is used by the camera in
- * response to being provided by this method.
- *
- */
- void provideSurface(
- @NonNull Surface surface,
- @NonNull Executor executor,
- @NonNull Consumer<Result> resultListener) {
- if (mSurfaceCompleter.set(surface) || mSurfaceFuture.isCancelled()) {
- // Session will be pending completion (or surface request was cancelled). Return the
- // session future.
- Futures.addCallback(mSessionStatusFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- resultListener.accept(Result.of(Result.RESULT_SURFACE_USED_SUCCESSFULLY,
- surface));
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- Preconditions.checkState(t instanceof RequestCancelledException, "Camera "
- + "surface session should only fail with request "
- + "cancellation. Instead failed due to:\n" + t);
- resultListener.accept(Result.of(Result.RESULT_REQUEST_CANCELLED, surface));
- }
- }, executor);
- } else {
- // Surface request is already complete
- Preconditions.checkState(mSurfaceFuture.isDone());
- try {
- mSurfaceFuture.get();
- // Getting this far means the surface was already provided.
- executor.execute(
- () -> resultListener.accept(
- Result.of(Result.RESULT_SURFACE_ALREADY_PROVIDED, surface)));
- } catch (InterruptedException | ExecutionException e) {
- executor.execute(
- () -> resultListener.accept(
- Result.of(Result.RESULT_WILL_NOT_PROVIDE_SURFACE, surface)));
- }
- }
- }
-
- /**
- * Signals that the request will never be fulfilled.
- *
- * <p>This may be called in the case where the application may be shutting down and a
- * surface will never be produced to fulfill the request.
- *
- * <p>This will be called by CameraViewfinder as soon as it is known that the request will not
- * be fulfilled. Failure to complete the SurfaceRequest via {@code willNotProvideSurface()}
- * or {@link #provideSurface(Surface, Executor, Consumer)} may cause long delays in shutting
- * down the camera.
- *
- * <p>Upon returning from this method, the request is guaranteed to be complete, regardless
- * of the return value. If the request was previously successfully completed by
- * {@link #provideSurface(Surface, Executor, Consumer)}, invoking this method will return
- * {@code false}, and will have no effect on how the surface is used by the camera.
- *
- * @return {@code true} if this call to {@code willNotProvideSurface()} successfully
- * completes the request, i.e., the request has not already been completed via
- * {@link #provideSurface(Surface, Executor, Consumer)} or by a previous call to
- * {@code willNotProvideSurface()} and has not already been cancelled by the camera.
- *
- */
- boolean willNotProvideSurface() {
- return mSurfaceCompleter.setException(
- new ViewfinderSurface.SurfaceUnavailableException("Surface request "
- + "will not complete."));
+ mViewfinderSurfaceRequest.addRequestCancellationListener(executor, listener);
}
/**
* Builder for {@link ViewfinderSurfaceRequest}.
+ *
+ * @deprecated Use {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder}
+ * instead.
*/
+ @Deprecated
public static final class Builder {
-
- @NonNull private final Size mResolution;
- @LensFacingValue private int mLensFacing = LENS_FACING_BACK;
- @SensorOrientationDegreesValue private int mSensorOrientation = 0;
- @Nullable private ImplementationMode mImplementationMode;
+ @NonNull
+ private androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder mBuilder;
/**
* Constructor for {@link Builder}.
@@ -427,7 +188,8 @@
* @param resolution viewfinder resolution.
*/
public Builder(@NonNull Size resolution) {
- mResolution = resolution;
+ mBuilder = new androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder(
+ resolution);
}
/**
@@ -439,10 +201,7 @@
* @param builder {@link Builder} instance.
*/
public Builder(@NonNull Builder builder) {
- mResolution = builder.mResolution;
- mImplementationMode = builder.mImplementationMode;
- mLensFacing = builder.mLensFacing;
- mSensorOrientation = builder.mSensorOrientation;
+ mBuilder = builder.mBuilder;
}
/**
@@ -455,32 +214,46 @@
* @param surfaceRequest {@link ViewfinderSurfaceRequest} instance.
*/
public Builder(@NonNull ViewfinderSurfaceRequest surfaceRequest) {
- mResolution = surfaceRequest.getResolution();
- mImplementationMode = surfaceRequest.getImplementationMode();
- mLensFacing = surfaceRequest.getLensFacing();
- mSensorOrientation = surfaceRequest.getSensorOrientation();
+ mBuilder = new androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder(
+ surfaceRequest.getResolution());
+ mBuilder.setSensorOrientation(surfaceRequest.getSensorOrientation());
+ mBuilder.setLensFacing(surfaceRequest.getLensFacing());
+ mBuilder.setImplementationMode(
+ androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+ surfaceRequest.getImplementationMode().getId()));
}
/**
- * Sets the {@link ImplementationMode}.
+ * Sets the {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
*
* <p><b>Possible values:</b></p>
* <ul>
- * <li>{@link ImplementationMode#PERFORMANCE PERFORMANCE}</li>
- * <li>{@link ImplementationMode#COMPATIBLE COMPATIBLE}</li>
+ * <li>{@link
+ * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#PERFORMANCE PERFORMANCE}
+ * </li>
+ * <li>{@link
+ * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#COMPATIBLE COMPATIBLE}
+ * </li>
* </ul>
*
- * <p>If not set or setting to null, the {@link ImplementationMode} set via {@code app
+ * <p>If not set or setting to null, the
+ * {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode} set via {@code app
* :implementationMode} in layout xml will be used for {@link CameraViewfinder}. If not
- * set in the layout xml, the default value {@link ImplementationMode#PERFORMANCE} will
+ * set in the layout xml, the default value
+ * {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#PERFORMANCE} will
* be used in {@link CameraViewfinder}.
*
- * @param implementationMode The {@link ImplementationMode}.
+ * @param implementationMode The {@link
+ * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
* @return This builder.
*/
@NonNull
- public Builder setImplementationMode(@Nullable ImplementationMode implementationMode) {
- mImplementationMode = implementationMode;
+ public Builder setImplementationMode(
+ @Nullable
+ androidx.camera.viewfinder.CameraViewfinder.ImplementationMode implementationMode) {
+ mBuilder.setImplementationMode(
+ androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+ implementationMode.getId()));
return this;
}
@@ -503,7 +276,7 @@
*/
@NonNull
public Builder setLensFacing(@LensFacingValue int lensFacing) {
- mLensFacing = lensFacing;
+ mBuilder.setLensFacing(lensFacing);
return this;
}
@@ -522,7 +295,7 @@
*/
@NonNull
public Builder setSensorOrientation(@SensorOrientationDegreesValue int sensorOrientation) {
- mSensorOrientation = sensorOrientation;
+ mBuilder.setSensorOrientation(sensorOrientation);
return this;
}
@@ -532,153 +305,7 @@
*/
@NonNull
public ViewfinderSurfaceRequest build() {
- if (mLensFacing != LENS_FACING_FRONT
- && mLensFacing != LENS_FACING_BACK
- && mLensFacing != LENS_FACING_EXTERNAL) {
- throw new IllegalArgumentException("Lens facing value: " + mLensFacing + " is "
- + "invalid");
- }
-
- if (mSensorOrientation != 0
- && mSensorOrientation != 90
- && mSensorOrientation != 180
- && mSensorOrientation != 270) {
- throw new IllegalArgumentException("Sensor orientation value: "
- + mSensorOrientation + " is invalid");
- }
-
- return new ViewfinderSurfaceRequest(
- mResolution,
- mLensFacing,
- mSensorOrientation,
- mImplementationMode);
- }
- }
-
- static final class RequestCancelledException extends RuntimeException {
- RequestCancelledException(@NonNull String message, @NonNull Throwable cause) {
- super(message, cause);
- }
- }
-
- /**
- * Result of providing a surface to a {@link ViewfinderSurfaceRequest} via
- * {@link #provideSurface(Surface, Executor, Consumer)}.
- *
- */
- @AutoValue
- abstract static class Result {
-
- /**
- * Possible result codes.
- *
- */
- @IntDef({RESULT_SURFACE_USED_SUCCESSFULLY, RESULT_REQUEST_CANCELLED, RESULT_INVALID_SURFACE,
- RESULT_SURFACE_ALREADY_PROVIDED, RESULT_WILL_NOT_PROVIDE_SURFACE})
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY)
- public @interface ResultCode {
- }
-
- /**
- * Provided surface was successfully used by the camera and eventually detached once no
- * longer needed by the camera.
- *
- * <p>This result denotes that it is safe to release the {@link Surface} and any underlying
- * resources.
- *
- * <p>For compatibility reasons, the {@link Surface} object should not be reused by
- * future {@link ViewfinderSurfaceRequest SurfaceRequests}, and a new surface should be
- * created instead.
- */
- public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0;
-
- /**
- * Provided surface was never attached to the camera due to the
- * {@link ViewfinderSurfaceRequest} being cancelled by the camera.
- *
- * <p>It is safe to release or reuse {@link Surface}, assuming it was not previously
- * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}. If
- * reusing the surface for a future surface request, it should be verified that the
- * surface still matches the resolution specified by
- * {@link ViewfinderSurfaceRequest#getResolution()}.
- */
- public static final int RESULT_REQUEST_CANCELLED = 1;
-
- /**
- * Provided surface could not be used by the camera.
- *
- * <p>This is likely due to the {@link Surface} being closed prematurely or the resolution
- * of the surface not matching the resolution specified by
- * {@link ViewfinderSurfaceRequest#getResolution()}.
- */
- public static final int RESULT_INVALID_SURFACE = 2;
-
- /**
- * Surface was not attached to the camera through this invocation of
- * {@link #provideSurface(Surface, Executor, Consumer)} due to the
- * {@link ViewfinderSurfaceRequest} already being complete with a surface.
- *
- * <p>The {@link ViewfinderSurfaceRequest} has already been completed by a previous
- * invocation of {@link #provideSurface(Surface, Executor, Consumer)}.
- *
- * <p>It is safe to release or reuse the {@link Surface}, assuming it was not previously
- * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}.
- */
- public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3;
-
- /**
- * Surface was not attached to the camera through this invocation of
- * {@link #provideSurface(Surface, Executor, Consumer)} due to the
- * {@link ViewfinderSurfaceRequest} already being marked as "will not provide surface".
- *
- * <p>The {@link ViewfinderSurfaceRequest} has already been marked as 'will not provide
- * surface' by a previous invocation of {@link #willNotProvideSurface()}.
- *
- * <p>It is safe to release or reuse the {@link Surface}, assuming it was not previously
- * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}.
- */
- public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4;
-
- /**
- * Creates a result from the given result code and surface.
- *
- * <p>Can be used to compare to results returned to {@code resultListener} in
- * {@link #provideSurface(Surface, Executor, Consumer)}.
- *
- * @param code One of {@link #RESULT_SURFACE_USED_SUCCESSFULLY},
- * {@link #RESULT_REQUEST_CANCELLED}, {@link #RESULT_INVALID_SURFACE},
- * {@link #RESULT_SURFACE_ALREADY_PROVIDED}, or
- * {@link #RESULT_WILL_NOT_PROVIDE_SURFACE}.
- * @param surface The {@link Surface} used to complete the {@link ViewfinderSurfaceRequest}.
- */
- @NonNull
- static Result of(@ResultCode int code, @NonNull Surface surface) {
- return new AutoValue_ViewfinderSurfaceRequest_Result(code, surface);
- }
-
- /**
- * Returns the result of invoking {@link #provideSurface(Surface, Executor, Consumer)}
- * with the surface from {@link #getSurface()}.
- *
- * @return One of {@link #RESULT_SURFACE_USED_SUCCESSFULLY},
- * {@link #RESULT_REQUEST_CANCELLED}, {@link #RESULT_INVALID_SURFACE}, or
- * {@link #RESULT_SURFACE_ALREADY_PROVIDED}, {@link #RESULT_WILL_NOT_PROVIDE_SURFACE}.
- */
- @ResultCode
- public abstract int getResultCode();
-
- /**
- * The surface used to complete a {@link ViewfinderSurfaceRequest} with
- * {@link #provideSurface(Surface, Executor, Consumer)}.
- *
- * @return the surface.
- */
- @NonNull
- public abstract Surface getSurface();
-
- // Ensure Result can't be subclassed outside the package
- Result() {
+ return new ViewfinderSurfaceRequest(mBuilder.build());
}
}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
index 1f16434..9283ecc 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
@file:JvmName("ViewfinderSurfaceRequestUtil")
package androidx.camera.viewfinder
@@ -30,6 +31,10 @@
* sensor orientation and [ImplementationMode]. If the hardware level is legacy,
* the [ImplementationMode] will be set to [ImplementationMode.COMPATIBLE].
*/
+@Deprecated(message = "Use androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest as argument",
+ replaceWith = ReplaceWith(
+ "populateFromCharacteristics returning " +
+ "androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder"))
@SuppressLint("ClassVerificationFailure")
@RequiresApi(21)
fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java
deleted file mode 100644
index 24423ac..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java
+++ /dev/null
@@ -1,105 +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.
- */
-
-package androidx.camera.viewfinder.internal.surface;
-
-import android.view.Surface;
-import android.view.SurfaceView;
-import android.view.TextureView;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.internal.utils.Logger;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-/**
- * A class for creating and tracking use of a {@link Surface} in an asynchronous manner.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public abstract class ViewfinderSurface {
-
- private static final String TAG = "ViewfinderSurface";
-
- @NonNull private final Object mLock = new Object();
- @NonNull private final ListenableFuture<Void> mTerminationFuture;
-
- @GuardedBy("mLock")
- private boolean mClosed = false;
-
- @Nullable
- @GuardedBy("mLock")
- private CallbackToFutureAdapter.Completer<Void> mTerminationCompleter;
-
- public ViewfinderSurface() {
- mTerminationFuture = CallbackToFutureAdapter.getFuture(completer -> {
- synchronized (mLock) {
- mTerminationCompleter = completer;
- }
- return "ViewfinderSurface-termination(" + ViewfinderSurface.this + ")";
- });
- }
-
- @NonNull
- public final ListenableFuture<Surface> getSurface() {
- return provideSurfaceAsync();
- }
-
- @NonNull
- public ListenableFuture<Void> getTerminationFuture() {
- return Futures.nonCancellationPropagating(mTerminationFuture);
- }
-
- /**
- * Close the surface.
- *
- * <p> After closing, the underlying surface resources can be safely released by
- * {@link SurfaceView} or {@link TextureView} implementation.
- */
- public void close() {
- CallbackToFutureAdapter.Completer<Void> terminationCompleter = null;
- synchronized (mLock) {
- if (!mClosed) {
- mClosed = true;
- terminationCompleter = mTerminationCompleter;
- mTerminationCompleter = null;
- Logger.d(TAG,
- "surface closed, closed=true " + this);
- }
- }
-
- if (terminationCompleter != null) {
- terminationCompleter.set(null);
- }
- }
-
- @NonNull
- protected abstract ListenableFuture<Surface> provideSurfaceAsync();
-
- /**
- * The exception that is returned by the ListenableFuture of {@link #getSurface()} if the
- * deferrable surface is unable to produce a {@link Surface}.
- */
- public static final class SurfaceUnavailableException extends Exception {
- public SurfaceUnavailableException(@NonNull String message) {
- super(message);
- }
- }
-}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java
deleted file mode 100644
index e7c5f26..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java
+++ /dev/null
@@ -1,45 +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.
- */
-
-package androidx.camera.viewfinder.internal.surface;
-
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest;
-
-/**
- * A interface implemented by the application to provide a {@link Surface} for viewfinder.
- *
- * <p> This interface is implemented by the application to provide a {@link Surface}. This
- * will be called by application when it needs a Surface for viewfinder. It also signals when the
- * Surface is no longer in use.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public interface ViewfinderSurfaceProvider {
- /**
- * Called when a new {@link Surface} has been requested by the camera.
- *
- * <p>This is called every time a new surface is required to keep the viewfinder running.
- * The camera may repeatedly request surfaces, but only a single request will be active at a
- * time.
- *
- * @param request the request for a surface which contains the requirements of the
- * surface and methods for completing the request.
- */
- void onSurfaceRequested(@NonNull ViewfinderSurfaceRequest request);
-}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java
deleted file mode 100644
index 30637d8..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java
+++ /dev/null
@@ -1,22 +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.
- */
-
-/**
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-package androidx.camera.viewfinder.internal.surface;
-
-import androidx.annotation.RestrictTo;
diff --git a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
index 2528c8d..bdf5010 100644
--- a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
+++ b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
@@ -16,6 +16,10 @@
package androidx.camera.viewfinder;
+import static androidx.camera.viewfinder.CameraViewfinder.shouldUseTextureView;
+import static androidx.camera.viewfinder.surface.ImplementationMode.COMPATIBLE;
+import static androidx.camera.viewfinder.surface.ImplementationMode.PERFORMANCE;
+
import static com.google.common.truth.Truth.assertThat;
import android.os.Build;
@@ -45,8 +49,7 @@
@Config(minSdk = Build.VERSION_CODES.N_MR1)
public void surfaceViewNormal_useSurfaceView() {
// Assert: SurfaceView is used.
- assertThat(CameraViewfinder.shouldUseTextureView(
- CameraViewfinder.ImplementationMode.PERFORMANCE)).isFalse();
+ assertThat(shouldUseTextureView(PERFORMANCE)).isFalse();
}
@Test
@@ -55,8 +58,7 @@
QuirkInjector.inject(new SurfaceViewStretchedQuirk());
// Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
- assertThat(CameraViewfinder.shouldUseTextureView(
- CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
+ assertThat(shouldUseTextureView(PERFORMANCE)).isTrue();
}
@Test
@@ -65,8 +67,7 @@
QuirkInjector.inject(new SurfaceViewNotCroppedByParentQuirk());
// Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
- assertThat(CameraViewfinder.shouldUseTextureView(
- CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
+ assertThat(shouldUseTextureView(PERFORMANCE)).isTrue();
}
@Test
@@ -75,7 +76,6 @@
QuirkInjector.inject(new SurfaceViewNotCroppedByParentQuirk());
// Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
- assertThat(CameraViewfinder.shouldUseTextureView(
- CameraViewfinder.ImplementationMode.COMPATIBLE)).isTrue();
+ assertThat(shouldUseTextureView(COMPATIBLE)).isTrue();
}
}
diff --git a/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt b/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
index 49cca5a..1f9ba23 100644
--- a/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
+++ b/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
@@ -31,4 +31,9 @@
find_library(egl-lib EGL)
-target_link_libraries(opengl_renderer_jni ${log-lib} ${android-lib} ${opengl-lib} ${egl-lib})
\ No newline at end of file
+target_link_libraries(opengl_renderer_jni ${log-lib} ${android-lib} ${opengl-lib} ${egl-lib})
+target_link_options(
+ opengl_renderer_jni
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index e37be17..c32f705 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -31,7 +31,7 @@
import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
import androidx.camera.integration.extensions.util.takePictureAndWaitForImageSavedIdle
-import androidx.camera.integration.extensions.util.waitForPreviewViewIdle
+import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
import androidx.camera.integration.extensions.utils.CameraSelectorUtil
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -160,7 +160,7 @@
with(activityScenario) {
use {
- waitForPreviewViewIdle()
+ waitForPreviewViewStreaming()
val camera = withActivity { mCamera }
// Retrieves the session processor from the camera's extended config
val sessionProcessor = camera.extendedConfig.sessionProcessor
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index 3627dd5..9b944f1 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -56,11 +56,12 @@
import androidx.appcompat.app.AlertDialog
import androidx.camera.core.impl.utils.CompareSizesByArea
import androidx.camera.viewfinder.CameraViewfinder
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode
import androidx.camera.viewfinder.CameraViewfinder.ScaleType
import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest
import androidx.camera.viewfinder.populateFromCharacteristics
+import androidx.camera.viewfinder.surface.ImplementationMode
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
@@ -171,7 +172,7 @@
when (item.itemId) {
R.id.implementationMode -> {
val implementationMode =
- when (cameraViewfinder.implementationMode) {
+ when (cameraViewfinder.surfaceImplementationMode) {
ImplementationMode.PERFORMANCE ->
ImplementationMode.COMPATIBLE
else -> ImplementationMode.PERFORMANCE
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
index e497bbe..74b0866 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
@@ -233,4 +233,72 @@
assertThat(animation.at(25)).isEqualTo(0.25f)
}
+
+ @Test
+ fun outOfRangeValuesOnly() {
+ val duration = 100
+ val delay = 200
+
+ // Out of range values should be effectively ignored.
+ // It should interpolate within the expected time range without issues
+ val animation = keyframes<Float> {
+ durationMillis = duration
+ delayMillis = delay
+
+ -1f at -delay using LinearEasing
+ -2f at -duration using LinearEasing
+ -3f at (duration + 50) using LinearEasing
+ }.vectorize(Float.VectorConverter)
+
+ // Within delay, should always return initial value unless it was overwritten
+ assertThat(animation.at(0)).isEqualTo(0f)
+ assertThat(animation.at(100)).isEqualTo(0f)
+ assertThat(animation.at(200)).isEqualTo(0f)
+
+ // Within time range
+ assertThat(animation.at(delay)).isEqualTo(0f)
+ assertThat(animation.at((duration / 2) + delay)).isEqualTo(0.5f)
+ assertThat(animation.at(duration + delay)).isEqualTo(1f)
+
+ // Out of range - past animation duration
+ // Should always be the target value unless it was overwritten
+ assertThat(animation.at(delay + duration + 1)).isEqualTo(1f)
+ assertThat(animation.at(delay + duration + 50)).isEqualTo(1f)
+ }
+
+ @Test
+ fun outOfRangeValues_withForcedInitialAndTarget() {
+ val duration = 100
+ val delay = 200
+
+ // Out of range values should be effectively ignored.
+ // It should interpolate within the expected time range without issues
+ val animation = keyframes<Float> {
+ durationMillis = duration
+ delayMillis = delay
+
+ -1f at -delay using LinearEasing
+ -2f at -duration using LinearEasing
+ -3f at (duration + 50) using LinearEasing
+
+ // Force initial and target
+ 4f at 0 using LinearEasing
+ 5f at duration using LinearEasing
+ }.vectorize(Float.VectorConverter)
+
+ // Within delay, should always return initial value unless it was overwritten
+ assertThat(animation.at(0)).isEqualTo(4f)
+ assertThat(animation.at(100)).isEqualTo(4f)
+ assertThat(animation.at(200)).isEqualTo(4f)
+
+ // Within time range
+ assertThat(animation.at(delay)).isEqualTo(4f)
+ assertThat(animation.at((duration / 2) + delay)).isEqualTo(4.5f)
+ assertThat(animation.at(duration + delay)).isEqualTo(5f)
+
+ // Out of range - past animation duration
+ // Should always be the target value unless it was overwritten
+ assertThat(animation.at(delay + duration + 1)).isEqualTo(5f)
+ assertThat(animation.at(delay + duration + 50)).isEqualTo(5f)
+ }
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
index 2b38bbc..a6e01cd 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
@@ -22,7 +22,7 @@
// This function exists so we do *not* inline the throw. It keeps
// the call site much smaller and since it's the slow path anyway,
// we don't mind the extra function call
-internal fun throwIllegalArgumentException(message: String): Nothing {
+internal fun throwIllegalArgumentException(message: String) {
throw IllegalArgumentException(message)
}
@@ -39,7 +39,11 @@
}
// See above
-internal fun throwIllegalStateException(message: String): Nothing {
+internal fun throwIllegalStateException(message: String) {
+ throw IllegalStateException(message)
+}
+
+internal fun throwIllegalStateExceptionForNullCheck(message: String): Nothing {
throw IllegalStateException(message)
}
@@ -64,7 +68,7 @@
}
if (value == null) {
- throwIllegalStateException(lazyMessage())
+ throwIllegalStateExceptionForNullCheck(lazyMessage())
}
return value
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index ddcaa2c..9d09a90 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -417,21 +417,21 @@
val timestampStart = timestamps[index]
val startValue: V = if (keyframes.contains(timestampStart)) {
keyframes[timestampStart]!!.vectorValue
- } else if (index == 0) {
- // Use initial value if it wasn't overwritten by the user
- initialValue
} else {
- throw IllegalStateException("No value to animate from at $clampedPlayTime millis")
+ // Use initial value if it wasn't overwritten by the user
+ // This is always the correct fallback assuming timestamps and keyframes were populated
+ // as expected
+ initialValue
}
val timestampEnd = timestamps[index + 1]
val endValue = if (keyframes.contains(timestampEnd)) {
keyframes[timestampEnd]!!.vectorValue
- } else if (index + 1 == timestamps.size - 1) {
- // Use target value if it wasn't overwritten by the user
- targetValue
} else {
- throw IllegalStateException("No value to animate to at $clampedPlayTime millis")
+ // Use target value if it wasn't overwritten by the user
+ // This is always the correct fallback assuming timestamps and keyframes were populated
+ // as expected
+ targetValue
}
for (i in 0 until valueVector.size) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index a43b12d..3324d7d 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -126,6 +126,12 @@
var animData: AnimData? by mutableStateOf(null)
+ override fun onReset() {
+ super.onReset()
+ // Reset is an indication that the node may be re-used, in such case, animData becomes stale
+ animData = null
+ }
+
override fun onAttach() {
super.onAttach()
// When re-attached, we may be attached to a tree without lookahead scope.
diff --git a/compose/compiler/compiler-hosted/build.gradle b/compose/compiler/compiler-hosted/build.gradle
index dfa7c90..e51c401 100644
--- a/compose/compiler/compiler-hosted/build.gradle
+++ b/compose/compiler/compiler-hosted/build.gradle
@@ -47,6 +47,15 @@
}
}
+afterEvaluate {
+ lint {
+ lintOptions {
+ // Until fully switch to K2, existing FE1.0 usages are legitimate
+ disable("KotlincFE10")
+ }
+ }
+}
+
androidx {
name = "Compose Hosted Compiler Plugin"
// This is only published because that is required when exporting it to g3.
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index db4656d..fcdbff4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -130,6 +130,7 @@
11700 to "1.6.0-alpha08",
11800 to "1.6.0-beta01",
12000 to "1.7.0-alpha01",
+ 12100 to "1.7.0-alpha02",
)
/**
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 022ce06..fba6623 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,6 +1,10 @@
// Baseline format: 1.0
AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
Added method androidx.compose.foundation.pager.PageInfo.getKey()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getOutOfBoundsPageCount():
+ Added method androidx.compose.foundation.pager.PagerLayoutInfo.getOutOfBoundsPageCount()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getSnapPosition():
+ Added method androidx.compose.foundation.pager.PagerLayoutInfo.getSnapPosition()
ChangedType: androidx.compose.foundation.gestures.snapping.SnapFlingBehaviorKt#rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider):
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 512013a..3328399 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -501,7 +501,7 @@
public final class DraggableKt {
method public static androidx.compose.foundation.gestures.DraggableState DraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
- method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.DraggableState rememberDraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
}
@@ -834,6 +834,17 @@
property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
}
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchScope {
+ method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchStrategy {
+ method public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? getPrefetchExecutor();
+ method public void onScroll(androidx.compose.foundation.lazy.LazyListPrefetchScope, float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+ method public void onVisibleItemsUpdated(androidx.compose.foundation.lazy.LazyListPrefetchScope, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+ property public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor;
+ }
+
@androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
@@ -844,6 +855,7 @@
@androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
method public suspend Object? animateScrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
method public int getFirstVisibleItemIndex();
@@ -851,6 +863,7 @@
method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
method public boolean isScrollInProgress();
+ method public void notifyPrefetchOnScroll(float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? scrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public boolean canScrollBackward;
@@ -870,6 +883,7 @@
public final class LazyListStateKt {
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
}
@kotlin.DslMarker public @interface LazyScopeMarker {
@@ -1093,7 +1107,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class LazyLayoutPrefetchState {
- ctor public LazyLayoutPrefetchState();
+ ctor public LazyLayoutPrefetchState(optional androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor);
method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index, long constraints);
}
@@ -1114,6 +1128,19 @@
property public int size;
}
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchExecutor {
+ method public void requestPrefetch(androidx.compose.foundation.lazy.layout.PrefetchExecutor.Request request);
+ }
+
+ public static sealed interface PrefetchExecutor.Request {
+ method public boolean isComposed();
+ method public boolean isValid();
+ method public void performComposition();
+ method public void performMeasure();
+ property public abstract boolean isComposed;
+ property public abstract boolean isValid;
+ }
+
}
package androidx.compose.foundation.lazy.staggeredgrid {
@@ -1261,8 +1288,7 @@
property public final float pageSize;
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class PagerDefaults {
- method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional float snapPositionalThreshold);
+ public final class PagerDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional @FloatRange(from=0.0, to=1.0) float snapPositionalThreshold);
method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.Orientation orientation);
field public static final androidx.compose.foundation.pager.PagerDefaults INSTANCE;
@@ -1270,11 +1296,11 @@
}
public final class PagerKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+ method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+ method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface PagerLayoutInfo {
+ public sealed interface PagerLayoutInfo {
method public int getAfterContentPadding();
method public int getBeforeContentPadding();
method public androidx.compose.foundation.gestures.Orientation getOrientation();
@@ -1313,7 +1339,7 @@
method public androidx.compose.foundation.pager.PagerSnapDistance atMost(int pages);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
+ @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction);
method public final suspend Object? animateScrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
@@ -1330,8 +1356,8 @@
method public boolean isScrollInProgress();
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public final suspend Object? scrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
- method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
property public final boolean canScrollBackward;
property public final boolean canScrollForward;
property public final int currentPage;
@@ -1345,8 +1371,8 @@
}
public final class PagerStateKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+ method public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
}
}
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 022ce06..fba6623 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,6 +1,10 @@
// Baseline format: 1.0
AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
Added method androidx.compose.foundation.pager.PageInfo.getKey()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getOutOfBoundsPageCount():
+ Added method androidx.compose.foundation.pager.PagerLayoutInfo.getOutOfBoundsPageCount()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getSnapPosition():
+ Added method androidx.compose.foundation.pager.PagerLayoutInfo.getSnapPosition()
ChangedType: androidx.compose.foundation.gestures.snapping.SnapFlingBehaviorKt#rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider):
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 99e40d4..e0a9478 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -503,7 +503,7 @@
public final class DraggableKt {
method public static androidx.compose.foundation.gestures.DraggableState DraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
- method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.DraggableState rememberDraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
}
@@ -836,6 +836,17 @@
property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
}
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchScope {
+ method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchStrategy {
+ method public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? getPrefetchExecutor();
+ method public void onScroll(androidx.compose.foundation.lazy.LazyListPrefetchScope, float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+ method public void onVisibleItemsUpdated(androidx.compose.foundation.lazy.LazyListPrefetchScope, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+ property public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor;
+ }
+
@androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
@@ -846,6 +857,7 @@
@androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
method public suspend Object? animateScrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
method public int getFirstVisibleItemIndex();
@@ -853,6 +865,7 @@
method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
method public boolean isScrollInProgress();
+ method public void notifyPrefetchOnScroll(float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? scrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public boolean canScrollBackward;
@@ -872,6 +885,7 @@
public final class LazyListStateKt {
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
}
@kotlin.DslMarker public @interface LazyScopeMarker {
@@ -1095,7 +1109,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class LazyLayoutPrefetchState {
- ctor public LazyLayoutPrefetchState();
+ ctor public LazyLayoutPrefetchState(optional androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor);
method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index, long constraints);
}
@@ -1116,6 +1130,19 @@
property public int size;
}
+ @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchExecutor {
+ method public void requestPrefetch(androidx.compose.foundation.lazy.layout.PrefetchExecutor.Request request);
+ }
+
+ public static sealed interface PrefetchExecutor.Request {
+ method public boolean isComposed();
+ method public boolean isValid();
+ method public void performComposition();
+ method public void performMeasure();
+ property public abstract boolean isComposed;
+ property public abstract boolean isValid;
+ }
+
}
package androidx.compose.foundation.lazy.staggeredgrid {
@@ -1263,8 +1290,7 @@
property public final float pageSize;
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class PagerDefaults {
- method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional float snapPositionalThreshold);
+ public final class PagerDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional @FloatRange(from=0.0, to=1.0) float snapPositionalThreshold);
method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.Orientation orientation);
field public static final androidx.compose.foundation.pager.PagerDefaults INSTANCE;
@@ -1272,11 +1298,11 @@
}
public final class PagerKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+ method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+ method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface PagerLayoutInfo {
+ public sealed interface PagerLayoutInfo {
method public int getAfterContentPadding();
method public int getBeforeContentPadding();
method public androidx.compose.foundation.gestures.Orientation getOrientation();
@@ -1315,7 +1341,7 @@
method public androidx.compose.foundation.pager.PagerSnapDistance atMost(int pages);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
+ @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction);
method public final suspend Object? animateScrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
@@ -1332,8 +1358,8 @@
method public boolean isScrollInProgress();
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public final suspend Object? scrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
- method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
property public final boolean canScrollBackward;
property public final boolean canScrollForward;
property public final int currentPage;
@@ -1347,8 +1373,8 @@
}
public final class PagerStateKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+ method public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
}
}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
index 2cc62ed..8df6bf0 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
@@ -49,15 +49,28 @@
@Test
fun lazyColumn_switchingItems_composition() {
- benchmarkRule.runBenchmark(composition = true)
+ benchmarkRule.runBenchmark(composition = true, switchingStateCount = NUMBER_OF_LAZY_ITEMS)
}
@Test
fun lazyColumn_switchingItems_measure() {
- benchmarkRule.runBenchmark(composition = false)
+ benchmarkRule.runBenchmark(composition = false, switchingStateCount = NUMBER_OF_LAZY_ITEMS)
}
- private fun ComposeBenchmarkRule.runBenchmark(composition: Boolean) {
+ @Test
+ fun lazyColumn_switchingItems_composition_one_state() {
+ benchmarkRule.runBenchmark(composition = true, switchingStateCount = 1)
+ }
+
+ @Test
+ fun lazyColumn_switchingItems_measure_one_state() {
+ benchmarkRule.runBenchmark(composition = false, switchingStateCount = 1)
+ }
+
+ private fun ComposeBenchmarkRule.runBenchmark(
+ composition: Boolean,
+ switchingStateCount: Int
+ ) {
runBenchmarkFor(
{ LazyColumnSwitchingItemsCase(readInComposition = composition) },
) {
@@ -69,14 +82,14 @@
measureRepeatedOnUiThread {
runWithTimingDisabled {
assertNoPendingChanges()
- repeat(getTestCase().items.size) {
+ repeat(switchingStateCount) {
getTestCase().toggle(it)
}
doFramesUntilIdle()
assertNoPendingChanges()
}
- repeat(getTestCase().items.size) {
+ repeat(switchingStateCount) {
getTestCase().toggle(it)
}
doFramesUntilIdle()
@@ -85,11 +98,12 @@
}
}
+// The number is based on height of items below (20 visible + 5 extra).
+private const val NUMBER_OF_LAZY_ITEMS = 25
class LazyColumnSwitchingItemsCase(
private val readInComposition: Boolean = false
) : ComposeTestCase {
- // The number is based on height of items below (20 visible + 5 extra).
- val items = List(25) {
+ val items = List(NUMBER_OF_LAZY_ITEMS) {
mutableStateOf(false)
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
index ff44780..35eb15d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
@@ -26,6 +26,7 @@
implementation("androidx.core:core:1.12.0")
implementation(libs.kotlinStdlib)
+ implementation(project(":activity:activity-compose"))
implementation(project(":compose:animation:animation"))
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
index a8bfcbd..828d57d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
@@ -159,7 +159,9 @@
val inlineTextContent = InlineTextContent(
placeholder = Placeholder(fontSize, fontSize, PlaceholderVerticalAlign.Center)
) {
- Box(modifier = Modifier.fillMaxSize().background(Color.Green))
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Green))
}
Text(buildAnnotatedString {
append("A ")
@@ -216,6 +218,29 @@
}
)
}
+ Sample("RTL text") {
+ val text = buildAnnotatedString {
+ withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+ append(loremIpsum(Language.Arabic, 2))
+ }
+ append(loremIpsum(Language.Arabic, 5))
+ withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+ append(loremIpsum(Language.Arabic, 3))
+ }
+ append(loremIpsum(Language.Arabic, 5))
+ }
+ Text(text)
+ }
+ Sample("Bidi text") {
+ val text = buildAnnotatedString {
+ append(loremIpsum(Language.Arabic, 2))
+ withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+ append(" developer.android.com ")
+ }
+ append(loremIpsum(Language.Arabic, 5))
+ }
+ Text(text)
+ }
}
}
@@ -225,7 +250,8 @@
Modifier
.fillMaxWidth()
.border(2.dp, Color.Black)
- .padding(8.dp)) {
+ .padding(8.dp)
+ ) {
Text(title, Modifier.align(Alignment.CenterHorizontally), fontWeight = FontWeight.Bold)
content()
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
index 4efffb4..511924a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
@@ -1007,7 +1007,6 @@
assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
"orientation",
"enabled",
- "canDrag",
"reverseDirection",
"interactionSource",
"startDragImmediately",
@@ -1018,6 +1017,18 @@
}
}
+ @Test
+ fun equalInputs_shouldResolveToEquals() {
+ val state = DraggableState { }
+
+ val firstModifier = Modifier.draggable(state, Orientation.Horizontal)
+ val secondModifier = Modifier.draggable(state, Orientation.Vertical)
+ val thirdModifier = Modifier.draggable(state, Orientation.Horizontal)
+
+ assertThat(firstModifier).isEqualTo(thirdModifier)
+ assertThat(firstModifier).isNotEqualTo(secondModifier)
+ }
+
private fun setDraggableContent(draggableFactory: @Composable () -> Modifier) {
rule.setContent {
Box {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
index bb04b09..e7721c3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.content
import android.content.ClipData
-import android.content.ClipDescription
import android.net.Uri
import android.view.DragEvent
import android.view.View
@@ -26,48 +25,85 @@
import androidx.compose.ui.unit.Density
/**
- * A helper scope creator to test multi-window Drag And Drop interactions.
+ * A helper to test multi-window Drag And Drop interactions.
*/
internal fun testDragAndDrop(view: View, density: Density, block: DragAndDropScope.() -> Unit) {
DragAndDropScopeImpl(view, density).block()
}
+internal interface DragAndDropScope : Density {
+
+ /**
+ * Drags an item with ClipData that only holds the given [text] to the [offset] location.
+ */
+ fun drag(offset: Offset, text: String): Boolean
+
+ /**
+ * Drags an item with ClipData that only holds the given [uri] to the [offset] location.
+ */
+ fun drag(offset: Offset, uri: Uri): Boolean
+
+ /**
+ * Drags an item with [clipData] payload to the [offset] location.
+ */
+ fun drag(offset: Offset, clipData: ClipData): Boolean
+
+ /**
+ * Drops the previously declared dragging item.
+ */
+ fun drop(): Boolean
+
+ /**
+ * Cancels the ongoing drag without dropping it.
+ */
+ fun cancelDrag()
+}
+
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
private class DragAndDropScopeImpl(
val view: View,
density: Density
) : DragAndDropScope, Density by density {
- private var lastDraggingItem: Pair<Offset, Any>? = null
+ private var lastDraggingOffsetAndItem: Pair<Offset, Any>? = null
- override fun drag(
- offset: Offset,
- item: Any,
- ) {
- val _lastDraggingItem = lastDraggingItem
+ override fun drag(offset: Offset, text: String): Boolean = dragAny(offset, text)
+
+ override fun drag(offset: Offset, uri: Uri): Boolean = dragAny(offset, uri)
+
+ override fun drag(offset: Offset, clipData: ClipData): Boolean = dragAny(offset, clipData)
+
+ /**
+ * @param item Can be only [String], [Uri], or [ClipData].
+ */
+ private fun dragAny(offset: Offset, item: Any): Boolean {
+ val _lastDraggingItem = lastDraggingOffsetAndItem
+ var result = false
if (_lastDraggingItem == null || _lastDraggingItem.second != item) {
- view.dispatchDragEvent(
- makeDragEvent(DragEvent.ACTION_DRAG_STARTED, item)
- )
+ result = view.dispatchDragEvent(
+ makeDragEvent(action = DragEvent.ACTION_DRAG_STARTED, item = item)
+ ) || result
}
- view.dispatchDragEvent(
+ result = view.dispatchDragEvent(
makeDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
+ action = DragEvent.ACTION_DRAG_LOCATION,
item = item,
offset = offset
)
- )
- lastDraggingItem = offset to item
+ ) || result
+ lastDraggingOffsetAndItem = offset to item
+ return result
}
- override fun drop() {
- val _lastDraggingItem = lastDraggingItem
- check(_lastDraggingItem != null) { "There are no ongoing dragging event to drop" }
+ override fun drop(): Boolean {
+ val lastDraggingOffsetAndItem = lastDraggingOffsetAndItem
+ check(lastDraggingOffsetAndItem != null) { "There are no ongoing dragging event to drop" }
+ val (lastDraggingOffset, lastDraggingItem) = lastDraggingOffsetAndItem
- view.dispatchDragEvent(
+ return view.dispatchDragEvent(
makeDragEvent(
DragEvent.ACTION_DROP,
- item = _lastDraggingItem.second,
- offset = _lastDraggingItem.first
+ item = lastDraggingItem,
+ offset = lastDraggingOffset
)
)
}
@@ -92,54 +128,15 @@
DragAndDropTestUtils.makeImageDragEvent(action, item, offset)
}
- is List<*> -> {
- val mimeTypes = mutableSetOf<String>()
- val clipDataItems = mutableListOf<ClipData.Item>()
- item.filterNotNull().forEach { actualItem ->
- when (actualItem) {
- is String -> {
- mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
- clipDataItems.add(ClipData.Item(actualItem))
- }
-
- is Uri -> {
- mimeTypes.add("image/*")
- clipDataItems.add(ClipData.Item(actualItem))
- }
- }
- }
- DragAndDropTestUtils.makeDragEvent(
- action = action,
- items = clipDataItems,
- mimeTypes = mimeTypes.toList(),
- offset = offset
- )
+ is ClipData -> {
+ DragAndDropTestUtils.makeDragEvent(action, item, offset)
}
else -> {
- DragAndDropTestUtils.makeImageDragEvent(action, offset = offset)
+ throw IllegalArgumentException(
+ "{item=$item} can only be one of [String], [Uri], or [ClipData]"
+ )
}
}
}
}
-
-internal interface DragAndDropScope : Density {
-
- /**
- * Drags an item which represent the payload to the [offset] location.
- *
- * @param item Should either be a [String] or a [Uri]. It can also be a [List] of [String]s or
- * [Uri]s.
- */
- fun drag(offset: Offset, item: Any)
-
- /**
- * Drops the previously declared dragging item.
- */
- fun drop()
-
- /**
- * Cancels the ongoing drag without dropping it.
- */
- fun cancelDrag()
-}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index fed28d6..cc9b072 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -326,6 +326,40 @@
}
@Test
+ fun prefetchAndCancelItemWithCustomExecutor() {
+ val itemProvider = itemProvider({ 1 }) { index ->
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
+ }
+
+ val executor = RecordingPrefetchExecutor()
+ val prefetchState = LazyLayoutPrefetchState(executor)
+ rule.setContent {
+ LazyLayout(itemProvider, prefetchState = prefetchState) {
+ layout(100, 100) {}
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
+ }
+
+ assertThat(executor.requests).hasSize(1)
+ assertThat(executor.requests[0].isValid).isTrue()
+
+ // Default PrefetchExecutor behavior should be overridden
+ rule.onNodeWithTag("0").assertDoesNotExist()
+
+ rule.runOnIdle {
+ handle.cancel()
+ }
+
+ assertThat(executor.requests[0].isValid).isFalse()
+ }
+
+ @Test
fun keptForReuseItemIsDisposedWhenCanceled() {
val needChild = mutableStateOf(true)
var composed = true
@@ -546,4 +580,14 @@
}
return { provider }
}
+
+ private class RecordingPrefetchExecutor : PrefetchExecutor {
+
+ private val _requests: MutableList<PrefetchExecutor.Request> = mutableListOf()
+ val requests: List<PrefetchExecutor.Request> = _requests
+
+ override fun requestPrefetch(request: PrefetchExecutor.Request) {
+ _requests.add(request)
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
new file mode 100644
index 0000000..94f0250
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
@@ -0,0 +1,304 @@
+/*
+ * 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.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.DefaultLazyListPrefetchStrategy
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
+import androidx.compose.foundation.lazy.LazyListPrefetchScope
+import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.list.LazyListPrefetchStrategyTest.RecordingLazyListPrefetchStrategy.Callback
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyListPrefetchStrategyTest(
+ val config: Config
+) : BaseLazyListTestWithOrientation(config.orientation) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ Config(Orientation.Vertical),
+ Config(Orientation.Horizontal),
+ )
+
+ class Config(
+ val orientation: Orientation,
+ ) {
+ override fun toString() = "orientation=$orientation"
+ }
+
+ private val LazyListLayoutInfo.visibleIndices: List<Int>
+ get() = visibleItemsInfo.map { it.index }.sorted()
+ }
+
+ private val itemsSizePx = 30
+ private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+ lateinit var state: LazyListState
+
+ @Test
+ fun callbacksTriggered_whenScrollForwardsWithoutVisibleItemsChanged() {
+ val strategy = RecordingLazyListPrefetchStrategy()
+
+ composeList(prefetchStrategy = strategy)
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(0, 1)
+ ),
+ ).inOrder()
+ strategy.reset()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnScroll(
+ delta = -5f,
+ visibleIndices = listOf(0, 1)
+ ),
+ ).inOrder()
+ }
+
+ @Test
+ fun callbacksTriggered_whenScrollBackwardsWithoutVisibleItemsChanged() {
+ val strategy = RecordingLazyListPrefetchStrategy()
+
+ composeList(firstItem = 10, itemOffset = 10, prefetchStrategy = strategy)
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(10, 11)
+ ),
+ ).inOrder()
+ strategy.reset()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-5f)
+ }
+ }
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnScroll(
+ delta = 5f,
+ visibleIndices = listOf(10, 11)
+ ),
+ ).inOrder()
+ }
+
+ @Test
+ fun callbacksTriggered_whenScrollWithVisibleItemsChanged() {
+ val strategy = RecordingLazyListPrefetchStrategy()
+
+ composeList(prefetchStrategy = strategy)
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(0, 1)
+ ),
+ ).inOrder()
+ strategy.reset()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemsSizePx + 5f)
+ }
+ }
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(1, 2)
+ ),
+ Callback.OnScroll(
+ delta = -(itemsSizePx + 5f),
+ visibleIndices = listOf(1, 2)
+ ),
+ ).inOrder()
+ }
+
+ @Test
+ fun callbacksTriggered_whenItemsChangedWithoutScroll() {
+ val strategy = RecordingLazyListPrefetchStrategy()
+ val numItems = mutableStateOf(100)
+
+ composeList(prefetchStrategy = strategy, numItems = numItems)
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(0, 1)
+ ),
+ ).inOrder()
+ strategy.reset()
+
+ numItems.value = 1
+
+ rule.waitForIdle()
+
+ assertThat(strategy.callbacks).containsExactly(
+ Callback.OnVisibleItemsUpdated(
+ visibleIndices = listOf(0)
+ ),
+ ).inOrder()
+ }
+
+ @Test
+ fun itemComposed_whenPrefetchedFromCallback() {
+ val strategy = PrefetchNextLargestIndexStrategy()
+
+ composeList(prefetchStrategy = strategy)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(2)
+ rule.onNodeWithTag("2")
+ .assertExists()
+ }
+
+ private fun waitForPrefetch(index: Int) {
+ rule.waitUntil {
+ activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ }
+ }
+
+ private val activeNodes = mutableSetOf<Int>()
+ private val activeMeasuredNodes = mutableSetOf<Int>()
+
+ @OptIn(ExperimentalFoundationApi::class)
+ private fun composeList(
+ firstItem: Int = 0,
+ itemOffset: Int = 0,
+ numItems: MutableState<Int> = mutableStateOf(100),
+ prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy()
+ ) {
+ rule.setContent {
+ state = rememberLazyListState(
+ initialFirstVisibleItemIndex = firstItem,
+ initialFirstVisibleItemScrollOffset = itemOffset,
+ prefetchStrategy = prefetchStrategy
+ )
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+ state,
+ ) {
+ items(numItems.value) {
+ DisposableEffect(it) {
+ activeNodes.add(it)
+ onDispose {
+ activeNodes.remove(it)
+ activeMeasuredNodes.remove(it)
+ }
+ }
+ Spacer(
+ Modifier
+ .mainAxisSize(itemsSizeDp)
+ .fillMaxCrossAxis()
+ .testTag("$it")
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ activeMeasuredNodes.add(it)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * LazyListPrefetchStrategy that just records callbacks without scheduling prefetches.
+ */
+ private class RecordingLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+ sealed interface Callback {
+ data class OnScroll(val delta: Float, val visibleIndices: List<Int>) : Callback
+ data class OnVisibleItemsUpdated(val visibleIndices: List<Int>) : Callback
+ }
+
+ private val _callbacks: MutableList<Callback> = mutableListOf()
+ val callbacks: List<Callback> = _callbacks
+
+ override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+ _callbacks.add(Callback.OnScroll(delta, layoutInfo.visibleIndices))
+ }
+
+ override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
+ _callbacks.add(Callback.OnVisibleItemsUpdated(layoutInfo.visibleIndices))
+ }
+
+ fun reset() {
+ _callbacks.clear()
+ }
+ }
+
+ /**
+ * LazyListPrefetchStrategy that always prefetches the next largest index off screen no matter
+ * the scroll direction.
+ */
+ private class PrefetchNextLargestIndexStrategy : LazyListPrefetchStrategy {
+
+ private var handle: LazyLayoutPrefetchState.PrefetchHandle? = null
+ private var prefetchIndex: Int = -1
+
+ override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+ val index = layoutInfo.visibleIndices.last() + 1
+ if (handle != null && index != prefetchIndex) {
+ cancelPrefetch()
+ }
+ handle = schedulePrefetch(index)
+ prefetchIndex = index
+ }
+
+ override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) =
+ Unit
+
+ private fun cancelPrefetch() {
+ handle?.cancel()
+ prefetchIndex = -1
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index df9f74f..da3f43f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -41,6 +41,7 @@
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@@ -2704,6 +2705,111 @@
}
}
+ @Test
+ fun animContentSize_resetOnReuse() = with(rule.density) {
+ val visBoxCount = 10
+ val maxItemCount = 10 * visBoxCount
+ val boxHeightPx = 100
+ val smallBoxPx = 100
+ val mediumBoxPx = 200
+ val largeBoxPx = 300
+ val maxHeightPx = visBoxCount * boxHeightPx
+
+ val mediumItemsStartIndex = 50
+
+ var assertedSmallItems = false
+ var assertedMediumItems = false
+
+ /**
+ * Since only the first item is expected to animate. This Modifier can check that all other
+ * items were only measure at their expected dimensions.
+ */
+ fun Modifier.assertMeasureCalls(index: Int): Modifier {
+ return this.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ when {
+ index == 0 -> {
+ // Do nothing
+ }
+
+ index >= mediumItemsStartIndex -> {
+ assertedMediumItems = true
+ assertEquals(mediumBoxPx, placeable.width)
+ assertEquals(boxHeightPx, placeable.height)
+ }
+
+ else -> {
+ assertedSmallItems = true
+ assertEquals(smallBoxPx, placeable.width)
+ assertEquals(boxHeightPx, placeable.height)
+ }
+ }
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ val isFirstElementExpanded = mutableStateOf(false)
+
+ val listState = LazyListState()
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.height(maxHeightPx.toDp()),
+ state = listState
+ ) {
+ items(maxItemCount) { index ->
+ val modifier = when {
+ index == 0 -> {
+ val sizeDp = if (isFirstElementExpanded.value) {
+ largeBoxPx.toDp()
+ } else {
+ smallBoxPx.toDp()
+ }
+ Modifier.requiredSize(sizeDp)
+ }
+
+ index >= mediumItemsStartIndex -> {
+ Modifier
+ .requiredSize(mediumBoxPx.toDp(), boxHeightPx.toDp())
+ }
+
+ else -> {
+ Modifier
+ .requiredSize(smallBoxPx.toDp(), boxHeightPx.toDp())
+ }
+ }
+ Box(
+ Modifier
+ .wrapContentSize()
+ .assertMeasureCalls(index)
+ .animateContentSize()
+ ) {
+ Box(modifier)
+ }
+ }
+ }
+ }
+
+ // Wait for layout to settle
+ rule.waitForIdle()
+
+ // Trigger an animation on the first element and wait for completion
+ isFirstElementExpanded.value = true
+ rule.waitForIdle()
+
+ // Scroll to a different index and wait for layout to settle.
+ // At the selected index, ALL layout nodes will have a different size. If
+ // `animateContentSize` wasn't properly reset, it might have started more animations, caught
+ // with `assertMeasureCalls`.
+ listState.scrollTo(mediumItemsStartIndex)
+ rule.waitForIdle()
+
+ // Safety check to prevent silent failures (no UI was run)
+ assert(assertedMediumItems)
+ assert(assertedSmallItems)
+ }
+
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
@Test
fun animVisibilityWithPlacementAnimator() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index 1cf1414..a376481 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsPlaced
import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.DisposableEffect
@@ -50,6 +51,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertTrue
@@ -2170,4 +2172,102 @@
assertThat(composedItems).isEqualTo(setOf(0, 1, 2, 3))
}
}
+
+ @Test
+ fun zeroSizeItemIsPlacedWhenItIsAtTheTop() {
+ lateinit var state: LazyStaggeredGridState
+
+ rule.setContent {
+ state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+ LazyStaggeredGrid(
+ lanes = 2,
+ state = state,
+ modifier = Modifier
+ .mainAxisSize(itemSizeDp * 2)
+ .crossAxisSize(itemSizeDp * 2)
+ ) {
+ repeat(10) { index ->
+ items(2) {
+ Spacer(Modifier.testTag("${index * 10 + it}"))
+ }
+ items(8) {
+ Spacer(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(10, 0)
+ }
+
+ rule.onNodeWithTag("10")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("11")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun itemsAreDistributedCorrectlyOnOverscrollPassWithSameOffset() {
+ val gridHeight = itemSizeDp * 11 // two big items + two small items
+ state = LazyStaggeredGridState()
+ rule.setContent {
+ LazyStaggeredGrid(
+ modifier = Modifier
+ .mainAxisSize(gridHeight)
+ .crossAxisSize(itemSizeDp * 2),
+ state = state,
+ lanes = 2,
+ ) {
+ items(20) {
+ Spacer(
+ Modifier
+ .mainAxisSize(if (it % 2 == 0) itemSizeDp * 5 else itemSizeDp * 0.5f)
+ .border(1.dp, Color.Red)
+ .testTag("$it")
+ )
+ }
+ }
+ }
+
+ // scroll to bottom
+ state.scrollBy(gridHeight * 2)
+
+ rule.onNodeWithTag("12")
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("13")
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+ // scroll a back a bit
+ state.scrollBy(-itemSizeDp * 5)
+
+ // scroll by a grid height
+ state.scrollBy(gridHeight)
+
+ rule.onNodeWithTag("12")
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("13")
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
index f22a59c..a7af694 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
@@ -46,6 +46,8 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsFocused
@@ -115,7 +117,7 @@
focusManager.moveFocus(FocusDirection.Previous)
}
- rule.onAllNodes(hasClickAction())[2].assertIsFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsFocused()
}
@Test
@@ -128,8 +130,8 @@
focusManager.moveFocus(FocusDirection.Previous)
}
- rule.onAllNodes(hasClickAction())[2].assertIsNotFocused()
- rule.onAllNodes(hasClickAction())[1].assertIsFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsNotFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[1].assertIsFocused()
}
@Test
@@ -143,9 +145,9 @@
focusManager.moveFocus(FocusDirection.Previous)
}
- rule.onAllNodes(hasClickAction())[2].assertIsNotFocused()
- rule.onAllNodes(hasClickAction())[1].assertIsNotFocused()
- rule.onAllNodes(hasClickAction())[0].assertIsFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsNotFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[1].assertIsNotFocused()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[0].assertIsFocused()
}
@Test
@@ -222,6 +224,96 @@
}
@Test
+ fun rtlText_onClick_insideFirstLink_opensFirstUrl() {
+ setupContent { RtlTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(3)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(Url1)
+ }
+ }
+
+ @Test
+ fun rtlText_onClick_insideSecondLink_opensSecondUrl() {
+ setupContent { RtlTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(30)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(Url2)
+ }
+ }
+
+ @Test
+ fun rtlText_onClick_outsideLink_doNothing() {
+ setupContent { RtlTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(35)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(null)
+ }
+ }
+
+ @Test
+ fun rtlText_onClick_inBetweenLinks_doNothing() {
+ setupContent { RtlTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(20)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(null)
+ }
+ }
+
+ @Test
+ fun bidiText_onClick_insideLink_opensUrl() {
+ setupContent { BidiTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(8)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(Url1)
+ }
+ }
+
+ @Test
+ fun bidiText_onClick_outsideLink_doNothing() {
+ setupContent { BidiTextWithLinks() }
+
+ rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+ rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+ val boundingBox = layoutResult!!.getBoundingBox(2)
+ click(boundingBox.center)
+ }
+
+ rule.runOnIdle {
+ assertThat(openedUri).isEqualTo(null)
+ }
+ }
+
+ @Test
fun link_andInlineContent_onClick_opensUrl() {
setupContent {
/***
@@ -254,9 +346,9 @@
)
}
- rule.onAllNodes(hasClickAction())[0].performClick()
+ rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[0].performClick()
- rule.onNodeWithTag("box").assertExists()
+ rule.onNodeWithTag("box", useUnmergedTree = true).assertExists()
rule.runOnIdle {
assertThat(openedUri).isEqualTo(Url1)
}
@@ -327,7 +419,7 @@
}
rule.onNodeWithTag("box").assertIsNotFocused()
- rule.onNode(hasClickAction()).assertIsFocused()
+ rule.onNode(hasClickAction(), useUnmergedTree = true).assertIsFocused()
}
@Test
@@ -440,6 +532,29 @@
}
}
+ @Composable
+ private fun BidiTextWithLinks() {
+ val text = buildAnnotatedString {
+ append("\u05D0\u05D1 \u05D2\u05D3")
+ withAnnotation(Url(Url1)) { append(" text ") }
+ append("\u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1 \u05D2\u05D3")
+ }
+ BasicText(text, onTextLayout = { layoutResult = it })
+ }
+
+ @Composable
+ private fun RtlTextWithLinks() {
+ val text = buildAnnotatedString {
+ withAnnotation(Url(Url1)) { append("\u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1") }
+ append(" \u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1 \u05D0\u05D1 \u05D2\u05D3")
+ withAnnotation(Url(Url2)) {
+ append(" \u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1")
+ }
+ append("\u05D0\u05D1 \u05D2\u05D3")
+ }
+ BasicText(text, onTextLayout = { layoutResult = it })
+ }
+
@OptIn(ExperimentalComposeUiApi::class)
private fun setupContent(content: @Composable () -> Unit) {
val keyboardMockManager = object : InputModeManager {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
index 7765353..82097da 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
@@ -16,14 +16,25 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withAnnotation
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,4 +67,41 @@
text = "after"
rule.onNodeWithText("after").assertExists()
}
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Test
+ fun link_semantics_AnnotatedString() {
+ rule.setContent {
+ BasicText(
+ text = buildAnnotatedString {
+ withAnnotation(LinkAnnotation.Url("url")) { append("abc") }
+ withAnnotation(LinkAnnotation.Clickable("tag")) { append("def") }
+ },
+ onLinkClicked = {}
+ )
+ }
+
+ val node = rule
+ .onNodeWithText("abcdef", useUnmergedTree = true)
+ .assertExists()
+ .fetchSemanticsNode()
+ assertThat(node.children.size).isEqualTo(2)
+ assertThat(node.config.isClearingSemantics).isTrue()
+
+ rule.onNodeWithText("abcdef", useUnmergedTree = true)
+ .onChildAt(0)
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CustomActions))
+ .assert(SemanticsMatcher.expectValue(
+ SemanticsProperties.TextSelectionRange,
+ TextRange(0, 3)
+ ))
+
+ rule.onNodeWithText("abcdef", useUnmergedTree = true)
+ .onChildAt(1)
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CustomActions))
+ .assert(SemanticsMatcher.expectValue(
+ SemanticsProperties.TextSelectionRange,
+ TextRange(3, 6)
+ ))
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
index 63c6bc8..aa22933 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
@@ -220,7 +220,9 @@
}
private fun expectInlineContentPosition(left: Int, right: Int) {
- val (boxLeft, boxRight) = with(rule.onNodeWithTag("box").fetchSemanticsNode()) {
+ val (boxLeft, boxRight) = with(
+ rule.onNodeWithTag("box", useUnmergedTree = true).fetchSemanticsNode()
+ ) {
Pair(positionInRoot.x, positionInRoot.x + size.width)
}
val (textLeft, textRight) = with(rule.onNodeWithTag("text").fetchSemanticsNode()) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
index f103123..a6fff29 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
@@ -34,6 +34,7 @@
import androidx.compose.foundation.text.selection.fetchTextLayoutResult
import androidx.compose.foundation.text2.BasicTextField2
import androidx.compose.foundation.text2.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -106,10 +107,16 @@
internal class BasicTextField2Test {
@get:Rule
val rule = createComposeRule()
+
+ @get:Rule
+ val immRule = ComposeInputMethodManagerTestRule()
+
private val inputMethodInterceptor = InputMethodInterceptor(rule)
private val Tag = "BasicTextField2"
+ private val imm = FakeInputMethodManager()
+
@Test
fun textField_rendersEmptyContent() {
var textLayoutResult: (() -> TextLayoutResult?)? = null
@@ -453,7 +460,10 @@
focusManager = LocalFocusManager.current
Row {
// Extra focusable that takes initial focus when focus is cleared.
- Box(Modifier.size(10.dp).focusable())
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusable())
BasicTextField2(
state = state,
modifier = Modifier.testTag("TextField")
@@ -1301,6 +1311,103 @@
assertThat(tfs.text.selectionInChars).isEqualTo(TextRange(longText.length))
}
+ @Test
+ fun selectAll_contextMenuAction_informsImeOfSelectionChange() {
+ immRule.setFactory { imm }
+ val state = TextFieldState("Hello")
+ inputMethodInterceptor.setTextFieldTestContent {
+ BasicTextField2(
+ state = state,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ requestFocus(Tag)
+
+ inputMethodInterceptor.withInputConnection {
+ performContextMenuAction(android.R.id.selectAll)
+ }
+
+ rule.runOnIdle {
+ assertThat(state.text.selectionInChars).isEqualTo(TextRange(0, 5))
+ assertThat(imm.expectCall("updateSelection(0, 5, -1, -1)"))
+ }
+ }
+
+ @Test
+ fun cut_contextMenuAction_cutsIntoClipboard() {
+ val clipboardManager = FakeClipboardManager("World")
+ val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 2))
+ inputMethodInterceptor.setTextFieldTestContent {
+ CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+ BasicTextField2(
+ state = state,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+ }
+
+ requestFocus(Tag)
+
+ inputMethodInterceptor.withInputConnection {
+ performContextMenuAction(android.R.id.cut)
+ }
+
+ rule.runOnIdle {
+ assertThat(clipboardManager.getText()?.text).isEqualTo("He")
+ assertThat(state.text.toString()).isEqualTo("llo")
+ }
+ }
+
+ @Test
+ fun copy_contextMenuAction_copiesIntoClipboard() {
+ val clipboardManager = FakeClipboardManager("World")
+ val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 2))
+ inputMethodInterceptor.setTextFieldTestContent {
+ CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+ BasicTextField2(
+ state = state,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+ }
+
+ requestFocus(Tag)
+
+ inputMethodInterceptor.withInputConnection {
+ performContextMenuAction(android.R.id.copy)
+ }
+
+ rule.runOnIdle {
+ assertThat(clipboardManager.getText()?.text).isEqualTo("He")
+ }
+ }
+
+ @Test
+ fun paste_contextMenuAction_pastesFromClipboard() {
+ val clipboardManager = FakeClipboardManager("World")
+ val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 4))
+ inputMethodInterceptor.setTextFieldTestContent {
+ CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+ BasicTextField2(
+ state = state,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+ }
+
+ requestFocus(Tag)
+
+ inputMethodInterceptor.withInputConnection {
+ performContextMenuAction(android.R.id.paste)
+ }
+
+ rule.runOnIdle {
+ assertThat(state.text.toString()).isEqualTo("Worldo")
+ assertThat(state.text.selectionInChars).isEqualTo(TextRange(5))
+ }
+ }
+
private fun requestFocus(tag: String) =
rule.onNodeWithTag(tag).requestFocus()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
index 7c98465..8ca4a6a1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.test.withKeyDown
import androidx.compose.ui.text.TextRange
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -351,6 +352,7 @@
)
}
+ @FlakyTest(bugId = 317749301)
@Test
fun multipleCodepoints_selectionIsMappedAroundCodepoints() {
val state = TextFieldState("a${SingleSurrogateCodepointString}c")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
index 7ac722a..db12a68 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
@@ -873,8 +873,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
)
view?.dispatchDragEvent(startEvent)
@@ -912,8 +912,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
)
view?.dispatchDragEvent(startEvent)
@@ -957,8 +957,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
)
view?.dispatchDragEvent(startEvent)
@@ -973,8 +973,8 @@
.assertCursor(cursorTopCenterInLtr)
val moveEvent2 = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(with(rule.density) { fontSize.toPx() * 4 }, 5f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(with(rule.density) { fontSize.toPx() * 4 }, 5f)
)
view?.dispatchDragEvent(moveEvent2)
rule.mainClock.advanceTimeBy(400)
@@ -1009,8 +1009,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
)
view?.dispatchDragEvent(startEvent)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
index c32aa761..28276f2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.DragAndDropScope
+import androidx.compose.foundation.content.createClipData
import androidx.compose.foundation.content.testDragAndDrop
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -184,7 +185,10 @@
rule.setContentAndTestDragAndDrop("aaaa") {
drag(
Offset(fontSize.toPx() * 2, 10f),
- listOf("Hello", "World")
+ createClipData {
+ addText("Hello")
+ addText("World")
+ }
)
drop()
assertThat(state.text.toString()).isEqualTo("aaHello\nWorldaa")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
index af4f35f..c5494a8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
@@ -17,11 +17,11 @@
package androidx.compose.foundation.text2.input.internal
import android.content.ClipData
-import android.content.ClipDescription
import android.net.Uri
import android.os.Build
import android.os.Parcel
import android.view.DragEvent
+import androidx.compose.foundation.content.createClipData
import androidx.compose.ui.geometry.Offset
/**
@@ -31,35 +31,22 @@
* Also it does not mock but uses Parcel to create a DragEvent.
*/
object DragAndDropTestUtils {
- private const val LABEL = "Label"
- const val SAMPLE_TEXT = "Drag Text"
- val SAMPLE_URI = Uri.parse("http://www.google.com")
+ private const val SAMPLE_TEXT = "Drag Text"
+ private val SAMPLE_URI = Uri.parse("http://www.google.com")
/**
* Makes a stub drag event containing fake text data.
*
* @param action One of the [DragEvent] actions.
*/
- fun makeTextDragEvent(action: Int, offset: Offset = Offset.Zero): DragEvent {
+ fun makeTextDragEvent(
+ action: Int,
+ text: String = SAMPLE_TEXT,
+ offset: Offset = Offset.Zero,
+ ): DragEvent {
return makeDragEvent(
action = action,
- items = listOf(ClipData.Item(SAMPLE_TEXT)),
- mimeTypes = listOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
- offset = offset
- )
- }
-
- /**
- * Makes a stub drag event containing text data.
- *
- * @param action One of the [DragEvent] actions.
- * @param text The text being dragged.
- */
- fun makeTextDragEvent(action: Int, text: String?, offset: Offset = Offset.Zero): DragEvent {
- return makeDragEvent(
- action = action,
- items = listOf(ClipData.Item(text)),
- mimeTypes = listOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
+ clipData = createClipData { addText(text) },
offset = offset
)
}
@@ -72,29 +59,23 @@
fun makeImageDragEvent(
action: Int,
item: Uri = SAMPLE_URI,
- offset: Offset = Offset.Zero
+ offset: Offset = Offset.Zero,
): DragEvent {
- // We're not actually resolving Uris in these tests, so this can be anything:
- val mimeType = "image/*"
return makeDragEvent(
action = action,
- items = listOf(ClipData.Item(item)),
- mimeTypes = listOf(mimeType),
+ clipData = createClipData {
+ // We're not actually resolving Uris in these tests, so this can be anything:
+ addUri(item, mimeType = "image/png")
+ },
offset = offset
)
}
fun makeDragEvent(
action: Int,
- items: List<ClipData.Item>,
- mimeTypes: List<String>,
+ clipData: ClipData,
offset: Offset = Offset.Zero
): DragEvent {
- val clipDescription = ClipDescription(LABEL, mimeTypes.toTypedArray())
- val clipData = ClipData(clipDescription, items.first()).apply {
- items.drop(1).forEach { addItem(it) }
- }
-
val parcel = Parcel.obtain()
parcel.writeInt(action)
@@ -109,7 +90,7 @@
parcel.writeInt(1)
clipData.writeToParcel(parcel, 0)
parcel.writeInt(1)
- clipDescription.writeToParcel(parcel, 0)
+ clipData.description.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
return DragEvent.CREATOR.createFromParcel(parcel)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
index b10aab3..1653ef2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
@@ -62,8 +62,11 @@
this@StatelessInputConnectionTest.onImeAction?.invoke(imeAction)
}
- override fun requestEdit(block: EditingBuffer.() -> Unit) {
- onRequestEdit?.invoke(block)
+ override fun requestEdit(
+ notifyImeOfChanges: Boolean,
+ block: EditingBuffer.() -> Unit
+ ) {
+ onRequestEdit?.invoke(notifyImeOfChanges, block)
}
override fun sendKeyEvent(keyEvent: KeyEvent) {
@@ -85,7 +88,7 @@
field = value
state = TextFieldState(value.toString(), value.selectionInChars)
}
- private var onRequestEdit: ((EditingBuffer.() -> Unit) -> Unit)? = null
+ private var onRequestEdit: ((Boolean, EditingBuffer.() -> Unit) -> Unit)? = null
private var onSendKeyEvent: ((KeyEvent) -> Unit)? = null
private var onImeAction: ((ImeAction) -> Unit)? = null
private var onCommitContent: ((TransferableContent) -> Boolean)? = null
@@ -188,9 +191,9 @@
@Test
fun commitTextTest_batchSession() {
var requestEditsCalled = 0
- onRequestEdit = {
+ onRequestEdit = { _, block ->
requestEditsCalled++
- state.mainBuffer.it()
+ state.mainBuffer.block()
}
value = TextFieldCharSequence(text = "", selection = TextRange.Zero)
@@ -320,9 +323,9 @@
@Test
fun mixedAPICalls_batchSession() {
var requestEditsCalled = 0
- onRequestEdit = {
+ onRequestEdit = { _, block ->
requestEditsCalled++
- state.mainBuffer.it()
+ state.mainBuffer.block()
}
value = TextFieldCharSequence(text = "", selection = TextRange.Zero)
@@ -361,7 +364,7 @@
@Test
fun do_not_callback_if_only_readonly_ops() {
var requestEditsCalled = 0
- onRequestEdit = { requestEditsCalled++ }
+ onRequestEdit = { _, _ -> requestEditsCalled++ }
ic.beginBatchEdit()
ic.getSelectedText(1)
ic.endBatchEdit()
@@ -414,6 +417,66 @@
}
@Test
+ fun selectAll_contextMenuAction_triggersSelectionAndImeNotification() {
+ value = TextFieldCharSequence("Hello")
+ var callCount = 0
+ var isNotifyIme = false
+ onRequestEdit = { notify, block ->
+ isNotifyIme = notify
+ callCount++
+ state.mainBuffer.block()
+ }
+
+ ic.performContextMenuAction(android.R.id.selectAll)
+
+ assertThat(callCount).isEqualTo(1)
+ assertThat(isNotifyIme).isTrue()
+ assertThat(state.mainBuffer.selection).isEqualTo(TextRange(0, 5))
+ }
+
+ @Test
+ fun cut_contextMenuAction_triggersSyntheticKeyEvents() {
+ val keyEvents = mutableListOf<KeyEvent>()
+ onSendKeyEvent = { keyEvents += it }
+
+ ic.performContextMenuAction(android.R.id.cut)
+
+ assertThat(keyEvents.size).isEqualTo(2)
+ assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+ assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_CUT)
+ assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+ assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_CUT)
+ }
+
+ @Test
+ fun copy_contextMenuAction_triggersSyntheticKeyEvents() {
+ val keyEvents = mutableListOf<KeyEvent>()
+ onSendKeyEvent = { keyEvents += it }
+
+ ic.performContextMenuAction(android.R.id.copy)
+
+ assertThat(keyEvents.size).isEqualTo(2)
+ assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+ assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_COPY)
+ assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+ assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_COPY)
+ }
+
+ @Test
+ fun paste_contextMenuAction_triggersSyntheticKeyEvents() {
+ val keyEvents = mutableListOf<KeyEvent>()
+ onSendKeyEvent = { keyEvents += it }
+
+ ic.performContextMenuAction(android.R.id.paste)
+
+ assertThat(keyEvents.size).isEqualTo(2)
+ assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+ assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_PASTE)
+ assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+ assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_PASTE)
+ }
+
+ @Test
fun debugMode_isDisabled() {
// run this in presubmit to check that we are not accidentally enabling logs on prod
assertFalse(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
index c223a84..00c788f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifierTest.kt
@@ -166,8 +166,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(40f, 10f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(40f, 10f)
)
view.dispatchDragEvent(startEvent)
@@ -206,8 +206,8 @@
val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
val moveEvent = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(40f, 10f)
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(40f, 10f)
)
view.dispatchDragEvent(startEvent)
@@ -219,8 +219,8 @@
rule.runOnIdle {
val moveEvent2 = makeTextDragEvent(
- DragEvent.ACTION_DRAG_LOCATION,
- Offset(40f, 40f) // force it out of BTF2's hit box
+ action = DragEvent.ACTION_DRAG_LOCATION,
+ offset = Offset(40f, 40f) // force it out of BTF2's hit box
)
view.dispatchDragEvent(moveEvent2)
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
similarity index 70%
rename from compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
index faec6d7..63e7631 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
@@ -24,28 +24,16 @@
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.remember
-import androidx.compose.ui.layout.SubcomposeLayoutState
-import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.trace
import java.util.concurrent.TimeUnit
@ExperimentalFoundationApi
@Composable
-internal actual fun LazyLayoutPrefetcher(
- prefetchState: LazyLayoutPrefetchState,
- itemContentFactory: LazyLayoutItemContentFactory,
- subcomposeLayoutState: SubcomposeLayoutState
-) {
+internal actual fun rememberDefaultPrefetchExecutor(): PrefetchExecutor {
val view = LocalView.current
- remember(subcomposeLayoutState, prefetchState, view) {
- LazyLayoutPrefetcher(
- prefetchState,
- subcomposeLayoutState,
- itemContentFactory,
- view
- )
+ return remember(view) {
+ AndroidPrefetchExecutor(view)
}
}
@@ -105,21 +93,15 @@
* Tracking bug: 187393922
*/
@ExperimentalFoundationApi
-internal class LazyLayoutPrefetcher(
- private val prefetchState: LazyLayoutPrefetchState,
- private val subcomposeLayoutState: SubcomposeLayoutState,
- private val itemContentFactory: LazyLayoutItemContentFactory,
+internal class AndroidPrefetchExecutor(
private val view: View
-) : RememberObserver,
- LazyLayoutPrefetchState.Prefetcher,
- Runnable,
- Choreographer.FrameCallback {
+) : PrefetchExecutor, RememberObserver, Runnable, Choreographer.FrameCallback {
/**
* The list of currently not processed prefetch requests. The requests will be processed one by
* during subsequent [run]s.
*/
- private val prefetchRequests = mutableVectorOf<PrefetchRequest>()
+ private val prefetchRequests = mutableVectorOf<PrefetchExecutor.Request>()
/**
* Average time the prefetching operations takes. Keeping it allows us to not start the work
@@ -158,54 +140,47 @@
var scheduleForNextFrame = false
while (prefetchRequests.isNotEmpty() && !scheduleForNextFrame) {
val request = prefetchRequests[0]
- val itemProvider = itemContentFactory.itemProvider()
- if (request.canceled || request.index !in 0 until itemProvider.itemCount) {
+ if (!request.isValid) {
prefetchRequests.removeAt(0)
- } else if (request.precomposeHandle == null) {
- trace("compose:lazylist:prefetch:compose") {
- val beforeTimeNs = System.nanoTime()
- // check if there is enough time left in this frame. otherwise, we schedule
- // a next frame callback in which we will post the message in the handler again.
- if (enoughTimeLeft(beforeTimeNs, nextFrameNs, averagePrecomposeTimeNs) ||
- oneOverTimeTaskAllowed
- ) {
+ } else if (!request.isComposed) {
+ val beforeTimeNs = System.nanoTime()
+ // check if there is enough time left in this frame. otherwise, we schedule
+ // a next frame callback in which we will post the message in the handler again.
+ if (enoughTimeLeft(
+ beforeTimeNs,
+ nextFrameNs,
+ averagePrecomposeTimeNs
+ ) || oneOverTimeTaskAllowed
+ ) {
+ trace("compose:lazylist:prefetch:compose") {
oneOverTimeTaskAllowed = false
- val key = itemProvider.getKey(request.index)
- val contentType = itemProvider.getContentType(request.index)
- val content = itemContentFactory.getContent(request.index, key, contentType)
- request.precomposeHandle = subcomposeLayoutState.precompose(key, content)
+ request.performComposition()
averagePrecomposeTimeNs = calculateAverageTime(
- System.nanoTime() - beforeTimeNs,
- averagePrecomposeTimeNs
+ System.nanoTime() - beforeTimeNs, averagePrecomposeTimeNs
)
- } else {
- scheduleForNextFrame = true
}
+ } else {
+ scheduleForNextFrame = true
}
} else {
- check(!request.measured) { "request already measured" }
- trace("compose:lazylist:prefetch:measure") {
- val beforeTimeNs = System.nanoTime()
- if (enoughTimeLeft(beforeTimeNs, nextFrameNs, averagePremeasureTimeNs) ||
- oneOverTimeTaskAllowed
- ) {
+ val beforeTimeNs = System.nanoTime()
+ if (enoughTimeLeft(
+ beforeTimeNs,
+ nextFrameNs,
+ averagePremeasureTimeNs
+ ) || oneOverTimeTaskAllowed
+ ) {
+ trace("compose:lazylist:prefetch:measure") {
oneOverTimeTaskAllowed = false
- val handle = request.precomposeHandle!!
- repeat(handle.placeablesCount) { placeableIndex ->
- handle.premeasure(
- placeableIndex,
- request.constraints
- )
- }
+ request.performMeasure()
averagePremeasureTimeNs = calculateAverageTime(
- System.nanoTime() - beforeTimeNs,
- averagePremeasureTimeNs
+ System.nanoTime() - beforeTimeNs, averagePremeasureTimeNs
)
// we finished this request
prefetchRequests.removeAt(0)
- } else {
- scheduleForNextFrame = true
}
+ } else {
+ scheduleForNextFrame = true
}
}
}
@@ -245,53 +220,27 @@
}
}
- override fun schedulePrefetch(
- index: Int,
- constraints: Constraints
- ): LazyLayoutPrefetchState.PrefetchHandle {
- val request = PrefetchRequest(index, constraints)
+ override fun requestPrefetch(request: PrefetchExecutor.Request) {
prefetchRequests.add(request)
if (!prefetchScheduled) {
prefetchScheduled = true
// schedule the prefetching
view.post(this)
}
- return request
}
override fun onRemembered() {
- prefetchState.prefetcher = this
isActive = true
}
override fun onForgotten() {
isActive = false
- prefetchState.prefetcher = null
view.removeCallbacks(this)
choreographer.removeFrameCallback(this)
}
override fun onAbandoned() {}
- private class PrefetchRequest(
- val index: Int,
- val constraints: Constraints
- ) : @Suppress("SEALED_INHERITOR_IN_DIFFERENT_MODULE")
- LazyLayoutPrefetchState.PrefetchHandle {
-
- var precomposeHandle: PrecomposedSlotHandle? = null
- var canceled = false
- var measured = false
-
- override fun cancel() {
- if (!canceled) {
- canceled = true
- precomposeHandle?.dispose()
- precomposeHandle = null
- }
- }
- }
-
companion object {
/**
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
index 2995f36..4e58bdc 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/AndroidTextInputSession.android.kt
@@ -102,9 +102,12 @@
override val text: TextFieldCharSequence
get() = state.visualText
- override fun requestEdit(block: EditingBuffer.() -> Unit) {
+ override fun requestEdit(
+ notifyImeOfChanges: Boolean,
+ block: EditingBuffer.() -> Unit
+ ) {
state.editUntransformedTextAsUser(
- notifyImeOfChanges = false,
+ notifyImeOfChanges = notifyImeOfChanges,
block = block
)
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
index d81a479..7eef0c9 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
@@ -358,7 +358,8 @@
logDebug("performContextMenuAction($id)")
when (id) {
android.R.id.selectAll -> {
- addEditCommandWithBatch {
+ // no need to batch context menu actions.
+ session.requestEdit(notifyImeOfChanges = true) {
setSelection(0, text.length)
}
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
index b5edfe8..45f1dac 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
@@ -40,8 +40,14 @@
/**
* Callback to execute for InputConnection to communicate the changes requested by the IME.
+ *
+ * @param notifyImeOfChanges Normally any request coming from IME should not be
+ * back-communicated but [InputConnection.performContextMenuAction] does not behave like a
+ * regular IME command. Its changes must be resent to IME to keep it in sync with
+ * [TextFieldState].
+ * @param block Lambda scoped to an EditingBuffer to apply changes direct onto a buffer.
*/
- fun requestEdit(block: EditingBuffer.() -> Unit)
+ fun requestEdit(notifyImeOfChanges: Boolean = false, block: EditingBuffer.() -> Unit)
/**
* Delegates IME requested KeyEvents.
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 ab9adee..59d6088 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
@@ -26,6 +26,7 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
@@ -184,45 +185,44 @@
* @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
* behave like bottom to top and left to right will behave like right to left.
*/
+@Stable
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
- onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
- onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
+ onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted,
+ onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped,
reverseDirection: Boolean = false
): Modifier = this then DraggableElement(
state = state,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
- startDragImmediately = { startDragImmediately },
+ startDragImmediately = startDragImmediately,
onDragStarted = onDragStarted,
- onDragStopped = { velocity -> onDragStopped(velocity.toFloat(orientation)) },
- reverseDirection = reverseDirection,
- canDrag = { true }
+ onDragStopped = onDragStopped,
+ reverseDirection = reverseDirection
)
internal class DraggableElement(
private val state: DraggableState,
- private val canDrag: (PointerInputChange) -> Boolean,
private val orientation: Orientation,
private val enabled: Boolean,
private val interactionSource: MutableInteractionSource?,
- private val startDragImmediately: () -> Boolean,
+ private val startDragImmediately: Boolean,
private val onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
- private val onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+ private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
private val reverseDirection: Boolean
) : ModifierNodeElement<DraggableNode>() {
override fun create(): DraggableNode = DraggableNode(
state,
- canDrag,
+ CanDrag,
orientation,
enabled,
interactionSource,
- startDragImmediately,
+ if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
onDragStarted,
onDragStopped,
reverseDirection
@@ -231,11 +231,11 @@
override fun update(node: DraggableNode) {
node.update(
state,
- canDrag,
+ CanDrag,
orientation,
enabled,
interactionSource,
- startDragImmediately,
+ if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
onDragStarted,
onDragStopped,
reverseDirection
@@ -250,7 +250,6 @@
other as DraggableElement
if (state != other.state) return false
- if (canDrag != other.canDrag) return false
if (orientation != other.orientation) return false
if (enabled != other.enabled) return false
if (interactionSource != other.interactionSource) return false
@@ -264,7 +263,6 @@
override fun hashCode(): Int {
var result = state.hashCode()
- result = 31 * result + canDrag.hashCode()
result = 31 * result + orientation.hashCode()
result = 31 * result + enabled.hashCode()
result = 31 * result + (interactionSource?.hashCode() ?: 0)
@@ -277,7 +275,6 @@
override fun InspectorInfo.inspectableProperties() {
name = "draggable"
- properties["canDrag"] = canDrag
properties["orientation"] = orientation
properties["enabled"] = enabled
properties["reverseDirection"] = reverseDirection
@@ -287,6 +284,12 @@
properties["onDragStopped"] = onDragStopped
properties["state"] = state
}
+
+ companion object {
+ val StartDragImmediately = { true }
+ val DoNotStartDragImmediately = { false }
+ val CanDrag: (PointerInputChange) -> Boolean = { true }
+ }
}
internal class DraggableNode(
@@ -297,7 +300,7 @@
interactionSource: MutableInteractionSource?,
startDragImmediately: () -> Boolean,
private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
- private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+ private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
reverseDirection: Boolean
) : AbstractDraggableNode(
canDrag,
@@ -321,7 +324,7 @@
this@DraggableNode.onDragStarted(this, startedPosition)
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) =
- this@DraggableNode.onDragStopped(this, velocity)
+ this@DraggableNode.onDragStopped(this, velocity.toFloat(orientation))
fun update(
state: DraggableState,
@@ -331,7 +334,7 @@
interactionSource: MutableInteractionSource?,
startDragImmediately: () -> Boolean,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
- onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+ onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
reverseDirection: Boolean
) {
var resetPointerInputHandling = false
@@ -691,3 +694,6 @@
private fun Velocity.toFloat(orientation: Orientation) =
if (orientation == Orientation.Vertical) this.y else this.x
+
+private val NoOpOnDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}
+private val NoOpOnDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 47afcb9..da8ca4d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -607,7 +607,7 @@
val draggableState = ScrollDraggableState(scrollLogic)
private val startDragImmediately = { scrollLogic.shouldScrollImmediately() }
- private val onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit = { velocity ->
+ private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = { velocity ->
nestedScrollDispatcher.coroutineScope.launch {
scrollLogic.onDragStopped(velocity)
}
@@ -742,6 +742,11 @@
fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this
+ fun Float.toVelocity() = Velocity(
+ x = if (orientation == Horizontal) this else 0f,
+ y = if (orientation == Orientation.Vertical) this else 0f,
+ )
+
/**
* @return the amount of scroll that was consumed
*/
@@ -793,11 +798,11 @@
}
}
- suspend fun onDragStopped(initialVelocity: Velocity) {
+ suspend fun onDragStopped(initialVelocity: Float) {
// Self started flinging, set
registerNestedFling(true)
- val availableVelocity = initialVelocity.singleAxisVelocity()
+ val availableVelocity = initialVelocity.toVelocity()
val performFling: suspend (Velocity) -> Velocity = { velocity ->
val preConsumedByParent = nestedScrollDispatcher
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
new file mode 100644
index 0000000..cc4614b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 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
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.PrefetchExecutor
+import androidx.compose.runtime.Stable
+
+/**
+ * Implementations of this interface control which indices of a LazyList should be prefetched
+ * (precomposed and premeasured during idle time) as the user interacts with it.
+ *
+ * Implementations should invoke [LazyListPrefetchScope.schedulePrefetch] to schedule prefetches
+ * from the [onScroll] and [onVisibleItemsUpdated] callbacks. If any of the returned PrefetchHandles
+ * no longer need to be prefetched, use [LazyLayoutPrefetchState.PrefetchHandle.cancel] to cancel
+ * the request.
+ */
+@ExperimentalFoundationApi
+interface LazyListPrefetchStrategy {
+
+ /**
+ * A PrefetchExecutor implementation which will be used to execute prefetch requests for this
+ * strategy implementation. If null, the default PrefetchExecutor for the platform will be used.
+ */
+ val prefetchExecutor: PrefetchExecutor?
+ get() = null
+
+ /**
+ * onScroll is invoked when the LazyList scrolls, whether or not the visible items have changed.
+ * If the visible items have also changed, then this will be invoked in the same frame *after*
+ * [onVisibleItemsUpdated].
+ *
+ * [delta] can be used to understand scroll direction: delta < 0 indicates scrolling down while
+ * delta > 0 indicates scrolling up.
+ */
+ fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo)
+
+ /**
+ * onVisibleItemsUpdated is invoked when the LazyList scrolls if the visible items have changed.
+ * Info about these visible items can be found in [layoutInfo]'s
+ * [LazyListLayoutInfo.visibleItemsInfo].
+ */
+ fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo)
+}
+
+/**
+ * Scope for callbacks in [LazyListPrefetchStrategy] which allows prefetches to be requested.
+ */
+@ExperimentalFoundationApi
+interface LazyListPrefetchScope {
+
+ /**
+ * Schedules a prefetch for the given index. Requests are executed in the order they're
+ * requested. If a requested prefetch is no longer necessary (for example, due to changing
+ * scroll direction), the request should be canceled via
+ * [LazyLayoutPrefetchState.PrefetchHandle.cancel].
+ *
+ * See [PrefetchExecutor].
+ */
+ fun schedulePrefetch(index: Int): LazyLayoutPrefetchState.PrefetchHandle
+}
+
+/**
+ * The default prefetching strategy for LazyLists - this will be used automatically if no other
+ * strategy is provided.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+internal class DefaultLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+
+ /**
+ * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+ */
+ private var indexToPrefetch = -1
+
+ /**
+ * The handle associated with the current index from [indexToPrefetch].
+ */
+ private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+ /**
+ * Keeps the scrolling direction during the previous calculation in order to be able to
+ * detect the scrolling direction change.
+ */
+ private var wasScrollingForward = false
+
+ override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+ if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
+ val scrollingForward = delta < 0
+ val indexToPrefetch = if (scrollingForward) {
+ layoutInfo.visibleItemsInfo.last().index + 1
+ } else {
+ layoutInfo.visibleItemsInfo.first().index - 1
+ }
+ if (indexToPrefetch != this@DefaultLazyListPrefetchStrategy.indexToPrefetch &&
+ indexToPrefetch in 0 until layoutInfo.totalItemsCount
+ ) {
+ if (wasScrollingForward != scrollingForward) {
+ // the scrolling direction has been changed which means the last prefetched
+ // is not going to be reached anytime soon so it is safer to dispose it.
+ // if this item is already visible it is safe to call the method anyway
+ // as it will be no-op
+ currentPrefetchHandle?.cancel()
+ }
+ this@DefaultLazyListPrefetchStrategy.wasScrollingForward = scrollingForward
+ this@DefaultLazyListPrefetchStrategy.indexToPrefetch = indexToPrefetch
+ currentPrefetchHandle = schedulePrefetch(
+ indexToPrefetch
+ )
+ }
+ }
+ }
+
+ override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
+ if (indexToPrefetch != -1 && layoutInfo.visibleItemsInfo.isNotEmpty()) {
+ val expectedPrefetchIndex = if (wasScrollingForward) {
+ layoutInfo.visibleItemsInfo.last().index + 1
+ } else {
+ layoutInfo.visibleItemsInfo.first().index - 1
+ }
+ if (indexToPrefetch != expectedPrefetchIndex) {
+ indexToPrefetch = -1
+ currentPrefetchHandle?.cancel()
+ currentPrefetchHandle = null
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 2090fa7..5528563 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -31,6 +31,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.LazyListState.Companion.Saver
import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
@@ -58,7 +59,6 @@
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.abs
import kotlin.ranges.IntRange
-import kotlin.ranges.until
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -86,6 +86,34 @@
}
/**
+ * Creates a [LazyListState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [LazyListState.firstVisibleItemScrollOffset]
+ * @param prefetchStrategy the [LazyListPrefetchStrategy] to use for prefetching content in this
+ * list
+ */
+@ExperimentalFoundationApi
+@Composable
+fun rememberLazyListState(
+ initialFirstVisibleItemIndex: Int = 0,
+ initialFirstVisibleItemScrollOffset: Int = 0,
+ prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy(),
+): LazyListState {
+ return rememberSaveable(saver = LazyListState.saver(prefetchStrategy)) {
+ LazyListState(
+ initialFirstVisibleItemIndex,
+ initialFirstVisibleItemScrollOffset,
+ prefetchStrategy
+ )
+ }
+}
+
+/**
* A state object that can be hoisted to control and observe scrolling.
*
* In most cases, this will be created via [rememberLazyListState].
@@ -93,14 +121,27 @@
* @param firstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
* @param firstVisibleItemScrollOffset the initial value for
* [LazyListState.firstVisibleItemScrollOffset]
+ * @param prefetchStrategy the [LazyListPrefetchStrategy] to use for prefetching content in this
+ * list
*/
@OptIn(ExperimentalFoundationApi::class)
@Stable
-class LazyListState constructor(
+class LazyListState @ExperimentalFoundationApi constructor(
firstVisibleItemIndex: Int = 0,
- firstVisibleItemScrollOffset: Int = 0
+ firstVisibleItemScrollOffset: Int = 0,
+ private val prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy(),
) : ScrollableState {
+ /**
+ * @param firstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [LazyListState.firstVisibleItemScrollOffset]
+ */
+ constructor(
+ firstVisibleItemIndex: Int = 0,
+ firstVisibleItemScrollOffset: Int = 0
+ ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, DefaultLazyListPrefetchStrategy())
+
internal var hasLookaheadPassOccurred: Boolean = false
private set
internal var postLookaheadLayoutInfo: LazyListMeasureResult? = null
@@ -145,6 +186,7 @@
EmptyLazyListMeasureResult,
neverEqualPolicy()
)
+
/**
* The object of [LazyListLayoutInfo] calculated during the last layout pass. For example,
* you can use it to calculate what items are currently visible.
@@ -198,22 +240,6 @@
internal var prefetchingEnabled: Boolean = true
/**
- * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
- */
- private var indexToPrefetch = -1
-
- /**
- * The handle associated with the current index from [indexToPrefetch].
- */
- private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
-
- /**
- * Keeps the scrolling direction during the previous calculation in order to be able to
- * detect the scrolling direction change.
- */
- private var wasScrollingForward = false
-
- /**
* The [Remeasurement] object associated with our layout. It allows us to remeasure
* synchronously during scroll.
*/
@@ -239,6 +265,19 @@
internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
+ internal val prefetchState = LazyLayoutPrefetchState(prefetchStrategy.prefetchExecutor)
+
+ private val prefetchScope = object : LazyListPrefetchScope {
+ override fun schedulePrefetch(index: Int): LazyLayoutPrefetchState.PrefetchHandle {
+ // Without read observation since this can be triggered from scroll - this will then
+ // cause us to recompose when the measure result changes. We don't care since the
+ // prefetch is best effort.
+ val constraints =
+ Snapshot.withoutReadObservation { layoutInfoState.value.childConstraints }
+ return prefetchState.schedulePrefetch(index, constraints)
+ }
+ }
+
/**
* Stores currently pinned items which are always composed.
*/
@@ -340,11 +379,16 @@
// we don't need to remeasure, so we only trigger re-placement:
placementScopeInvalidator.invalidateScope()
- notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed, layoutInfo)
+ notifyPrefetchOnScroll(
+ preScrollToBeConsumed - scrollToBeConsumed,
+ layoutInfo
+ )
} else {
remeasurement?.forceRemeasure()
-
- notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+ notifyPrefetchOnScroll(
+ preScrollToBeConsumed - scrollToBeConsumed,
+ this.layoutInfo
+ )
}
}
@@ -362,57 +406,17 @@
}
}
- private fun notifyPrefetch(
- delta: Float,
- layoutInfo: LazyListMeasureResult = layoutInfoState.value
- ) {
- if (!prefetchingEnabled) {
- return
- }
- val info = layoutInfo
- if (info.visibleItemsInfo.isNotEmpty()) {
- val scrollingForward = delta < 0
- val indexToPrefetch = if (scrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (indexToPrefetch != this.indexToPrefetch &&
- indexToPrefetch in 0 until info.totalItemsCount
- ) {
- if (wasScrollingForward != scrollingForward) {
- // the scrolling direction has been changed which means the last prefetched
- // is not going to be reached anytime soon so it is safer to dispose it.
- // if this item is already visible it is safe to call the method anyway
- // as it will be no-op
- currentPrefetchHandle?.cancel()
- }
- this.wasScrollingForward = scrollingForward
- this.indexToPrefetch = indexToPrefetch
- currentPrefetchHandle = prefetchState.schedulePrefetch(
- indexToPrefetch, layoutInfo.childConstraints
+ fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+ if (prefetchingEnabled) {
+ with(prefetchStrategy) {
+ prefetchScope.onScroll(
+ delta,
+ layoutInfo
)
}
}
}
- private fun cancelPrefetchIfVisibleItemsChanged(info: LazyListLayoutInfo) {
- if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
- val expectedPrefetchIndex = if (wasScrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (indexToPrefetch != expectedPrefetchIndex) {
- indexToPrefetch = -1
- currentPrefetchHandle?.cancel()
- currentPrefetchHandle = null
- }
- }
- }
-
- internal val prefetchState = LazyLayoutPrefetchState()
-
/**
* Animate (smooth scroll) to the given item.
*
@@ -449,17 +453,23 @@
if (isLookingAhead) {
hasLookaheadPassOccurred = true
}
- if (visibleItemsStayedTheSame) {
- scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffset)
- } else {
- scrollPosition.updateFromMeasureResult(result)
- cancelPrefetchIfVisibleItemsChanged(result)
- }
+
canScrollBackward = result.canScrollBackward
canScrollForward = result.canScrollForward
scrollToBeConsumed -= result.consumedScroll
layoutInfoState.value = result
+ if (visibleItemsStayedTheSame) {
+ scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffset)
+ } else {
+ scrollPosition.updateFromMeasureResult(result)
+ if (prefetchingEnabled) {
+ with(prefetchStrategy) {
+ prefetchScope.onVisibleItemsUpdated(result)
+ }
+ }
+ }
+
if (isLookingAhead) {
updateScrollDeltaForPostLookahead(
result.scrollBackAmount,
@@ -538,6 +548,23 @@
)
}
)
+
+ /**
+ * A [Saver] implementation for [LazyListState] that handles setting a custom
+ * [LazyListPrefetchStrategy].
+ */
+ @ExperimentalFoundationApi
+ internal fun saver(prefetchStrategy: LazyListPrefetchStrategy): Saver<LazyListState, *> =
+ listSaver(
+ save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+ restore = {
+ LazyListState(
+ firstVisibleItemIndex = it[0],
+ firstVisibleItemScrollOffset = it[1],
+ prefetchStrategy
+ )
+ }
+ )
}
}
@@ -551,6 +578,7 @@
measureResult = object : MeasureResult {
override val width: Int = 0
override val height: Int = 0
+
@Suppress("PrimitiveInCollection")
override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
override fun placeChildren() {}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 031d0f1..f1d49fd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
@@ -85,12 +86,23 @@
val subcomposeLayoutState = remember {
SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
}
- prefetchState?.let {
- LazyLayoutPrefetcher(
+ if (prefetchState != null) {
+ val executor = prefetchState.prefetchExecutor ?: rememberDefaultPrefetchExecutor()
+ DisposableEffect(
prefetchState,
itemContentFactory,
- subcomposeLayoutState
- )
+ subcomposeLayoutState,
+ executor
+ ) {
+ prefetchState.prefetchHandleProvider = PrefetchHandleProvider(
+ itemContentFactory,
+ subcomposeLayoutState,
+ executor
+ )
+ onDispose {
+ prefetchState.prefetchHandleProvider = null
+ }
+ }
}
SubcomposeLayout(
@@ -143,15 +155,3 @@
* 5 (RecycledViewPool.DEFAULT_MAX_SCRAP) + 2 (Recycler.DEFAULT_CACHE_SIZE)
*/
private const val MaxItemsToRetainForReuse = 7
-
-/**
- * Platform specific implementation of lazy layout items prefetching - precomposing next items in
- * advance during the scrolling.
- */
-@ExperimentalFoundationApi
-@Composable
-internal expect fun LazyLayoutPrefetcher(
- prefetchState: LazyLayoutPrefetchState,
- itemContentFactory: LazyLayoutItemContentFactory,
- subcomposeLayoutState: SubcomposeLayoutState
-)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
index 3aa8757..eac40e5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
@@ -17,7 +17,9 @@
package androidx.compose.foundation.lazy.layout
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
import androidx.compose.runtime.Stable
+import androidx.compose.ui.layout.SubcomposeLayoutState
import androidx.compose.ui.unit.Constraints
/**
@@ -26,11 +28,14 @@
* Note: this class is a part of [LazyLayout] harness that allows for building custom lazy
* layouts. LazyLayout and all corresponding APIs are still under development and are subject to
* change.
+ *
+ * @param prefetchExecutor the PrefetchExecutor implementation to use to execute prefetch requests.
+ * If null is provided, the default PrefetchExecutor for the platform will be used.
*/
@ExperimentalFoundationApi
@Stable
-class LazyLayoutPrefetchState {
- internal var prefetcher: Prefetcher? = null
+class LazyLayoutPrefetchState(internal val prefetchExecutor: PrefetchExecutor? = null) {
+ internal var prefetchHandleProvider: PrefetchHandleProvider? = null
/**
* Schedules precomposition and premeasure for the new item.
@@ -39,7 +44,7 @@
* @param constraints [Constraints] to use for premeasuring.
*/
fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle {
- return prefetcher?.schedulePrefetch(index, constraints) ?: DummyHandle
+ return prefetchHandleProvider?.schedulePrefetch(index, constraints) ?: DummyHandle
}
sealed interface PrefetchHandle {
@@ -49,13 +54,84 @@
*/
fun cancel()
}
-
- internal interface Prefetcher {
- fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle
- }
}
@ExperimentalFoundationApi
-private object DummyHandle : LazyLayoutPrefetchState.PrefetchHandle {
+private object DummyHandle : PrefetchHandle {
override fun cancel() {}
}
+
+/**
+ * PrefetchHandleProvider is used to connect the [LazyLayoutPrefetchState], which provides the API
+ * to schedule prefetches, to a [LazyLayoutItemContentFactory] which resolves key and content from
+ * an index, [SubcomposeLayoutState] which knows how to precompose/premeasure,
+ * and a specific [PrefetchExecutor] used to execute a request.
+ */
+@ExperimentalFoundationApi
+internal class PrefetchHandleProvider(
+ private val itemContentFactory: LazyLayoutItemContentFactory,
+ private val subcomposeLayoutState: SubcomposeLayoutState,
+ private val executor: PrefetchExecutor
+) {
+ fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle =
+ HandleAndRequestImpl(index, constraints).also {
+ executor.requestPrefetch(it)
+ }
+
+ @ExperimentalFoundationApi
+ private inner class HandleAndRequestImpl(
+ private val index: Int,
+ private val constraints: Constraints
+ ) : PrefetchHandle, PrefetchExecutor.Request {
+
+ private var precomposeHandle: SubcomposeLayoutState.PrecomposedSlotHandle? = null
+ private var isMeasured = false
+ private var isCanceled = false
+
+ override val isValid: Boolean
+ get() = !isCanceled &&
+ index in 0 until itemContentFactory.itemProvider().itemCount
+
+ override val isComposed get() = precomposeHandle != null
+
+ override fun cancel() {
+ if (!isCanceled) {
+ isCanceled = true
+ precomposeHandle?.dispose()
+ precomposeHandle = null
+ }
+ }
+
+ override fun performComposition() {
+ require(isValid) {
+ "Callers should check whether the request is still valid before calling " +
+ "performComposition()"
+ }
+ require(precomposeHandle == null) { "Request was already composed!" }
+ val itemProvider = itemContentFactory.itemProvider()
+ val key = itemProvider.getKey(index)
+ val contentType = itemProvider.getContentType(index)
+ val content = itemContentFactory.getContent(index, key, contentType)
+ precomposeHandle = subcomposeLayoutState.precompose(key, content)
+ }
+
+ override fun performMeasure() {
+ require(!isCanceled) {
+ "Callers should check whether the request is still valid before calling " +
+ "performMeasure()"
+ }
+ require(!isMeasured) { "Request was already measured!" }
+ isMeasured = true
+ val handle = requireNotNull(precomposeHandle) {
+ "performComposition() must be called before performMeasure()"
+ }
+ repeat(handle.placeablesCount) { placeableIndex ->
+ handle.premeasure(placeableIndex, constraints)
+ }
+ }
+
+ override fun toString(): String =
+ "HandleAndRequestImpl { index = $index, constraints = $constraints, " +
+ "isComposed = $isComposed, isMeasured = $isMeasured, isCanceled = $isCanceled }"
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt
new file mode 100644
index 0000000..7e1b31e
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 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.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+
+/**
+ * Remembers the platform-specific implementation for scheduling lazy layout item prefetch
+ * (pre-composing next items in advance during the scrolling).
+ */
+@ExperimentalFoundationApi
+@Composable
+internal expect fun rememberDefaultPrefetchExecutor(): PrefetchExecutor
+
+/**
+ * Implementations of this interface accept prefetch requests via [requestPrefetch] and decide when
+ * to execute them in a way that will have minimal impact on user experience, e.g. during frame idle
+ * time. Executing a request involves invoking [Request.performComposition] and
+ * [Request.performMeasure].
+ */
+@ExperimentalFoundationApi
+interface PrefetchExecutor {
+
+ /**
+ * Accepts a prefetch request. Implementations should find a time to execute them which will
+ * have minimal impact on user experience.
+ */
+ fun requestPrefetch(request: Request)
+
+ sealed interface Request {
+
+ /**
+ * Whether this is still a valid request (wasn't canceled, within list bounds). If it's
+ * not valid, it should be dropped and not executed.
+ */
+ val isValid: Boolean
+
+ /**
+ * Whether this request has been composed via [performComposition].
+ */
+ val isComposed: Boolean
+
+ /**
+ * Composes the content belonging to this request.
+ */
+ fun performComposition()
+
+ /**
+ * Measures the Composition belonging to this request. Must be called after
+ * [performComposition].
+ */
+ fun performMeasure()
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 64b2cfd..dfc0dae 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -444,6 +444,7 @@
-firstItemOffsets[it]
}
+ val minVisibleOffset = minOffset + mainAxisSpacing
val maxOffset = (mainAxisAvailableSize + afterContentPadding).coerceAtLeast(0)
debugLog {
@@ -473,14 +474,20 @@
)
laneInfo.setLane(itemIndex, spanRange.laneInfo)
- val offset = currentItemOffsets.maxInRange(spanRange) + measuredItem.sizeWithSpacings
+ val offset = currentItemOffsets.maxInRange(spanRange)
spanRange.forEach { lane ->
- currentItemOffsets[lane] = offset
+ currentItemOffsets[lane] = offset + measuredItem.sizeWithSpacings
currentItemIndices[lane] = itemIndex
measuredItems[lane].addLast(measuredItem)
}
- if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+ // item is not visible if both start and end bounds are outside of the visible range.
+ if (
+ offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+ ) {
+ // We scrolled past measuredItem, and it is not visible anymore. We measured it
+ // for correct positioning of other items, but there's no need to place it.
+ // Mark it as not visible and filter below.
measuredItem.isVisible = false
remeasureNeeded = true
}
@@ -538,7 +545,10 @@
}
laneInfo.setGaps(itemIndex, gaps)
- if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+ // item is not visible if both start and end bounds are outside of the visible range.
+ if (
+ offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+ ) {
// We scrolled past measuredItem, and it is not visible anymore. We measured it
// for correct positioning of other items, but there's no need to place it.
// Mark it as not visible and filter below.
@@ -564,6 +574,7 @@
firstItemIndices[laneIndex] = laneItems.firstOrNull()?.index ?: Unset
}
+ // ensure no spacing for the last item
if (currentItemIndices.any { it == itemCount - 1 }) {
currentItemOffsets.offsetBy(-mainAxisSpacing)
}
@@ -592,13 +603,21 @@
// Note that it is different from initial pass up where we selected largest index
// instead. The reason is that we already distributed items on downward pass and
// gap would be incorrect if those are moved.
- val laneIndex = firstItemOffsets.indexOfMinValue()
+ var laneIndex = firstItemOffsets.indexOfMinValue()
+ val nextLaneIndex = firstItemIndices.indexOfMaxValue()
- if (laneIndex != firstItemIndices.indexOfMaxValue()) {
- // If min offset lane doesn't have largest value, it means items are misaligned.
- // The correct thing here is to restart measure. We will measure up to the end
- // and restart measure from there after this pass.
- gapDetected = true
+ if (laneIndex != nextLaneIndex) {
+ if (firstItemOffsets[laneIndex] == firstItemOffsets[nextLaneIndex]) {
+ // If the offsets are the same, it means that there's no gap here.
+ // In this case, we should choose the lane where item would go normally.
+ laneIndex = nextLaneIndex
+ } else {
+ // If min offset lane doesn't have largest value, it means items are
+ // misaligned.
+ // The correct thing here is to restart measure. We will measure up to the
+ // end and restart measure from there after this pass.
+ gapDetected = true
+ }
}
val currentIndex =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index 973ed6d..97add2d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalFoundationApi::class)
+
package androidx.compose.foundation.pager
import androidx.compose.animation.core.AnimationSpec
@@ -63,7 +65,7 @@
import kotlin.math.roundToInt
import kotlinx.coroutines.coroutineScope
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun Pager(
/** Modifier to be applied for the inner layout */
@@ -267,7 +269,6 @@
/**
* A modifier to detect up and down events in a Pager.
*/
-@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.dragDirectionDetector(state: PagerState) =
this then Modifier.pointerInput(state) {
coroutineScope {
@@ -328,7 +329,6 @@
/**
* Wraps [SnapFlingBehavior] to give out information about target page coming from flings.
*/
-@OptIn(ExperimentalFoundationApi::class)
private class PagerWrapperFlingBehavior(
val originalFlingBehavior: TargetedFlingBehavior,
val pagerState: PagerState
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 13b10ea..781762c 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
@@ -19,10 +19,8 @@
import androidx.annotation.FloatRange
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
@@ -105,7 +103,6 @@
* @param pageContent This Pager's page Composable.
*/
@Composable
-@ExperimentalFoundationApi
fun HorizontalPager(
state: PagerState,
modifier: Modifier = Modifier,
@@ -192,7 +189,6 @@
* @param pageContent This Pager's page Composable.
*/
@Composable
-@ExperimentalFoundationApi
fun VerticalPager(
state: PagerState,
modifier: Modifier = Modifier,
@@ -234,7 +230,6 @@
/**
* Contains the default values used by [Pager].
*/
-@ExperimentalFoundationApi
object PagerDefaults {
/**
@@ -291,6 +286,7 @@
* position. If the velocity is high enough, the Pager will use the logic described in
* [decayAnimationSpec] and [snapAnimationSpec].
*/
+ @OptIn(ExperimentalFoundationApi::class)
@Composable
fun flingBehavior(
state: PagerState,
@@ -335,89 +331,6 @@
}
/**
- * A [SnapFlingBehavior] that will snap pages to the start of the layout. One can use the
- * given parameters to control how the snapping animation will happen.
- * @see androidx.compose.foundation.gestures.snapping.SnapFlingBehavior for more information
- * on what which parameter controls in the overall snapping animation.
- *
- * The animation specs used by the fling behavior will depend on 2 factors:
- * 1) The gesture velocity.
- * 2) The target page proposed by [pagerSnapDistance].
- *
- * If you're using single page snapping (the most common use case for [Pager]), there won't
- * be enough space to actually run a decay animation to approach the target page, so the Pager
- * will always use the snapping animation from [snapAnimationSpec].
- * If you're using multi-page snapping (this means you're abs(targetPage - currentPage) > 1)
- * the Pager may use [highVelocityAnimationSpec] or [lowVelocityAnimationSpec] to approach the
- * targetPage, it will depend on the velocity generated by the triggering gesture.
- * If the gesture has a high enough velocity to approach the target page, the Pager will use
- * [highVelocityAnimationSpec] followed by [snapAnimationSpec] for the final step of the
- * animation. If the gesture doesn't have enough velocity, the Pager will use
- * [lowVelocityAnimationSpec] + [snapAnimationSpec] in a similar fashion.
- *
- * @param state The [PagerState] that controls the which to which this FlingBehavior will
- * be applied to.
- * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
- * The default behavior will result in any fling going to the next page in the direction of the
- * fling (if the fling has enough velocity, otherwise the Pager will bounce back). Use
- * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
- * fling after scrolling is finished and fling has started.
- * @param lowVelocityAnimationSpec An animation spec used to approach the target offset. When
- * the fling velocity is not large enough. Large enough means large enough to naturally decay.
- * When snapping through many pages, the Pager may not be able to run a decay animation, so it
- * will use this spec to run an animation to approach the target page requested by
- * [pagerSnapDistance].
- * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
- * the fling velocity is large enough. Large enough means large enough to naturally decay. For
- * single page snapping this usually never happens since there won't be enough space to run a
- * decay animation.
- * @param snapAnimationSpec The animation spec used to finally snap to the position. This
- * animation will be often used in 2 cases: 1) There was enough space to an approach animation,
- * the Pager will use [snapAnimationSpec] in the last step of the animation to settle the page
- * into position. 2) There was not enough space to run the approach animation.
- * @param snapPositionalThreshold If the fling has a low velocity (e.g. slow scroll),
- * this fling behavior will use this snap threshold in order to determine if the pager should
- * snap back or move forward. Use a number between 0 and 1 as a fraction of the page size that
- * needs to be scrolled before the Pager considers it should move to the next page.
- * For instance, if snapPositionalThreshold = 0.35, it means if this pager is scrolled with a
- * slow velocity and the Pager scrolls more than 35% of the page size, then will jump to the
- * next page, if not it scrolls back.
- * Note that any fling that has high enough velocity will *always* move to the next page
- * in the direction of the fling.
- *
- * @return An instance of [FlingBehavior] that will perform Snapping to the next page by
- * default. The animation will be governed by the post scroll velocity and the Pager will use
- * either
- * [lowVelocityAnimationSpec] or [highVelocityAnimationSpec] to approach the snapped position
- * If a velocity is not high enough the pager will use [snapAnimationSpec] to reach the snapped
- * position. If the velocity is high enough, the Pager will use the logic described in
- * [highVelocityAnimationSpec] and [lowVelocityAnimationSpec].
- */
- @Suppress("UNUSED_PARAMETER")
- @Deprecated(
- "Please use the overload without lowVelocityAnimationSpec.",
- level = DeprecationLevel.ERROR
- )
- @Composable
- fun flingBehavior(
- state: PagerState,
- pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
- lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
- easing = LinearEasing,
- durationMillis = LowVelocityAnimationDefaultDuration
- ),
- highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
- snapPositionalThreshold: Float = 0.5f
- ) = flingBehavior(
- state,
- pagerSnapDistance,
- highVelocityAnimationSpec,
- snapAnimationSpec,
- snapPositionalThreshold
- )
-
- /**
* The default implementation of Pager's pageNestedScrollConnection.
*
* @param state state of the pager
@@ -461,7 +374,6 @@
return (snapOffset - currentPageOffsetFraction * (pageSize + spaceBetweenPages)).roundToInt()
}
-@OptIn(ExperimentalFoundationApi::class)
private class DefaultPagerNestedScrollConnection(
val state: PagerState,
val orientation: Orientation
@@ -534,7 +446,6 @@
}
}
-@OptIn(ExperimentalFoundationApi::class)
@Suppress("ComposableModifierFactory")
@Composable
internal fun Modifier.pagerSemantics(state: PagerState, isVertical: Boolean): Modifier {
@@ -572,8 +483,6 @@
})
}
-private const val LowVelocityAnimationDefaultDuration = 500
-
private inline fun debugLog(generateMsg: () -> String) {
if (PagerDebugConfig.MainPagerComposable) {
println("Pager: ${generateMsg()}")
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
index 3462881..cf6a1da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.pager
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.ui.unit.IntSize
@@ -27,7 +26,6 @@
*
* Use [PagerState.layoutInfo] to retrieve this
*/
-@ExperimentalFoundationApi
sealed interface PagerLayoutInfo {
/**
* A list of all pages that are currently visible in the [Pager]
@@ -106,6 +104,5 @@
val snapPosition: SnapPosition
}
-@ExperimentalFoundationApi
internal val PagerLayoutInfo.mainAxisViewportSize: Int
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
index 1495575..bf6ab3b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
@@ -16,14 +16,12 @@
package androidx.compose.foundation.pager
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
-@OptIn(ExperimentalFoundationApi::class)
internal class PagerMeasureResult(
override val visiblePagesInfo: List<MeasuredPage>,
override val pageSize: Int,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 940f9d0..6912ae3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -72,7 +72,6 @@
* snapped position.
* @param pageCount The amount of pages this Pager will have.
*/
-@ExperimentalFoundationApi
@Composable
fun rememberPagerState(
initialPage: Int = 0,
@@ -102,14 +101,12 @@
* snapped position.
* @param pageCount The amount of pages this Pager will have.
*/
-@ExperimentalFoundationApi
fun PagerState(
currentPage: Int = 0,
@FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
pageCount: () -> Int
): PagerState = DefaultPagerState(currentPage, currentPageOffsetFraction, pageCount)
-@ExperimentalFoundationApi
private class DefaultPagerState(
currentPage: Int,
currentPageOffsetFraction: Float,
@@ -148,7 +145,7 @@
* @param currentPageOffsetFraction The offset of the initial page with respect to the start of
* the layout.
*/
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
@Stable
abstract class PagerState(
currentPage: Int = 0,
@@ -505,6 +502,7 @@
* @param pageOffsetFraction A fraction of the page size that indicates the offset the
* destination page will be offset from its snapped position.
*/
+ @ExperimentalFoundationApi
fun ScrollScope.updateCurrentPage(
page: Int,
@FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f
@@ -525,6 +523,7 @@
* Please refer to the sample to learn how to use this API.
* @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
*/
+ @ExperimentalFoundationApi
fun ScrollScope.updateTargetPage(targetPage: Int) {
programmaticScrollTargetPage = targetPage.coerceInPageRange()
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
index 631e0e1..26d0e5b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
@@ -37,15 +37,19 @@
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.util.fastForEach
-import kotlin.math.min
internal typealias LinkRange = AnnotatedString.Range<LinkAnnotation>
@@ -100,19 +104,25 @@
val path = it.getPathForRange(range.start, range.end)
val firstCharBoundingBox = it.getBoundingBox(range.start)
- val minTop = firstCharBoundingBox.top
- var minLeft = firstCharBoundingBox.left
- val firstLine = it.getLineForOffset(range.start)
- val lastLine = it.getLineForOffset(range.end)
- // might be enough to just check if the second line exist
- // if yes - take it's left bound or even just 0
- // TODO(soboleva) check in RTL
- for (line in firstLine + 1..lastLine) {
- val lineLeft = it.getLineLeft(line)
- minLeft = min(minLeft, lineLeft)
+ val lastCharBoundingBox = it.getBoundingBox(range.end - 1)
+
+ val rangeStartLine = it.getLineForOffset(range.start)
+ val rangeEndLine = it.getLineForOffset(range.end)
+
+ val xOffset = if (rangeStartLine == rangeEndLine) {
+ // if the link occupies a single line, we take the left most position of the
+ // link's range
+ minOf(lastCharBoundingBox.left, firstCharBoundingBox.left)
+ } else {
+ // if the link occupies more than one line, the left sides of the link node and
+ // text node match so we don't need to do anything
+ 0f
}
- path.translate(-Offset(minLeft, minTop))
+ // the top of the top-most (first) character
+ val yOffset = firstCharBoundingBox.top
+
+ path.translate(-Offset(xOffset, yOffset))
return path
}
}
@@ -136,6 +146,19 @@
Box(
clipModifier
.textRange(range.start, range.end)
+ .semantics {
+ linkClickHandler?.let {
+ customActions = listOf(
+ // this action will be passed down to the Talkback through the
+ // ClickableSpan's onClick method
+ CustomAccessibilityAction("") {
+ it.onClick(range.item)
+ true
+ }
+ )
+ textSelectionRange = TextRange(range.start, range.end)
+ }
+ }
.pointerHoverIcon(PointerIcon.Hand)
.combinedClickable(null, indication, onClick = {
handleLink(range.item, uriHandler, linkClickHandler)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 9766ec8..69d0f74 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -378,6 +378,9 @@
getTextLayoutResult(action = localSemanticsTextLayoutResult)
}
+ override val shouldClearDescendantSemantics: Boolean
+ get() = true
+
fun measureNonExtension(
measureScope: MeasureScope,
measurable: Measurable,
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
similarity index 72%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
rename to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
index 40510e7..6f9cc99 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
@@ -18,14 +18,15 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
-import androidx.compose.ui.layout.SubcomposeLayoutState
@ExperimentalFoundationApi
@Composable
-internal actual fun LazyLayoutPrefetcher(
- prefetchState: LazyLayoutPrefetchState,
- itemContentFactory: LazyLayoutItemContentFactory,
- subcomposeLayoutState: SubcomposeLayoutState
-) {
- // there is no prefetch implementation on desktop yet
+actual fun rememberDefaultPrefetchExecutor(): PrefetchExecutor {
+ return NoOpPrefetchExecutor
+}
+
+@ExperimentalFoundationApi
+private object NoOpPrefetchExecutor : PrefetchExecutor {
+ override fun requestPrefetch(request: PrefetchExecutor.Request) {
+ }
}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index d56e2a2..1873374 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -91,6 +91,19 @@
</intent-filter>
</activity>
<activity
+ android:name=".FrameExperimentActivity"
+ android:label="FrameExp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.FRAME_EXPERIMENT_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".BaselineProfileActivity"
android:exported="true">
<intent-filter>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
new file mode 100644
index 0000000..7c4f269
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.integration.macrobenchmark.target
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Bundle
+import android.os.Trace
+import android.view.View
+import androidx.activity.ComponentActivity
+
+private const val FRAME_ADDED_WORK_MS = 20L
+private const val FRAME_BASELINE_WORK_MS = 10L
+private const val FRAME_COUNT = 100
+
+// NOTE: Keep in sync with FrameExperimentBenchmark!!
+private enum class FrameMode(val id: Int) {
+ Fast(0),
+ PrefetchEveryFrame(1),
+ WorkDuringEveryFrame(2),
+ PrefetchSomeFrames(3),
+}
+
+private class FrameExperimentView(context: Context, val mode: FrameMode) : View(context) {
+
+ init {
+ setOnClickListener {
+ remainingFrames = FRAME_COUNT - 1
+ invalidate()
+ }
+ }
+ var remainingFrames = 0
+
+ fun work(durationMs: Long = FRAME_ADDED_WORK_MS, label: String = "Added item work") {
+ Trace.beginSection(label)
+
+ // spin!
+ val endTime = System.nanoTime() + durationMs * 1_000_000
+ @Suppress("ControlFlowWithEmptyBody")
+ while (System.nanoTime() < endTime) {}
+
+ Trace.endSection()
+ }
+
+ val paintA = Paint().apply { setColor(Color.LTGRAY) }
+ val paintB = Paint().apply { setColor(Color.WHITE) }
+
+ override fun onDraw(canvas: Canvas) {
+ if (mode == FrameMode.WorkDuringEveryFrame) {
+ work()
+ }
+ super.onDraw(canvas)
+
+ work(durationMs = FRAME_BASELINE_WORK_MS, "Baseline work frame $remainingFrames")
+
+ // small rect to reduce flicker
+ canvas.drawRect(
+ 0f, 0f, 200f, 200f,
+ if (remainingFrames % 2 == 0) paintA else paintB
+ )
+
+ if (remainingFrames >= 1) {
+ remainingFrames--
+ invalidate()
+
+ if (mode == FrameMode.PrefetchEveryFrame ||
+ (mode == FrameMode.PrefetchSomeFrames && remainingFrames % 5 == 0)) {
+ this.post {
+ work()
+ }
+ }
+ }
+ }
+}
+
+class FrameExperimentActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val frameModeId = intent.getIntExtra(EXTRA_FRAME_MODE, defaultMode.id)
+ val frameMode = FrameMode.values().first { it.id == frameModeId }
+
+ setContentView(
+ FrameExperimentView(this, frameMode)
+ )
+ }
+
+ companion object {
+ const val EXTRA_FRAME_MODE = "FRAME_MODE"
+ private val defaultMode = FrameMode.Fast
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
new file mode 100644
index 0000000..36e8335
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingGfxInfoMetric
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.createCompilationParams
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Benchmark for experimenting with synthetic frame patterns/durations and how
+ * they show up in metrics
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FrameExperimentBenchmark {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ // NOTE: Keep in sync with FrameExperimentActivity!!
+ private enum class FrameMode(val id: Int) {
+ Fast(0),
+ PrefetchEveryFrame(1),
+ WorkDuringEveryFrame(2),
+ PrefetchSomeFrames(3),
+ }
+
+ @Test
+ fun fast() = benchmark(FrameMode.Fast)
+ @Test
+ fun prefetchEveryFrame() = benchmark(FrameMode.PrefetchEveryFrame)
+ @Test
+ fun workDuringEveryFrame() = benchmark(FrameMode.WorkDuringEveryFrame)
+ @Test
+ fun prefetchSomeFrames() = benchmark(FrameMode.PrefetchSomeFrames)
+
+ @OptIn(ExperimentalMetricApi::class)
+ private fun benchmark(mode: FrameMode) {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
+ compilationMode = CompilationMode.DEFAULT,
+ iterations = 1,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ intent.putExtra("FRAME_MODE", mode.id)
+ startActivityAndWait(intent)
+ }
+ ) {
+ device.click(device.displayWidth / 2, device.displayHeight / 2)
+ Thread.sleep(4_000) // empirically enough to produce expected frames
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+ private const val ACTION =
+ "androidx.compose.integration.macrobenchmark.target.FRAME_EXPERIMENT_ACTIVITY"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index 67bb0c3..b84882b 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -28,12 +28,13 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.impl.source.PsiClassReferenceType
+import org.jetbrains.kotlin.psi.KtCallElement
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.ULambdaExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.UUnknownExpression
import org.jetbrains.uast.UVariable
-import org.jetbrains.uast.kotlin.KotlinUBlockExpression
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
-import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
-import org.jetbrains.uast.kotlin.UnknownKotlinExpression
import org.jetbrains.uast.skipParenthesizedExprDown
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
@@ -81,14 +82,17 @@
class UnnecessaryLambdaCreationHandler(private val context: JavaContext) : UElementHandler() {
override fun visitLambdaExpression(node: ULambdaExpression) {
- val expressions = (node.body as? KotlinUBlockExpression)?.expressions ?: return
+ val expressions = (node.body as? UBlockExpression)?.expressions ?: return
if (expressions.size != 1) return
val expression = when (val expr = expressions.first().skipParenthesizedExprDown()) {
- is KotlinUFunctionCallExpression -> expr
- is KotlinUImplicitReturnExpression ->
- expr.returnExpression as? KotlinUFunctionCallExpression
+ is UCallExpression -> expr
+ is UReturnExpression -> {
+ if (expr.sourcePsi == null) { // implicit return
+ expr.returnExpression as? UCallExpression
+ } else null
+ }
else -> null
} ?: return
@@ -97,7 +101,7 @@
// We want to make sure this lambda is being invoked in the context of a function call,
// and not as a property assignment - so we cast to KotlinUFunctionCallExpression to
// filter out such cases.
- val parentExpression = (node.uastParent as? KotlinUFunctionCallExpression) ?: return
+ val parentExpression = (node.uastParent as? UCallExpression) ?: return
// If we can't resolve the parent call, then the parent function is defined in a
// separate module, so we don't have the right metadata - and hence the argumentType
@@ -120,7 +124,8 @@
val expectedComposable = node.isComposable
// Try and get the UElement for the source of the lambda
- val resolvedLambdaSource = expression.sourcePsi.calleeExpression?.toUElement()
+ val sourcePsi = expression.sourcePsi as? KtCallElement ?: return
+ val resolvedLambdaSource = sourcePsi.calleeExpression?.toUElement()
?.tryResolve()?.toUElement()
// Sometimes the above will give us a method (representing the getter for a
// property), when the actual backing element is a property. Going to the source
@@ -132,7 +137,7 @@
// TODO: if the resolved source is a parameter in a local function, it
// incorrectly returns an UnknownKotlinExpression instead of a UParameter
// https://youtrack.jetbrains.com/issue/KTIJ-19125
- is UnknownKotlinExpression -> return
+ is UUnknownExpression -> return
else -> error(parentExpression.asSourceString())
}
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 026f271..1bdfdec 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -40,12 +40,12 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- api(project(":compose:foundation:foundation"))
- api(project(":compose:runtime:runtime"))
+ api("androidx.compose.foundation:foundation:1.6.0")
+ api("androidx.compose.runtime:runtime:1.6.0")
implementation("androidx.collection:collection:1.4.0")
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.animation:animation:1.6.0")
+ implementation("androidx.compose.ui:ui-util:1.6.0")
}
}
@@ -63,10 +63,6 @@
skikoMain {
dependsOn(commonMain)
dependencies {
- api(project(":compose:foundation:foundation"))
- api(project(":compose:runtime:runtime"))
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:ui:ui-util"))
}
}
@@ -93,7 +89,7 @@
dependsOn(jvmTest)
dependencies {
implementation(project(":compose:test-utils"))
-
+ implementation(project(":compose:foundation:foundation"))
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index df79395..7234f90 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -41,17 +41,17 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- api(project(":compose:animation:animation-core"))
+ api("androidx.compose.animation:animation-core:1.6.0")
api(project(":compose:foundation:foundation"))
api(project(":compose:material:material-icons-core"))
api(project(":compose:material:material-ripple"))
- api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-text"))
+ api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.ui:ui:1.6.0")
+ api("androidx.compose.ui:ui-text:1.6.0")
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.animation:animation:1.6.0")
+ implementation("androidx.compose.foundation:foundation-layout:1.6.0")
+ implementation("androidx.compose.ui:ui-util:1.6.0")
}
}
@@ -69,13 +69,6 @@
skikoMain {
dependsOn(commonMain)
dependencies {
- api(project(":compose:animation:animation-core"))
- api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-text"))
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:ui:ui-util"))
}
}
@@ -111,6 +104,8 @@
dependencies {
implementation(project(":compose:material:material:material-samples"))
implementation(project(":compose:test-utils"))
+ implementation("androidx.compose.ui:ui-test:1.6.0")
+ implementation("androidx.compose.ui:ui-test-junit4:1.6.0")
implementation(project(":test:screenshot:screenshot"))
implementation(libs.testRules)
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
index 3626a13e..3787c6c 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
@@ -149,7 +149,7 @@
if (isMpp) {
CoreIconGenerationTask.register(project, null)
} else {
- libraryExtension.libraryVariants.all { variant ->
+ libraryExtension.libraryVariants.configureEach { variant ->
CoreIconGenerationTask.register(project, variant)
}
}
@@ -165,7 +165,7 @@
project: Project,
libraryExtension: LibraryExtension
) {
- libraryExtension.libraryVariants.all { variant ->
+ libraryExtension.libraryVariants.configureEach { variant ->
if (variant.name == "release") {
ExtendedIconGenerationTask.register(project, variant)
}
@@ -189,7 +189,7 @@
project: Project,
libraryExtension: LibraryExtension
) {
- libraryExtension.testVariants.all { variant ->
+ libraryExtension.testVariants.configureEach { variant ->
IconTestingGenerationTask.register(project, variant)
}
}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
index 21b602a..6b2b165 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
@@ -19,8 +19,8 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.mutableIntStateOf
diff --git a/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md b/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
index 645b7b6..92347e5 100644
--- a/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
+++ b/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
@@ -2,4 +2,4 @@
Compose Material Adaptive Navigation Suite
-# Package androidx.compose.material3.adaptive.navigation.suite
\ No newline at end of file
+# Package androidx.compose.material3.adaptive.navigationsuite
\ No newline at end of file
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index a87d1e4..a4d597b 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -1,10 +1,10 @@
// Signature format: 4.0
-package androidx.compose.material3.adaptive.navigation.suite {
+package androidx.compose.material3.adaptive.navigationsuite {
@SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
method public long getNavigationBarContainerColor();
method public long getNavigationBarContentColor();
method public long getNavigationDrawerContainerColor();
@@ -19,12 +19,12 @@
property public final long navigationRailContentColor;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
- method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteDefaults INSTANCE;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
method public androidx.compose.material3.NavigationRailItemColors getNavigationRailItemColors();
@@ -33,23 +33,23 @@
property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults INSTANCE;
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults INSTANCE;
}
public final class NavigationSuiteScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
- method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+ method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType.Companion Companion;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
}
public static final class NavigationSuiteType.Companion {
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index a87d1e4..a4d597b 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -1,10 +1,10 @@
// Signature format: 4.0
-package androidx.compose.material3.adaptive.navigation.suite {
+package androidx.compose.material3.adaptive.navigationsuite {
@SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
method public long getNavigationBarContainerColor();
method public long getNavigationBarContentColor();
method public long getNavigationDrawerContainerColor();
@@ -19,12 +19,12 @@
property public final long navigationRailContentColor;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
- method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteDefaults INSTANCE;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
method public androidx.compose.material3.NavigationRailItemColors getNavigationRailItemColors();
@@ -33,23 +33,23 @@
property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults INSTANCE;
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults INSTANCE;
}
public final class NavigationSuiteScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
- method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+ method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
- field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType.Companion Companion;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+ field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
}
public static final class NavigationSuiteType.Companion {
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index ae2c546..249b82a 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -45,8 +45,8 @@
implementation(libs.kotlinStdlibCommon)
implementation(project(":compose:material3:material3"))
implementation(project(":compose:material3:material3-adaptive"))
- implementation(project(":compose:material3:material3-window-size-class"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
+ implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
}
}
@@ -108,7 +108,7 @@
}
android {
- namespace "androidx.compose.material3.adaptive.navigation.suite"
+ namespace "androidx.compose.material3.adaptive.navigationsuite"
}
androidx {
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
index 653909e..088b2b6 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
@@ -36,13 +36,13 @@
compileOnly(project(":annotation:annotation-sampled"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:foundation:foundation-layout"))
+ implementation("androidx.compose.foundation:foundation:1.6.0-rc01")
+ implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
implementation(project(":compose:material3:material3"))
implementation(project(":compose:material3:material3-adaptive"))
implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
implementation(project(":compose:material3:material3-window-size-class"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
@@ -57,5 +57,5 @@
}
android {
- namespace "androidx.compose.material3.adaptive.navigation.suite.samples"
+ namespace "androidx.compose.material3.adaptive.navigationsuite.samples"
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
similarity index 89%
rename from compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt
rename to compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
index ed9664a..0b5cf96 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite.samples
+package androidx.compose.material3.adaptive.navigationsuite.samples
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.padding
@@ -24,10 +24,10 @@
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
-import androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffold
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType
+import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 7cbf1c3..737aa7f 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
similarity index 96%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
index 3dffee7..df34599 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
similarity index 94%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
index be34e16..4af953a 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 745cccb..c349a8e 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
similarity index 93%
rename from compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
index 054c8e5..44f0baa 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
@RequiresOptIn(
"This material3-adaptive-navigation-suite API is experimental and is likely to" +
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index 4787981..b0c6f78 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -65,9 +65,9 @@
* navigation component on the screen according to the current [NavigationSuiteType].
*
* Example default usage:
- * @sample androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldSample
+ * @sample androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
* Example custom configuration usage:
- * @sample androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldCustomConfigSample
+ * @sample androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldCustomConfigSample
*
* @param navigationSuiteItems the navigation items to be displayed
* @param modifier the [Modifier] to be applied to the navigation suite scaffold
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt b/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
similarity index 95%
rename from compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
index 913dc42..e068dd4 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index 2766f8b..95f48e9 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -42,10 +42,10 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- api(project(":compose:foundation:foundation"))
- api(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:material3:material3-window-size-class"))
- implementation(project(":compose:ui:ui-util"))
+ api("androidx.compose.foundation:foundation:1.6.0-rc01")
+ implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
+ implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
+ implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
}
}
diff --git a/compose/material3/material3-adaptive/samples/build.gradle b/compose/material3/material3-adaptive/samples/build.gradle
index 110964e..1c8ac3c 100644
--- a/compose/material3/material3-adaptive/samples/build.gradle
+++ b/compose/material3/material3-adaptive/samples/build.gradle
@@ -36,12 +36,12 @@
compileOnly(project(":annotation:annotation-sampled"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:foundation:foundation-layout"))
+ implementation("androidx.compose.foundation:foundation:1.6.0-rc01")
+ implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
implementation(project(":compose:material3:material3"))
implementation(project(":compose:material3:material3-adaptive"))
implementation(project(":compose:material3:material3-window-size-class"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 13c0af1..3ddad4c 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -21,8 +21,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldCustomConfigSample
-import androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldSample
+import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldCustomConfigSample
+import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
import androidx.compose.material3.catalog.library.util.AdaptiveNavigationSuiteSampleSourceUrl
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
index bbbed64..da46a8a 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
@@ -62,7 +62,10 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipe
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -579,6 +582,78 @@
.assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.DropdownList))
}
+ @Test
+ fun edm_positionProvider() {
+ val topWindowInsets = 50
+ val density = Density(1f)
+ val anchorSize = IntSize(width = 200, height = 100)
+ val popupSize = IntSize(width = 200, height = 340)
+ val windowSize = IntSize(width = 500, height = 500)
+ val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
+ val layoutDirection = LayoutDirection.Ltr
+
+ val edmPositionProvider = ExposedDropdownMenuPositionProvider(
+ density = density,
+ topWindowInsets = topWindowInsets,
+ )
+
+ // typical case
+ assertThat(
+ edmPositionProvider.calculatePosition(
+ anchorBounds = IntRect(
+ size = anchorSize,
+ offset = IntOffset(0, 0),
+ ),
+ windowSize = windowSize,
+ popupContentSize = popupSize,
+ layoutDirection = layoutDirection,
+ )
+ ).isEqualTo(IntOffset(0, anchorSize.height))
+
+ // off-screen (above)
+ assertThat(
+ edmPositionProvider.calculatePosition(
+ anchorBounds = IntRect(
+ size = anchorSize,
+ offset = IntOffset(0, -150),
+ ),
+ windowSize = windowSize,
+ popupContentSize = popupSize,
+ layoutDirection = layoutDirection,
+ )
+ ).isEqualTo(IntOffset(0, verticalMargin))
+
+ // interacting with window insets
+ assertThat(
+ edmPositionProvider.calculatePosition(
+ anchorBounds = IntRect(
+ size = anchorSize,
+ // If it weren't for topWindowInsets allowance,
+ // the menu would be considered "off-screen"
+ offset = IntOffset(0, 100),
+ ),
+ windowSize = windowSize,
+ popupContentSize = popupSize,
+ layoutDirection = layoutDirection,
+ )
+ ).isEqualTo(IntOffset(0, 100 + anchorSize.height))
+
+ // off-screen (below)
+ assertThat(
+ edmPositionProvider.calculatePosition(
+ anchorBounds = IntRect(
+ size = anchorSize,
+ offset = IntOffset(0, windowSize.height + 100),
+ ),
+ windowSize = windowSize,
+ popupContentSize = popupSize,
+ layoutDirection = layoutDirection,
+ )
+ ).isEqualTo(
+ IntOffset(0, windowSize.height + topWindowInsets - verticalMargin - popupSize.height)
+ )
+ }
+
@Composable
fun ExposedDropdownMenuForTest(
expanded: Boolean,
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
index cb83585..ff4fa9e 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
@@ -27,6 +27,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
@@ -37,6 +39,7 @@
import androidx.compose.material3.tokens.OutlinedAutocompleteTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -66,11 +69,15 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.window.PopupPositionProvider
import kotlin.math.max
import kotlin.math.roundToInt
@@ -282,10 +289,11 @@
if (expandedState.currentState || expandedState.targetState) {
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
val density = LocalDensity.current
- val popupPositionProvider = remember(density) {
- DropdownMenuPositionProvider(
- DpOffset.Zero,
- density,
+ val topWindowInsets = WindowInsets.statusBars.getTop(density)
+ val popupPositionProvider = remember(density, topWindowInsets) {
+ ExposedDropdownMenuPositionProvider(
+ density = density,
+ topWindowInsets = topWindowInsets,
) { anchorBounds, menuBounds ->
transformOriginState.value = calculateTransformOrigin(anchorBounds, menuBounds)
}
@@ -1037,6 +1045,91 @@
)
}
+@Immutable
+internal data class ExposedDropdownMenuPositionProvider(
+ val density: Density,
+ val topWindowInsets: Int,
+ val verticalMargin: Int = with(density) { MenuVerticalMargin.roundToPx() },
+ val onPositionCalculated: (anchorBounds: IntRect, menuBounds: IntRect) -> Unit = { _, _ -> }
+) : PopupPositionProvider {
+ // Horizontal position
+ private val startToAnchorStart = MenuPosition.startToAnchorStart()
+ private val endToAnchorEnd = MenuPosition.endToAnchorEnd()
+ private val leftToWindowLeft = MenuPosition.leftToWindowLeft()
+ private val rightToWindowRight = MenuPosition.rightToWindowRight()
+
+ // Vertical position
+ private val topToAnchorBottom = MenuPosition.topToAnchorBottom()
+ private val bottomToAnchorTop = MenuPosition.bottomToAnchorTop()
+ private val topToWindowTop = MenuPosition.topToWindowTop(margin = verticalMargin)
+ private val bottomToWindowBottom = MenuPosition.bottomToWindowBottom(margin = verticalMargin)
+
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset {
+ // TODO(b/256233441): Popup fails to account for window insets so we do it here instead
+ @Suppress("NAME_SHADOWING")
+ val windowSize = IntSize(windowSize.width, windowSize.height + topWindowInsets)
+
+ val xCandidates = listOf(
+ startToAnchorStart,
+ endToAnchorEnd,
+ if (anchorBounds.center.x < windowSize.width / 2) {
+ leftToWindowLeft
+ } else {
+ rightToWindowRight
+ }
+ )
+ var x = 0
+ for (index in xCandidates.indices) {
+ val xCandidate = xCandidates[index].position(
+ anchorBounds = anchorBounds,
+ windowSize = windowSize,
+ menuWidth = popupContentSize.width,
+ layoutDirection = layoutDirection
+ )
+ if (index == xCandidates.lastIndex ||
+ (xCandidate >= 0 && xCandidate + popupContentSize.width <= windowSize.width)) {
+ x = xCandidate
+ break
+ }
+ }
+
+ val yCandidates = listOf(
+ topToAnchorBottom,
+ bottomToAnchorTop,
+ if (anchorBounds.center.y < windowSize.height / 2) {
+ topToWindowTop
+ } else {
+ bottomToWindowBottom
+ }
+ )
+ var y = 0
+ for (index in yCandidates.indices) {
+ val yCandidate = yCandidates[index].position(
+ anchorBounds = anchorBounds,
+ windowSize = windowSize,
+ menuHeight = popupContentSize.height
+ )
+ if (index == yCandidates.lastIndex ||
+ (yCandidate >= 0 && yCandidate + popupContentSize.height <= windowSize.height)) {
+ y = yCandidate
+ break
+ }
+ }
+
+ val menuOffset = IntOffset(x, y)
+ onPositionCalculated(
+ /* anchorBounds = */anchorBounds,
+ /* menuBounds = */IntRect(offset = menuOffset, size = popupContentSize)
+ )
+ return menuOffset
+ }
+}
+
private fun Modifier.expandable(
expanded: Boolean,
onExpandedChange: () -> Unit,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 4a9810e..5826971 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -118,7 +118,8 @@
textColor = fromToken(MenuTokens.ListItemLabelTextColor),
leadingIconColor = fromToken(MenuTokens.ListItemLeadingIconColor),
trailingIconColor = fromToken(MenuTokens.ListItemTrailingIconColor),
- disabledTextColor = fromToken(MenuTokens.ListItemDisabledLabelTextColor),
+ disabledTextColor = fromToken(MenuTokens.ListItemDisabledLabelTextColor)
+ .copy(alpha = MenuTokens.ListItemDisabledLabelTextOpacity),
disabledLeadingIconColor = fromToken(MenuTokens.ListItemDisabledLeadingIconColor)
.copy(alpha = MenuTokens.ListItemDisabledLeadingIconOpacity),
disabledTrailingIconColor = fromToken(MenuTokens.ListItemDisabledTrailingIconColor)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
index 5853b39..476b703 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
@@ -16,6 +16,8 @@
package androidx.compose.material3
+import androidx.compose.material3.MenuPosition.Horizontal
+import androidx.compose.material3.MenuPosition.Vertical
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.AbsoluteAlignment
@@ -26,8 +28,6 @@
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastMap
import androidx.compose.ui.window.PopupPositionProvider
/**
@@ -368,17 +368,21 @@
} else {
rightToWindowRight
}
- ).fastMap {
- it.position(
+ )
+ var x = 0
+ for (index in xCandidates.indices) {
+ val xCandidate = xCandidates[index].position(
anchorBounds = anchorBounds,
windowSize = windowSize,
menuWidth = popupContentSize.width,
layoutDirection = layoutDirection
)
+ if (index == xCandidates.lastIndex ||
+ (xCandidate >= 0 && xCandidate + popupContentSize.width <= windowSize.width)) {
+ x = xCandidate
+ break
+ }
}
- val x = xCandidates.fastFirstOrNull {
- it >= 0 && it + popupContentSize.width <= windowSize.width
- } ?: xCandidates.last()
val yCandidates = listOf(
topToAnchorBottom,
@@ -389,17 +393,21 @@
} else {
bottomToWindowBottom
}
- ).fastMap {
- it.position(
+ )
+ var y = 0
+ for (index in yCandidates.indices) {
+ val yCandidate = yCandidates[index].position(
anchorBounds = anchorBounds,
windowSize = windowSize,
menuHeight = popupContentSize.height
)
+ if (index == yCandidates.lastIndex ||
+ (yCandidate >= verticalMargin &&
+ yCandidate + popupContentSize.height <= windowSize.height - verticalMargin)) {
+ y = yCandidate
+ break
+ }
}
- val y = yCandidates.fastFirstOrNull {
- it >= verticalMargin &&
- it + popupContentSize.height <= windowSize.height - verticalMargin
- } ?: yCandidates.last()
val menuOffset = IntOffset(x, y)
onPositionCalculated(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index dd2f455..f27a6b6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -102,7 +102,7 @@
val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
val isOutOfKeylineBounds = keylineBefore == keylineAfter
- return this then layout { measurable, constraints ->
+ return layout { measurable, constraints ->
// Force the item to use the strategy's itemMainAxisSize along its main axis
val mainAxisSize = strategy.itemMainAxisSize
val itemConstraints = if (isVertical) {
@@ -125,7 +125,7 @@
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
- } then graphicsLayer {
+ }.graphicsLayer {
// Clip the item
clip = true
shape = object : Shape {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
index 4600350..177e18c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
@@ -57,7 +57,8 @@
| | [DatePickerDialog] | M3 date picker embeeded in dialog |
| | [DateRangePicker] | M3 date range picker |
| **Dialogs** | [AlertDialog] | M3 basic dialog |
-| **Dividers** | [Divider] | M3 divider |
+| **Dividers** | [HorizontalDivider] | M3 horizontal divider |
+| | [VerticalDivider] | M3 vertical divider |
| **Extended FAB** | [ExtendedFloatingActionButton] | M3 extended FAB |
| **FAB** | [FloatingActionButton] | M3 FAB |
| | [SmallFloatingActionButton] | M3 small FAB |
@@ -87,6 +88,7 @@
| | [NavigationRailItem] | M3 navigation rail item |
| **Progress indicators** | [LinearProgressIndicator] | M3 linear progress indicator |
| | [CircularProgressIndicator] | M3 circular progress indicator |
+| **Pull refresh ** | [PullToRefreshContainer] | M3 pull to refresh indicator |
| **Radio button** | [RadioButton] | M3 radio button |
| **Search Bar** | [SearchBar] | M3 search bar |
| | [DockedSearchBar] | M3 docked search bar |
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
index b3763a5..aeb86fe 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
@@ -40,7 +40,7 @@
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.java.JavaUCallExpression
+import org.jetbrains.uast.kotlin.isKotlin
import org.jetbrains.uast.skipParenthesizedExprDown
/**
@@ -71,7 +71,7 @@
override fun getApplicableMethodNames() = listOf(Names.Runtime.MutableStateOf.shortName)
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (node is JavaUCallExpression) return
+ if (!isKotlin(node.lang)) return
if (!method.isInPackageName(Names.Runtime.PackageName)) return
val replacement = getSuggestedReplacementName(node) ?: return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index 762d8f1..694aff4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -203,7 +203,7 @@
}
override fun up() {
- check(stack.isNotEmpty()) { "empty stack" }
+ checkPrecondition(stack.isNotEmpty()) { "empty stack" }
current = stack.removeAt(stack.size - 1)
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 283b6e3..ee4f5ad 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
* IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
* `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
*/
- const val version: Int = 12000
+ const val version: Int = 12100
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 69d2ce2..83aaba4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -35,6 +35,8 @@
import androidx.compose.runtime.snapshots.fastToSet
import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.LocalInspectionTables
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
private class GroupInfo(
@@ -100,7 +102,7 @@
var groupIndex: Int = 0
init {
- require(startIndex >= 0) { "Invalid start index" }
+ requirePrecondition(startIndex >= 0) { "Invalid start index" }
}
private val usedKeys = mutableListOf<KeyInfo>()
@@ -1703,7 +1705,7 @@
}
fun endReuseFromRoot() {
- require(!isComposing && reusingGroup == rootKey) {
+ requirePrecondition(!isComposing && reusingGroup == rootKey) {
"Cannot disable reuse from root if it was caused by other groups"
}
reusingGroup = -1
@@ -4313,10 +4315,14 @@
internal class ComposeRuntimeError(override val message: String) : IllegalStateException()
-internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> Any) {
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> String) {
+ contract {
+ returns() implies value
+ }
if (!value) {
- val message = lazyMessage()
- composeRuntimeError(message.toString())
+ composeImmediateRuntimeError(lazyMessage())
}
}
@@ -4330,6 +4336,16 @@
)
}
+// Unit variant of composeRuntimeError() so the call site doesn't add 3 extra
+// instructions to throw a KotlinNothingValueException
+internal fun composeImmediateRuntimeError(message: String) {
+ throw ComposeRuntimeError(
+ "Compose Runtime internal error. Unexpected or incorrect use of the Compose " +
+ "internal runtime API ($message). Please report to Google or use " +
+ "https://goo.gle/compose-feedback"
+ )
+}
+
private val InvalidationLocationAscending = Comparator<Invalidation> { i1, i2 ->
i1.location.compareTo(i2.location)
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index a7a6180..4bac17f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -639,7 +639,7 @@
}
private fun composeInitial(content: @Composable () -> Unit) {
- check(!disposed) { "The composition is disposed" }
+ checkPrecondition(!disposed) { "The composition is disposed" }
this.composable = content
parent.composeInitial(this, composable)
}
@@ -742,7 +742,7 @@
override fun dispose() {
synchronized(lock) {
- check(!composer.isComposing) {
+ checkPrecondition(!composer.isComposing) {
"Composition is disposed while composing. If dispose is triggered by a call in " +
"@Composable function, consider wrapping it with SideEffect block."
}
@@ -1206,7 +1206,7 @@
val scopes = slotTable.slots.mapNotNull { it as? RecomposeScopeImpl }
scopes.fastForEach { scope ->
scope.anchor?.let { anchor ->
- check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
+ checkPrecondition(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
val dataIndex = slotTable.slots.indexOf(scope)
"Misaligned anchor $anchor in scope $scope encountered, scope found at " +
"$dataIndex"
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt
new file mode 100644
index 0000000..90aa989
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.runtime
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+// This function exists so we do *not* inline the throw. It keeps
+// the call site much smaller and since it's the slow path anyway,
+// we don't mind the extra function call
+internal fun throwIllegalArgumentException(message: String) {
+ throw IllegalArgumentException(message)
+}
+
+// Like Kotlin's require() but without the .toString() call
+@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's require()
+@OptIn(ExperimentalContracts::class)
+internal inline fun requirePrecondition(value: Boolean, lazyMessage: () -> String) {
+ contract {
+ returns() implies value
+ }
+ if (!value) {
+ throwIllegalArgumentException(lazyMessage())
+ }
+}
+
+// See above
+internal fun throwIllegalStateException(message: String) {
+ throw IllegalStateException(message)
+}
+
+// Like Kotlin's check() but without the .toString() call
+@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's check()
+@OptIn(ExperimentalContracts::class)
+internal inline fun checkPrecondition(value: Boolean, lazyMessage: () -> String) {
+ contract {
+ returns() implies value
+ }
+ if (!value) {
+ throwIllegalStateException(lazyMessage())
+ }
+}
+
+@Suppress("BanInlineOptIn", "NOTHING_TO_INLINE")
+@OptIn(ExperimentalContracts::class)
+internal inline fun checkPrecondition(value: Boolean) {
+ contract {
+ returns() implies value
+ }
+ if (!value) {
+ throwIllegalStateException("Check failed.")
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 144b1fb..910a9a1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -852,7 +852,7 @@
suspend fun runRecomposeConcurrentlyAndApplyChanges(
recomposeCoroutineContext: CoroutineContext
) = recompositionRunner { parentFrameClock ->
- require(recomposeCoroutineContext[Job] == null) {
+ requirePrecondition(recomposeCoroutineContext[Job] == null) {
"recomposeCoroutineContext may not contain a Job; found " +
recomposeCoroutineContext[Job]
}
@@ -1559,7 +1559,7 @@
* available up until this point. (Synchronizing access to that data is up to the caller.)
*/
fun takeFrameRequestLocked() {
- check(pendingFrameContinuation === FramePending) { "frame not pending" }
+ checkPrecondition(pendingFrameContinuation === FramePending) { "frame not pending" }
pendingFrameContinuation = null
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index b207499..2177e18 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -218,7 +218,9 @@
*/
fun anchor(index: Int): Anchor {
runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead" }
- require(index in 0 until groupsSize) { "Parameter index is out of range" }
+ requirePrecondition(index in 0 until groupsSize) {
+ "Parameter index is out of range"
+ }
return anchors.getOrAdd(index, groupsSize) {
Anchor(index)
}
@@ -240,7 +242,7 @@
*/
fun anchorIndex(anchor: Anchor): Int {
runtimeCheck(!writer) { "Use active SlotWriter to determine anchor location instead" }
- require(anchor.valid) { "Anchor refers to a group that was removed" }
+ requirePrecondition(anchor.valid) { "Anchor refers to a group that was removed" }
return anchor.location
}
@@ -301,7 +303,7 @@
sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
calledByMap: MutableIntObjectMap<MutableIntSet>?
) {
- require(writer.table === this && this.writer) { "Unexpected writer close()" }
+ requirePrecondition(writer.table === this && this.writer) { "Unexpected writer close()" }
this.writer = false
setTo(groups, groupsSize, slots, slotsSize, anchors, sourceInformationMap, calledByMap)
}
@@ -438,38 +440,38 @@
fun validateGroup(parent: Int, parentEnd: Int): Int {
val group = current++
val parentIndex = groups.parentAnchor(group)
- check(parentIndex == parent) {
+ checkPrecondition(parentIndex == parent) {
"Invalid parent index detected at $group, expected parent index to be $parent " +
"found $parentIndex"
}
val end = group + groups.groupSize(group)
- check(end <= groupsSize) {
+ checkPrecondition(end <= groupsSize) {
"A group extends past the end of the table at $group"
}
- check(end <= parentEnd) {
+ checkPrecondition(end <= parentEnd) {
"A group extends past its parent group at $group"
}
val dataStart = groups.dataAnchor(group)
val dataEnd = if (group >= groupsSize - 1) slotsSize else groups.dataAnchor(group + 1)
- check(dataEnd <= slots.size) {
+ checkPrecondition(dataEnd <= slots.size) {
"Slots for $group extend past the end of the slot table"
}
- check(dataStart <= dataEnd) {
+ checkPrecondition(dataStart <= dataEnd) {
"Invalid data anchor at $group"
}
val slotStart = groups.slotAnchor(group)
- check(slotStart <= dataEnd) {
+ checkPrecondition(slotStart <= dataEnd) {
"Slots start out of range at $group"
}
val minSlotsNeeded = (if (groups.isNode(group)) 1 else 0) +
(if (groups.hasObjectKey(group)) 1 else 0) +
(if (groups.hasAux(group)) 1 else 0)
- check(dataEnd - dataStart >= minSlotsNeeded) {
+ checkPrecondition(dataEnd - dataStart >= minSlotsNeeded) {
"Not enough slots added for group $group"
}
val isNode = groups.isNode(group)
- check(!isNode || slots[groups.nodeIndex(group)] != null) {
+ checkPrecondition(!isNode || slots[groups.nodeIndex(group)] != null) {
"No node recorded for a node group at $group"
}
var nodeCount = 0
@@ -478,17 +480,17 @@
}
val expectedNodeCount = groups.nodeCount(group)
val expectedSlotCount = groups.groupSize(group)
- check(expectedNodeCount == nodeCount) {
+ checkPrecondition(expectedNodeCount == nodeCount) {
"Incorrect node count detected at $group, " +
"expected $expectedNodeCount, received $nodeCount"
}
val actualSlotCount = current - group
- check(expectedSlotCount == actualSlotCount) {
+ checkPrecondition(expectedSlotCount == actualSlotCount) {
"Incorrect slot count detected at $group, expected $expectedSlotCount, received " +
"$actualSlotCount"
}
if (groups.containsAnyMark(group)) {
- check(group <= 0 || groups.containsMark(parent)) {
+ checkPrecondition(group <= 0 || groups.containsMark(parent)) {
"Expected group $parent to record it contains a mark because $group does"
}
}
@@ -500,14 +502,14 @@
while (current < groupsSize) {
validateGroup(-1, current + groups.groupSize(current))
}
- check(current == groupsSize) {
+ checkPrecondition(current == groupsSize) {
"Incomplete group at root $current expected to be $groupsSize"
}
}
// Verify that slot gap contains all nulls
for (index in slotsSize until slots.size) {
- check(slots[index] == null) {
+ checkPrecondition(slots[index] == null) {
"Non null value in the slot gap at index $index"
}
}
@@ -516,8 +518,10 @@
var lastLocation = -1
anchors.fastForEach { anchor ->
val location = anchor.toIndexFor(this)
- require(location in 0..groupsSize) { "Invalid anchor, location out of bound" }
- require(lastLocation < location) { "Anchor is out of order" }
+ requirePrecondition(location in 0..groupsSize) {
+ "Invalid anchor, location out of bound"
+ }
+ requirePrecondition(lastLocation < location) { "Anchor is out of order" }
lastLocation = location
}
@@ -526,10 +530,10 @@
group.groups?.fastForEach { item ->
when (item) {
is Anchor -> {
- require(item.valid) {
+ requirePrecondition(item.valid) {
"Source map contains invalid anchor"
}
- require(ownsAnchor(item)) {
+ requirePrecondition(ownsAnchor(item)) {
"Source map anchor is not owned by the slot table"
}
}
@@ -540,10 +544,10 @@
sourceInformationMap?.let { sourceInformationMap ->
for ((anchor, sourceGroup) in sourceInformationMap) {
- require(anchor.valid) {
+ requirePrecondition(anchor.valid) {
"Source map contains invalid anchor"
}
- require(ownsAnchor(anchor)) {
+ requirePrecondition(ownsAnchor(anchor)) {
"Source map anchor is not owned by the slot table"
}
verifySourceGroup(sourceGroup)
@@ -1061,7 +1065,7 @@
*/
fun parentOf(index: Int): Int {
@Suppress("ConvertTwoComparisonsToRangeCheck")
- require(index >= 0 && index < groupsSize) { "Invalid group index $index" }
+ requirePrecondition(index >= 0 && index < groupsSize) { "Invalid group index $index" }
return groups.parentAnchor(index)
}
@@ -1132,7 +1136,7 @@
* End reporting [Composer.Empty] for calls to [next] and [get],
*/
fun endEmpty() {
- require(emptyCount > 0) { "Unbalanced begin/end empty" }
+ requirePrecondition(emptyCount > 0) { "Unbalanced begin/end empty" }
emptyCount--
}
@@ -1152,7 +1156,9 @@
if (emptyCount <= 0) {
val parent = parent
val currentGroup = currentGroup
- require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" }
+ requirePrecondition(groups.parentAnchor(currentGroup) == parent) {
+ "Invalid slot table detected"
+ }
sourceInformationMap?.get(anchor(parent))?.reportGroup(table, currentGroup)
val currentSlotStack = currentSlotStack
val currentSlot = currentSlot
@@ -1177,7 +1183,7 @@
*/
fun startNode() {
if (emptyCount <= 0) {
- require(groups.isNode(currentGroup)) { "Expected a node group" }
+ requirePrecondition(groups.isNode(currentGroup)) { "Expected a node group" }
startGroup()
}
}
@@ -1696,7 +1702,7 @@
// scope inserted by a restart group and the lambda value in a composableLambda
// instance) so this is the only case currently supported.
val slotsToMove = currentSlot - auxIndex
- check(slotsToMove < 3) { "Moving more than two slot not supported" }
+ checkPrecondition(slotsToMove < 3) { "Moving more than two slot not supported" }
if (slotsToMove > 1) {
slots[auxAddress + 2] = slots[auxAddress + 1]
}
@@ -1875,7 +1881,7 @@
*/
fun advanceBy(amount: Int) {
runtimeCheck(amount >= 0) { "Cannot seek backwards" }
- check(insertCount <= 0) { "Cannot call seek() while inserting" }
+ checkPrecondition(insertCount <= 0) { "Cannot call seek() while inserting" }
if (amount == 0) return
val index = currentGroup + amount
@Suppress("ConvertTwoComparisonsToRangeCheck")
@@ -1917,7 +1923,7 @@
* Ends inserting.
*/
fun endInsert() {
- check(insertCount > 0) { "Unbalanced begin/end insert" }
+ checkPrecondition(insertCount > 0) { "Unbalanced begin/end insert" }
if (--insertCount == 0) {
runtimeCheck(nodeCountStack.size == startStack.size) {
"startGroup/endGroup mismatch while inserting"
@@ -3347,15 +3353,15 @@
val address = groupIndexToAddress(index)
val dataAnchor = groups.dataAnchor(address)
val dataIndex = groups.dataIndex(address)
- check(dataIndex >= previousDataIndex) {
+ checkPrecondition(dataIndex >= previousDataIndex) {
"Data index out of order at $index, previous = $previousDataIndex, current = " +
"$dataIndex"
}
- check(dataIndex <= slotsSize) {
+ checkPrecondition(dataIndex <= slotsSize) {
"Data index, $dataIndex, out of bound at $index"
}
if (dataAnchor < 0 && !ownerFound) {
- check(owner == index) {
+ checkPrecondition(owner == index) {
"Expected the slot gap owner to be $owner found gap at $index"
}
ownerFound = true
@@ -3371,7 +3377,7 @@
val capacity = capacity
for (groupAddress in 0 until gapStart) {
val parentAnchor = groups.parentAnchor(groupAddress)
- check(parentAnchor > parentAnchorPivot) {
+ checkPrecondition(parentAnchor > parentAnchorPivot) {
"Expected a start relative anchor at $groupAddress"
}
}
@@ -3379,11 +3385,11 @@
val parentAnchor = groups.parentAnchor(groupAddress)
val parentIndex = parentAnchorToIndex(parentAnchor)
if (parentIndex < gapStart) {
- check(parentAnchor > parentAnchorPivot) {
+ checkPrecondition(parentAnchor > parentAnchorPivot) {
"Expected a start relative anchor at $groupAddress"
}
} else {
- check(parentAnchor <= parentAnchorPivot) {
+ checkPrecondition(parentAnchor <= parentAnchorPivot) {
"Expected an end relative anchor at $groupAddress"
}
}
@@ -4111,8 +4117,8 @@
for (index in 0 until size / 2) {
val left = (index + 1) * 2 - 1
val right = (index + 1) * 2
- check(list[index] >= list[left])
- check(right >= size || list[index] >= list[right])
+ checkPrecondition(list[index] >= list[left])
+ checkPrecondition(right >= size || list[index] >= list[right])
}
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
index 11956a3..46fe6b5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
@@ -22,6 +22,8 @@
import androidx.compose.runtime.SlotWriter
import androidx.compose.runtime.changelist.Operation.IntParameter
import androidx.compose.runtime.changelist.Operation.ObjectParameter
+import androidx.compose.runtime.checkPrecondition
+import androidx.compose.runtime.requirePrecondition
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
@@ -141,7 +143,7 @@
* defines any arguments.
*/
fun push(operation: Operation) {
- require(operation.ints == 0 && operation.objects == 0) {
+ requirePrecondition(operation.ints == 0 && operation.objects == 0) {
"Cannot push $operation without arguments because it expects " +
"${operation.ints} ints and ${operation.objects} objects."
}
@@ -168,7 +170,7 @@
WriteScope(this).args()
// Verify all arguments were written to.
- check(
+ checkPrecondition(
pushedIntMask == createExpectedArgMask(operation.ints) &&
pushedObjectMask == createExpectedArgMask(operation.objects)
) {
@@ -330,7 +332,7 @@
fun setInt(parameter: IntParameter, value: Int) = with(stack) {
val mask = 0b1 shl parameter.offset
- check(pushedIntMask and mask == 0) {
+ checkPrecondition(pushedIntMask and mask == 0) {
"Already pushed argument ${operation.intParamName(parameter)}"
}
pushedIntMask = pushedIntMask or mask
@@ -339,7 +341,7 @@
fun <T> setObject(parameter: ObjectParameter<T>, value: T) = with(stack) {
val mask = 0b1 shl parameter.offset
- check(pushedObjectMask and mask == 0) {
+ checkPrecondition(pushedObjectMask and mask == 0) {
"Already pushed argument ${operation.objectParamName(parameter)}"
}
pushedObjectMask = pushedObjectMask or mask
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
index 1e95898..5c18dcd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
@@ -9,7 +9,7 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkElementIndex
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkPositionIndex
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
-import androidx.compose.runtime.external.kotlinx.collections.immutable.mutate
+import androidx.compose.runtime.requirePrecondition
/**
* Persistent vector made of a trie of leaf buffers entirely filled with [MAX_BUFFER_SIZE] elements and a tail having
@@ -27,7 +27,10 @@
private val rootShift: Int) : PersistentList<E>, AbstractPersistentList<E>() {
init {
- require(size > MAX_BUFFER_SIZE) { "Trie-based persistent vector should have at least ${MAX_BUFFER_SIZE + 1} elements, got $size" }
+ requirePrecondition(size > MAX_BUFFER_SIZE) {
+ "Trie-based persistent vector should have at least " +
+ "${MAX_BUFFER_SIZE + 1} elements, got $size"
+ }
assert(size - rootSize(size) <= tail.size.coerceAtMost(MAX_BUFFER_SIZE))
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
index ad6aa069..c2d4fd1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
@@ -10,6 +10,7 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkPositionIndex
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
+import androidx.compose.runtime.requirePrecondition
internal class PersistentVectorBuilder<E>(private var vector: PersistentList<E>,
private var vectorRoot: Array<Any?>?,
@@ -238,8 +239,8 @@
* Returns the resulting root.
*/
private fun pushBuffers(root: Array<Any?>?, rootSize: Int, shift: Int, buffersIterator: Iterator<Array<Any?>>): Array<Any?> {
- require(buffersIterator.hasNext()) {"invalid buffersIterator"}
- require(shift >= 0) {"negative shift"}
+ requirePrecondition(buffersIterator.hasNext()) {"invalid buffersIterator"}
+ requirePrecondition(shift >= 0) {"negative shift"}
if (shift == 0) {
return buffersIterator.next()
@@ -476,7 +477,7 @@
nullBuffers: Int,
nextBuffer: Array<Any?>
) {
- require(nullBuffers >= 1) { "requires at least one nullBuffer" }
+ requirePrecondition(nullBuffers >= 1) { "requires at least one nullBuffer" }
val firstBuffer = makeMutable(startBuffer)
buffers[0] = firstBuffer
@@ -742,7 +743,7 @@
* If the height of the root is bigger than needed to store [size] elements, it's decreased.
*/
private fun retainFirst(root: Array<Any?>, size: Int): Array<Any?>? {
- require(size and MAX_BUFFER_SIZE_MINUS_ONE == 0) { "invalid size" }
+ requirePrecondition(size and MAX_BUFFER_SIZE_MINUS_ONE == 0) { "invalid size" }
if (size == 0) {
rootShift = 0
@@ -765,7 +766,7 @@
* Used to prevent memory leaks after reusing nodes.
*/
private fun nullifyAfter(root: Array<Any?>, index: Int, shift: Int): Array<Any?> {
- require(shift >= 0) { "shift should be positive" }
+ requirePrecondition(shift >= 0) { "shift should be positive" }
if (shift == 0) {
// the `root` is a leaf buffer.
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
index f97c0ed..8165c7f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
@@ -9,6 +9,7 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.forEachOneBit
+import androidx.compose.runtime.checkPrecondition
internal const val MAX_BRANCHING_FACTOR = 32
@@ -636,7 +637,7 @@
// we can use this later to skip calling equals() again
}
@Suppress("ExceptionMessage")
- check(newNodeMap and newDataMap == 0)
+ checkPrecondition(newNodeMap and newDataMap == 0)
val mutableNode = when {
this.ownedBy == mutator.ownership && this.dataMap == newDataMap && this.nodeMap == newNodeMap -> this
else -> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 58759f2..d08defb 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -23,9 +23,11 @@
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.SnapshotThreadLocal
+import androidx.compose.runtime.checkPrecondition
import androidx.compose.runtime.collection.IdentityArraySet
import androidx.compose.runtime.currentThreadId
import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
+import androidx.compose.runtime.requirePrecondition
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
import androidx.compose.runtime.snapshots.SnapshotApplyResult.Failure
@@ -179,7 +181,7 @@
*/
@ExperimentalComposeApi
fun unsafeLeave(oldSnapshot: Snapshot?) {
- check(threadSnapshot.get() === this) {
+ checkPrecondition(threadSnapshot.get() === this) {
"Cannot leave snapshot; $this is not the current snapshot"
}
restoreCurrent(oldSnapshot)
@@ -273,7 +275,7 @@
}
internal fun validateNotDisposed() {
- require(!disposed) { "Cannot use a disposed snapshot" }
+ requirePrecondition(!disposed) { "Cannot use a disposed snapshot" }
}
internal fun releasePinnedSnapshotLocked() {
@@ -912,7 +914,7 @@
override fun nestedActivated(snapshot: Snapshot) { snapshots++ }
override fun nestedDeactivated(snapshot: Snapshot) {
- require(snapshots > 0) { "no pending nested snapshots" }
+ requirePrecondition(snapshots > 0) { "no pending nested snapshots" }
if (--snapshots == 0) {
if (!applied) {
abandon()
@@ -936,13 +938,13 @@
}
private fun validateNotApplied() {
- check(!applied) {
+ checkPrecondition(!applied) {
"Unsupported operation on a snapshot that has been applied"
}
}
private fun validateNotAppliedOrPinned() {
- check(!applied || isPinned) {
+ checkPrecondition(!applied || isPinned) {
"Unsupported operation on a disposed or applied snapshot"
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
index a932d6b..22fda4b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
@@ -19,8 +19,8 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
+import androidx.compose.runtime.requirePrecondition
import androidx.compose.runtime.synchronized
-import kotlin.jvm.JvmName
/**
* An implementation of [MutableList] that can be observed and snapshot. This is the result type
@@ -96,7 +96,7 @@
override fun listIterator(): MutableListIterator<T> = StateListIterator(this, 0)
override fun listIterator(index: Int): MutableListIterator<T> = StateListIterator(this, index)
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
- require(fromIndex in 0..toIndex && toIndex <= size) {
+ requirePrecondition(fromIndex in 0..toIndex && toIndex <= size) {
"fromIndex or toIndex are out of bounds"
}
return SubList(this, fromIndex, toIndex)
@@ -472,7 +472,7 @@
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
- require(fromIndex in 0..toIndex && toIndex <= size) {
+ requirePrecondition(fromIndex in 0..toIndex && toIndex <= size) {
"fromIndex or toIndex are out of bounds"
}
validateModification()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index c806f66..47c549d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -30,6 +30,7 @@
import androidx.compose.runtime.currentThreadId
import androidx.compose.runtime.currentThreadName
import androidx.compose.runtime.observeDerivedStateRecalculations
+import androidx.compose.runtime.requirePrecondition
import androidx.compose.runtime.structuralEqualityPolicy
/**
@@ -238,7 +239,7 @@
val oldThreadId = currentMapThreadId
if (oldThreadId != -1L) {
- require(oldThreadId == currentThreadId()) {
+ requirePrecondition(oldThreadId == currentThreadId()) {
"Detected multithreaded access to SnapshotStateObserver: " +
"previousThreadId=$oldThreadId), " +
"currentThread={id=${currentThreadId()}, name=${currentThreadName()}}. " +
diff --git a/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt b/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
index 8e5e6e3..f4fb0e4 100644
--- a/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
+++ b/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
@@ -31,8 +31,9 @@
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
import java.util.EnumSet
+import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.kotlin.KotlinULiteralExpression
+import org.jetbrains.uast.ULiteralExpression
/**
* [Detector] that checks hex Color definitions to ensure that they provide values for all four
@@ -49,8 +50,8 @@
if (node.valueArgumentCount == 1) {
val argument = node.valueArguments.first()
// Ignore non-literal expressions
- if (argument !is KotlinULiteralExpression) return
- val argumentText = argument.sourcePsi.text ?: return
+ if (argument !is ULiteralExpression) return
+ val argumentText = (argument.sourcePsi as? KtConstantExpression)?.text ?: return
val hexPrefix = "0x"
val hexIndex = argumentText.indexOf(hexPrefix, ignoreCase = true)
// Ignore if this isn't a hex value
diff --git a/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt b/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
index 2e027c8..417fea7 100644
--- a/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
+++ b/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
@@ -36,3 +36,4 @@
target_include_directories(compose_inspection_jni
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/external_jvmti)
target_link_libraries(compose_inspection_jni ${android-lib} ${log-lib})
+target_link_options(compose_inspection_jni PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/compose/ui/ui-lint/build.gradle b/compose/ui/ui-lint/build.gradle
index debfd8e..acb5164 100644
--- a/compose/ui/ui-lint/build.gradle
+++ b/compose/ui/ui-lint/build.gradle
@@ -32,7 +32,7 @@
BundleInsideHelper.forInsideLintJar(project)
dependencies {
- compileOnly(libs.androidLintMinComposeApi)
+ compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
bundleInside(project(":compose:lint:common"))
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
index 6911407..4e4b0cd 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
@@ -30,11 +30,11 @@
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import java.util.EnumSet
+import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
+import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
-import org.jetbrains.uast.kotlin.KotlinULambdaExpression
@Suppress("UnstableApiUsage")
class SuspiciousCompositionLocalModifierReadDetector : Detector(), SourceCodeScanner {
@@ -76,7 +76,7 @@
}
}
return
- } else if (node is KotlinULambdaExpression.Body) {
+ } else if (node is UBlockExpression && node.uastParent is ULambdaExpression) {
return
}
@@ -90,7 +90,7 @@
) {
if (node == null) {
return
- } else if (node is KotlinUFunctionCallExpression && node.isLazyDelegate()) {
+ } else if (node is UCallExpression && node.isLazyDelegate()) {
report(context, usage) { localBeingRead ->
"Reading $localBeingRead lazily will only access the CompositionLocal's value " +
"once. To be notified of the latest value of the CompositionLocal, read " +
@@ -121,7 +121,7 @@
this?.implementsListTypes
?.any { it.canonicalText == ClConsumerModifierNode } == true
- private fun KotlinUFunctionCallExpression.isLazyDelegate(): Boolean =
+ private fun UCallExpression.isLazyDelegate(): Boolean =
resolve()?.run { isInPackageName(Package("kotlin")) && name == "lazy" } == true
companion object {
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.kt
new file mode 100644
index 0000000..c2d960b
--- /dev/null
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.ui.lint
+
+import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
+import androidx.compose.lint.isInPackageName
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.isBelow
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import java.util.EnumSet
+import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.calls.KtCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCompoundAccessCall
+import org.jetbrains.kotlin.analysis.api.calls.KtImplicitReceiverValue
+import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
+import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KtReceiverParameterSymbol
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * [Detector] that checks calls to Modifier.then to make sure the parameter does not contain a
+ * Modifier factory function called with an receiver, as this will cause duplicate modifiers in the
+ * chain. E.g. this.then(foo()), will result in this.then(this.then(foo)), as foo() internally will
+ * call this.then(FooModifier).
+ */
+class SuspiciousModifierThenDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> = listOf(ThenName)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!method.isInPackageName(Names.Ui.PackageName)) return
+
+ val otherModifierArgument = node.valueArguments.firstOrNull() ?: return
+ val otherModifierArgumentSource = otherModifierArgument.sourcePsi ?: return
+
+ otherModifierArgument.accept(object : AbstractUastVisitor() {
+ /**
+ * Visit all calls to look for calls to a Modifier factory with implicit receiver
+ */
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ val hasModifierReceiverType =
+ node.receiverType?.inheritsFrom(Names.Ui.Modifier) == true
+ val usesImplicitThis = node.receiver == null
+
+ if (!hasModifierReceiverType || !usesImplicitThis) {
+ return false
+ }
+
+ val ktCallExpression = node.sourcePsi as? KtCallExpression ?: return false
+ // Resolve the implicit `this` to its source, if possible.
+ val implicitReceiver = analyze(ktCallExpression) {
+ getImplicitReceiverValue(ktCallExpression)?.getImplicitReceiverPsi()
+ }
+
+ // The receiver used by the modifier function is defined within the then() call,
+ // such as then(Modifier.composed { otherModifierFactory() }). We don't know what
+ // the value of this receiver will be, so we ignore this case.
+ if (implicitReceiver.isBelow(otherModifierArgumentSource)) {
+ return false
+ }
+
+ context.report(
+ SuspiciousModifierThen,
+ node,
+ context.getNameLocation(node),
+ "Using Modifier.then with a Modifier factory function with an implicit receiver"
+ )
+
+ // Keep on searching for more errors
+ return false
+ }
+ })
+ }
+
+ companion object {
+ val SuspiciousModifierThen = Issue.create(
+ "SuspiciousModifierThen",
+ "Using Modifier.then with a Modifier factory function with an implicit receiver",
+ "Calling a Modifier factory function with an implicit receiver inside " +
+ "Modifier.then will result in the receiver (`this`) being added twice to the " +
+ "chain. For example, fun Modifier.myModifier() = this.then(otherModifier()) - " +
+ "the implementation of factory functions such as Modifier.otherModifier() will " +
+ "internally call this.then(...) to chain the provided modifier with their " +
+ "implementation. When you expand this.then(otherModifier()), it becomes: " +
+ "this.then(this.then(OtherModifierImplementation)) - so you can see that `this` " +
+ "is included twice in the chain, which results in modifiers such as padding " +
+ "being applied twice, for example. Instead, you should either remove the then() " +
+ "and directly chain the factory function on the receiver, this.otherModifier(), " +
+ "or add the empty Modifier as the receiver for the factory, such as " +
+ "this.then(Modifier.otherModifier())",
+ Category.CORRECTNESS,
+ 3,
+ Severity.ERROR,
+ Implementation(
+ SuspiciousModifierThenDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
+
+private const val ThenName = "then"
+
+// Below functions taken from AnalysisApiLintUtils.kt
+
+/**
+ * Returns the PSI for [this], which will be the owning lambda expression or the surrounding class.
+ */
+private fun KtImplicitReceiverValue.getImplicitReceiverPsi(): PsiElement? {
+ return when (val receiverParameterSymbol = this.symbol) {
+ // the owning lambda expression
+ is KtReceiverParameterSymbol -> receiverParameterSymbol.owningCallableSymbol.psi
+ // the class that we are in, calling a method
+ is KtClassOrObjectSymbol -> receiverParameterSymbol.psi
+ else -> null
+ }
+}
+
+/**
+ * Returns the implicit receiver value of the call-like expression [ktExpression] (can include
+ * property accesses, for example).
+ */
+private fun KtAnalysisSession.getImplicitReceiverValue(
+ ktExpression: KtExpression
+): KtImplicitReceiverValue? {
+ val partiallyAppliedSymbol =
+ when (val call = ktExpression.resolveCall()?.singleCallOrNull<KtCall>()) {
+ is KtCompoundAccessCall -> call.compoundAccess.operationPartiallyAppliedSymbol
+ is KtCallableMemberCall<*, *> -> call.partiallyAppliedSymbol
+ else -> null
+ } ?: return null
+
+ return partiallyAppliedSymbol.extensionReceiver as? KtImplicitReceiverValue
+ ?: partiallyAppliedSymbol.dispatchReceiver as? KtImplicitReceiverValue
+}
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
index 4332db6..78e535b9 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
@@ -39,6 +39,7 @@
MultipleAwaitPointerEventScopesDetector.MultipleAwaitPointerEventScopes,
ReturnFromAwaitPointerEventScopeDetector.ExitAwaitPointerEventScope,
SuspiciousCompositionLocalModifierReadDetector.SuspiciousCompositionLocalModifierRead,
+ SuspiciousModifierThenDetector.SuspiciousModifierThen
)
override val vendor = Vendor(
vendorName = "Jetpack Compose",
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
index 1c0b6e9..567f6d43 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
@@ -34,6 +34,6 @@
val registry = UiIssueRegistry()
assertThat(registry.api).isEqualTo(CURRENT_API)
- assertThat(registry.minApi).isEqualTo(10)
+ assertThat(registry.minApi).isEqualTo(14)
}
}
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
index 3d33d33..3c53513 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
@@ -122,7 +122,7 @@
this@test
}
- fun Modifier.test(): Modifier {
+ fun Modifier.test2(): Modifier {
return composed {
this@test
}
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt
new file mode 100644
index 0000000..cd8c963
--- /dev/null
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.ui.lint
+
+import androidx.compose.lint.test.Stubs
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+
+/**
+ * Test for [SuspiciousModifierThenDetector].
+ */
+@RunWith(JUnit4::class)
+class SuspiciousModifierThenDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = SuspiciousModifierThenDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(SuspiciousModifierThenDetector.SuspiciousModifierThen)
+
+ @Test
+ fun clean() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+
+ object TestModifier : Modifier.Element
+
+ fun Modifier.test(): Modifier = this.then(TestModifier)
+
+ fun Modifier.test2() = this.then(Modifier.test())
+
+ fun Modifier.test3() = this.then(with(Modifier) { test() })
+
+ fun Modifier.test4() = this.then(if (true) TestModifier else TestModifier)
+ fun Modifier.test5() = this.then(if (true) Modifier.test() else Modifier.test())
+
+ // We don't know what the receiver will be inside an arbitrary lambda like here, so
+ // we shouldn't warn for any calls inside a lambda
+ fun Modifier.composed(
+ factory: Modifier.() -> Modifier
+ ): Modifier = then(with(Modifier) { factory() })
+
+ fun Modifier.test6() = this.then(Modifier.composed { TestModifier })
+ fun Modifier.test7() = this.then(Modifier.composed { test() })
+
+ fun Modifier.test8(): Modifier {
+ val lambda: Modifier.() -> Modifier = {
+ Modifier.test()
+ }
+ return this.then(lambda())
+ }
+"""
+ ),
+ Stubs.Modifier
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun errors() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+
+ object TestModifier : Modifier.Element
+
+ fun Modifier.test(): Modifier = this.then(TestModifier)
+
+ fun Modifier.test2() = this.then(test())
+
+ fun Modifier.test3() = this.then(with(1) { test() })
+
+ fun Modifier.test4() = this.then(if (true) test() else TestModifier)
+"""
+ ),
+ Stubs.Modifier
+ )
+ .run()
+ .expect(
+ """
+src/test/TestModifier.kt:10: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+ fun Modifier.test2() = this.then(test())
+ ~~~~
+src/test/TestModifier.kt:12: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+ fun Modifier.test3() = this.then(with(1) { test() })
+ ~~~~
+src/test/TestModifier.kt:14: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+ fun Modifier.test4() = this.then(if (true) test() else TestModifier)
+ ~~~~
+3 errors, 0 warnings
+ """
+ )
+ }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
index f078a82..5210584 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
@@ -19,9 +19,11 @@
import android.content.res.Configuration
import android.util.DisplayMetrics
import android.view.View
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
@@ -123,7 +125,85 @@
}
@Test
- fun sizeOverride_allowsForCorrectSpace() {
+ fun sizeOverride_allowsForCorrectSpace_smallPortraitAspectRatio() {
+ lateinit var actualDensity: Density
+ var actualConstraints: Constraints? = null
+
+ rule.setContent {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.ForcedSize(DpSize(30.dp, 40.dp))
+ ) {
+ Spacer(
+ modifier = Modifier.layout { measurable, constraints ->
+ actualConstraints = constraints
+ actualDensity = this
+
+ val placeable = measurable.measure(constraints)
+
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+ )
+ }
+ }
+
+ // The constraint should be within 0.5 pixels of the specified size
+ // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+ // this is true in normal Compose code as well
+ assertEquals(
+ with(actualDensity) { 30.dp.toPx() },
+ actualConstraints!!.maxWidth.toFloat(),
+ 0.5f
+ )
+ assertEquals(
+ with(actualDensity) { 40.dp.toPx() },
+ actualConstraints!!.maxHeight.toFloat(),
+ 0.5f
+ )
+ }
+
+ @Test
+ fun sizeOverride_allowsForCorrectSpace_smallLandscapeAspectRatio() {
+ lateinit var actualDensity: Density
+ var actualConstraints: Constraints? = null
+
+ rule.setContent {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.ForcedSize(DpSize(40.dp, 30.dp))
+ ) {
+ Spacer(
+ modifier = Modifier.layout { measurable, constraints ->
+ actualConstraints = constraints
+ actualDensity = this
+
+ val placeable = measurable.measure(constraints)
+
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+ )
+ }
+ }
+
+ // The constraint should be within 0.5 pixels of the specified size
+ // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+ // this is true in normal Compose code as well
+ assertEquals(
+ with(actualDensity) { 40.dp.toPx() },
+ actualConstraints!!.maxWidth.toFloat(),
+ 0.5f
+ )
+ assertEquals(
+ with(actualDensity) { 30.dp.toPx() },
+ actualConstraints!!.maxHeight.toFloat(),
+ 0.5f
+ )
+ }
+
+ @Test
+ fun sizeOverride_allowsForCorrectSpace_largePortraitAspectRatio() {
lateinit var actualDensity: Density
var actualConstraints: Constraints? = null
@@ -146,29 +226,170 @@
}
}
- assertTrue(with(actualDensity) { actualConstraints!!.maxWidth.toDp() >= 3000.dp })
- assertTrue(with(actualDensity) { actualConstraints!!.maxHeight.toDp() >= 4000.dp })
+ // The constraint should be within 0.5 pixels of the specified size
+ // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+ // this is true in normal Compose code as well
+ assertEquals(
+ with(actualDensity) { 3000.dp.toPx() },
+ actualConstraints!!.maxWidth.toFloat(),
+ 0.5f
+ )
+ assertEquals(
+ with(actualDensity) { 4000.dp.toPx() },
+ actualConstraints!!.maxHeight.toFloat(),
+ 0.5f
+ )
}
@Test
- fun sizeOverride_overridesConfigurationDensity() {
- lateinit var density: Density
- lateinit var configuration: Configuration
+ fun sizeOverride_allowsForCorrectSpace_largeLandscapeAspectRatio() {
+ lateinit var actualDensity: Density
+ var actualConstraints: Constraints? = null
rule.setContent {
DeviceConfigurationOverride(
+ DeviceConfigurationOverride.ForcedSize(DpSize(4000.dp, 3000.dp))
+ ) {
+ Spacer(
+ modifier = Modifier.layout { measurable, constraints ->
+ actualConstraints = constraints
+ actualDensity = this
+
+ val placeable = measurable.measure(constraints)
+
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+ )
+ }
+ }
+
+ // The constraint should be within 0.5 pixels of the specified size
+ // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+ // this is true in normal Compose code as well
+ assertEquals(
+ with(actualDensity) { 4000.dp.toPx() },
+ actualConstraints!!.maxWidth.toFloat(),
+ 0.5f
+ )
+ assertEquals(
+ with(actualDensity) { 3000.dp.toPx() },
+ actualConstraints!!.maxHeight.toFloat(),
+ 0.5f
+ )
+ }
+
+ @Test
+ fun sizeOverride_largeRequestedSize_overridesConfigurationDensity() {
+ lateinit var originalDensity: Density
+ lateinit var overriddenDensity: Density
+ lateinit var overriddenConfiguration: Configuration
+
+ rule.setContent {
+ originalDensity = LocalDensity.current
+ DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(3000.dp, 4000.dp))
) {
- density = LocalDensity.current
- configuration = LocalConfiguration.current
+ overriddenDensity = LocalDensity.current
+ overriddenConfiguration = LocalConfiguration.current
+ }
+ }
+
+ // A 3000dp by 4000dp device is so big, that we can assume that the density needs to be
+ // overridden.
+ // If this test runs on a device with that size screen, where overriding density is not
+ // necessary, this test might fail. If that is happening, hopefully the future is a nice
+ // place.
+ assertTrue(originalDensity.density > overriddenDensity.density)
+
+ // Convert the Configuration's density in DPI to the raw float multiplier
+ val overriddenConfigurationDensityMultiplier =
+ overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+ assertEquals(
+ overriddenDensity.density,
+ overriddenConfigurationDensityMultiplier,
+ // Compare within half a step of density DPI changes
+ 1f / DisplayMetrics.DENSITY_DEFAULT / 2f
+ )
+ }
+
+ @Test
+ fun sizeOverride_notNeededForPortrait_doesNotOverrideConfigurationDensity() {
+ lateinit var originalDensity: Density
+ lateinit var overriddenDensity: Density
+ lateinit var overriddenConfiguration: Configuration
+
+ rule.setContent {
+ originalDensity = LocalDensity.current
+ Box(Modifier.size(35.dp, 45.dp)) {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.ForcedSize(DpSize(30.dp, 40.dp))
+ ) {
+ overriddenDensity = LocalDensity.current
+ overriddenConfiguration = LocalConfiguration.current
+ }
}
}
// This is a strict equality for floating point values which is normally problematic, but
- // this should be precisely equal
+ // these should be precisely equal
assertEquals(
- density.density,
- configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+ originalDensity.density,
+ overriddenDensity.density
+ )
+
+ // Convert the Configuration's density in DPI to the raw float multiplier
+ val overriddenConfigurationDensityMultiplier =
+ overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+ // This is a strict equality for floating point values which is normally problematic, but
+ // these should be precisely equal
+ assertEquals(
+ overriddenDensity.density,
+ overriddenConfigurationDensityMultiplier,
+ // Compare within half a step of density DPI changes
+ 1f / DisplayMetrics.DENSITY_DEFAULT / 2f
+ )
+ }
+
+ @Test
+ fun sizeOverride_notNeededForLandscape_doesNotOverrideConfigurationDensity() {
+ lateinit var originalDensity: Density
+ lateinit var overriddenDensity: Density
+ lateinit var overriddenConfiguration: Configuration
+
+ rule.setContent {
+ originalDensity = LocalDensity.current
+ Box(Modifier.size(45.dp, 35.dp)) {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.ForcedSize(DpSize(40.dp, 30.dp))
+ ) {
+ overriddenDensity = LocalDensity.current
+ overriddenConfiguration = LocalConfiguration.current
+ }
+ }
+ }
+
+ // This is a strict equality for floating point values which is normally problematic, but
+ // these should be precisely equal
+ assertEquals(
+ originalDensity.density,
+ overriddenDensity.density
+ )
+
+ // Convert the Configuration's density in DPI to the raw float multiplier
+ val overriddenConfigurationDensityMultiplier =
+ overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+ // This is a strict equality for floating point values which is normally problematic, but
+ // these should be precisely equal
+ assertEquals(
+ overriddenDensity.density,
+ overriddenConfigurationDensityMultiplier,
+ // Compare within half a step of density DPI changes
+ 1f / DisplayMetrics.DENSITY_DEFAULT / 2f
)
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
index c393dcb..b735798 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
@@ -46,13 +46,6 @@
) {
SubcomposeLayout(
modifier = modifier
- .then(
- if (size.isSpecified) {
- Modifier.size(size)
- } else {
- Modifier
- }
- )
) { constraints ->
val measurables = subcompose(Unit) {
val maxWidth = constraints.maxWidth.toDp()
@@ -63,7 +56,7 @@
maxWidth
}
val requiredHeight = if (size.isSpecified) {
- max(maxWidth, size.height)
+ max(maxHeight, size.height)
} else {
maxHeight
}
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index 6aede09..2900208 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -25,3 +25,7 @@
RemovedClass: androidx.compose.ui.text.input.AndroidImeOptions:
Removed class androidx.compose.ui.text.input.AndroidImeOptions
+
+
+RemovedPackage: androidx.compose.ui.text.platform:
+ Removed package androidx.compose.ui.text.platform
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 7196bff..f3272ff 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1320,15 +1320,6 @@
}
-package androidx.compose.ui.text.platform {
-
- @SuppressCompatibility @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
- ctor public URLSpanCache();
- method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
- }
-
-}
-
package androidx.compose.ui.text.platform.extensions {
public final class TtsAnnotationExtensions_androidKt {
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 9e9bcf6..f6a490e 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1329,11 +1329,6 @@
@kotlin.PublishedApi internal final class SynchronizedObject {
}
- @SuppressCompatibility @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
- ctor public URLSpanCache();
- method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
- }
-
}
package androidx.compose.ui.text.platform.extensions {
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index dc89b2a..0eeb331 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -84,7 +84,7 @@
api("androidx.annotation:annotation-experimental:1.4.0")
implementation("androidx.core:core:1.7.0")
implementation("androidx.emoji2:emoji2:1.2.0")
- implementation('androidx.collection:collection:1.0.0')
+ implementation("androidx.collection:collection:1.4.0")
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index 8976a8b..e8025f9 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -20,6 +20,7 @@
import android.text.SpannableString
import android.text.style.AbsoluteSizeSpan
import android.text.style.BackgroundColorSpan
+import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LocaleSpan
import android.text.style.RelativeSizeSpan
@@ -30,10 +31,12 @@
import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import android.text.style.UnderlineSpan
+import androidx.collection.longObjectMapOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.UncachedFontFamilyResolver
import androidx.compose.ui.text.UrlAnnotation
@@ -55,6 +58,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.packInts
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -84,7 +88,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -109,7 +115,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -133,7 +141,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -157,7 +167,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -180,7 +192,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -203,7 +217,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -227,7 +243,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -252,7 +270,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -273,7 +293,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -297,7 +319,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -321,7 +345,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -344,7 +370,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -368,7 +396,9 @@
val spannableString = annotatedString.toAccessibilitySpannableString(
density,
fontFamilyResolver,
- urlSpanCache
+ urlSpanCache,
+ null,
+ 0
)
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -393,7 +423,7 @@
val fontFamilyResolver = createFontFamilyResolver(context)
// see if font span is added
- string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache())
+ string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache(), null, 0)
// toAccessibilitySpannableString should _not_ make any font requests
assertThat(loader.blockingRequests).isEmpty()
@@ -411,14 +441,174 @@
append("link")
}
- val spannable1 =
- string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
- val spannable2 =
- string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, null, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, null, 0
+ )
val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
assertThat(spannable1).isNotSameInstanceAs(spannable2)
assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
}
+
+ @Test
+ fun urlSpansSame_forSameAnnotationAndRange() {
+ val link = LinkAnnotation.Url("url")
+
+ val string = buildAnnotatedString {
+ pushLink(link)
+ append("link")
+ }
+
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, null, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, null, 0
+ )
+ val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
+ val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
+ }
+
+ @Test
+ fun clickableSpansSame_forSameAnnotationAndRange_forUrlsWithCallback() {
+ val link = LinkAnnotation.Url("url")
+ val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+ val string = buildAnnotatedString {
+ pushLink(link)
+ append("link")
+ }
+
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java).single()
+ val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java).single()
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(span1).isSameInstanceAs(span2)
+ }
+
+ @Test
+ fun clickableSpansSame_forSameAnnotationAndRange_forClickables() {
+ val link = LinkAnnotation.Clickable("Tag")
+ val linkActions = longObjectMapOf(packInts(0, 9), {})
+
+ val string = buildAnnotatedString {
+ pushLink(link)
+ append("clickable")
+ }
+
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java).single()
+ val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java).single()
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(span1).isSameInstanceAs(span2)
+ }
+
+ @Test
+ fun clickableSpansDifferent_forSameAnnotationButDifferentRange_forUrlsWithCallback() {
+ val link = LinkAnnotation.Url("url")
+ val linkActions = longObjectMapOf(
+ packInts(0, 4), {},
+ packInts(4, 8), {}
+ )
+
+ val string = buildAnnotatedString {
+ withAnnotation(link) { append("link") }
+ withAnnotation(link) { append("link") }
+ }
+
+ val spannable = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val span1 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[0]
+ val span2 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[1]
+
+ assertThat(span1).isNotSameInstanceAs(span2)
+ }
+
+ @Test
+ fun clickableSpansDifferent_forSameAnnotatedStringWithDifferentRange_forClickables() {
+ val link = LinkAnnotation.Clickable("tag")
+ val linkActions = longObjectMapOf(
+ packInts(0, 4), {},
+ packInts(4, 8), {}
+ )
+
+ val string = buildAnnotatedString {
+ withAnnotation(link) { append("link") }
+ withAnnotation(link) { append("link") }
+ }
+
+ val spannable = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val span1 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[0]
+ val span2 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[1]
+
+ assertThat(span1).isNotSameInstanceAs(span2)
+ }
+
+ @Test
+ fun clickableSpansDifferent_forSameAnnotationAndRange_differentNodeInfo_forUrlsWithCallback() {
+ val link = LinkAnnotation.Url("url")
+ val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+ val string = buildAnnotatedString {
+ withAnnotation(link) { append("link") }
+ withAnnotation(link) { append("link") }
+ }
+
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 1
+ )
+ val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java)[0]
+ val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java)[0]
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(span1).isNotSameInstanceAs(span2)
+ }
+
+ @Test
+ fun clickableSpansDifferent_forSameAnnotationAndRange_differentNodeInfo_forClickables() {
+ val link = LinkAnnotation.Clickable("tag")
+ val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+ val string = buildAnnotatedString {
+ withAnnotation(link) { append("link") }
+ withAnnotation(link) { append("link") }
+ }
+
+ val spannable1 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 0
+ )
+ val spannable2 = string.toAccessibilitySpannableString(
+ density, fontFamilyResolver, urlSpanCache, linkActions, 1
+ )
+ val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java)[0]
+ val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java)[0]
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(span1).isNotSameInstanceAs(span2)
+ }
}
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
index 0e0bb15..9574329 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
@@ -28,9 +28,11 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.collection.LongObjectMap
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -53,7 +55,9 @@
fun AnnotatedString.toAccessibilitySpannableString(
density: Density,
fontFamilyResolver: FontFamily.Resolver,
- urlSpanCache: URLSpanCache
+ urlSpanCache: URLSpanCache,
+ linkActions: LongObjectMap<() -> Unit>?,
+ accessibilityNodeId: Int
): SpannableString {
val spannableString = SpannableString(text)
spanStylesOrNull?.fastForEach { (style, start, end) ->
@@ -81,6 +85,27 @@
)
}
+ getLinkAnnotations(0, length).fastForEach { linkRange ->
+ when {
+ linkActions != null && linkActions.isNotEmpty() -> {
+ spannableString.setSpan(
+ urlSpanCache.toClickableSpan(linkRange, linkActions, accessibilityNodeId),
+ linkRange.start,
+ linkRange.end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ linkRange.item is LinkAnnotation.Url -> {
+ spannableString.setSpan(
+ urlSpanCache.toURLSpan(linkRange.toUrl()),
+ linkRange.start,
+ linkRange.end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ }
+ }
+
return spannableString
}
@@ -179,3 +204,6 @@
@DoNotInline
fun createTypefaceSpan(typeface: Typeface): TypefaceSpan = TypefaceSpan(typeface)
}
+
+private fun AnnotatedString.Range<LinkAnnotation>.toUrl() =
+ AnnotatedString.Range(this.item as LinkAnnotation.Url, this.start, this.end)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
index f50503b..522c6a6 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
@@ -16,10 +16,17 @@
package androidx.compose.ui.text.platform
+import android.text.style.ClickableSpan
import android.text.style.URLSpan
+import android.view.View
+import androidx.annotation.RestrictTo
+import androidx.collection.LongObjectMap
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.UrlAnnotation
+import androidx.compose.ui.util.packInts
import java.util.WeakHashMap
/**
@@ -37,10 +44,52 @@
@Suppress("AcronymName")
@OptIn(ExperimentalTextApi::class)
@InternalTextApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class URLSpanCache {
private val spansByAnnotation = WeakHashMap<UrlAnnotation, URLSpan>()
+ private val urlSpansByAnnotation =
+ WeakHashMap<AnnotatedString.Range<LinkAnnotation.Url>, URLSpan>()
+ private val clickableSpansByAnnotation =
+ WeakHashMap<AnnotatedString.Range<LinkAnnotation>, Pair<ComposeClickableSpan, Int>>()
@Suppress("AcronymName")
fun toURLSpan(urlAnnotation: UrlAnnotation): URLSpan =
spansByAnnotation.getOrPut(urlAnnotation) { URLSpan(urlAnnotation.url) }
+
+ @Suppress("AcronymName")
+ fun toURLSpan(urlRange: AnnotatedString.Range<LinkAnnotation.Url>): URLSpan =
+ urlSpansByAnnotation.getOrPut(urlRange) { URLSpan(urlRange.item.url) }
+
+ /**
+ * This method takes a [linkRange] which is an annotation that occupies range in Compose text
+ * and converts it into a ClickableSpan passing the corresponding [linkActions] to the
+ * ClickableSpan's onClick method.
+ * We use [accessibilityNodeId] to invalidate cache entry for cases when the original Compose text was
+ * disposed and so the corresponding link actions are not valid anymore
+ */
+ fun toClickableSpan(
+ linkRange: AnnotatedString.Range<LinkAnnotation>,
+ linkActions: LongObjectMap<() -> Unit>,
+ accessibilityNodeId: Int
+ ): ClickableSpan? {
+ val spanWithNodeId = clickableSpansByAnnotation[linkRange]
+
+ return if (spanWithNodeId == null || spanWithNodeId.second != accessibilityNodeId) {
+ // either clickable span hasn't been created yet or the cache needs invalidation for a
+ // given annotation
+ linkActions[packInts(linkRange.start, linkRange.end)]?.let { action ->
+ ComposeClickableSpan(action).also { newSpan ->
+ clickableSpansByAnnotation[linkRange] = Pair(newSpan, accessibilityNodeId)
+ }
+ }
+ } else {
+ spanWithNodeId.first
+ }
+ }
+}
+
+private class ComposeClickableSpan(private val linkAction: () -> Unit) : ClickableSpan() {
+ override fun onClick(widget: View) {
+ linkAction()
+ }
}
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 4eecfad..ceda641 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -16,9 +16,9 @@
package androidx.compose.ui.unit
-import androidx.collection.IntIntPair
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Constraints.Companion.Infinity
import kotlin.math.min
/**
@@ -223,84 +223,6 @@
const val Infinity = Int.MAX_VALUE
/**
- * The bit distribution when the focus of the bits should be on the width, but only
- * a minimal difference in focus.
- *
- * 16 bits assigned to width, 15 bits assigned to height.
- */
- private const val MinFocusWidth = 0x02L
-
- /**
- * The bit distribution when the focus of the bits should be on the width, and a
- * maximal number of bits assigned to the width.
- *
- * 18 bits assigned to width, 13 bits assigned to height.
- */
- private const val MaxFocusWidth = 0x03L
-
- /**
- * The bit distribution when the focus of the bits should be on the height, but only
- * a minimal difference in focus.
- *
- * 15 bits assigned to width, 16 bits assigned to height.
- */
- private const val MinFocusHeight = 0x01L
-
- /**
- * The bit distribution when the focus of the bits should be on the height, and a
- * a maximal number of bits assigned to the height.
- *
- * 13 bits assigned to width, 18 bits assigned to height.
- */
- private const val MaxFocusHeight = 0x00L
-
- /**
- * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth],
- * [MinFocusHeight], [MaxFocusHeight]).
- */
- private const val FocusMask = 0x03L
-
- /**
- * The number of bits used for the focused dimension when there is minimal focus.
- */
- private const val MinFocusBits = 16
-
- /**
- * The mask to use for the focused dimension when there is minimal focus.
- */
- private const val MinFocusMask = 0xFFFF // 64K (16 bits)
-
- /**
- * The number of bits used for the non-focused dimension when there is minimal focus.
- */
- private const val MinNonFocusBits = 15
-
- /**
- * The mask to use for the non-focused dimension when there is minimal focus.
- */
- private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)
-
- /**
- * The number of bits to use for the focused dimension when there is maximal focus.
- */
- private const val MaxFocusBits = 18
-
- /**
- * The mask to use for the focused dimension when there is maximal focus.
- */
- private const val MaxFocusMask = 0x3FFFF // 256K (18 bits)
-
- /**
- * The number of bits to use for the non-focused dimension when there is maximal focus.
- */
- private const val MaxNonFocusBits = 13
-
- /**
- * The mask to use for the non-focused dimension when there is maximal focus.
- */
- private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
-
- /**
* Creates constraints for fixed size in both dimensions.
*/
@Stable
@@ -374,95 +296,197 @@
prioritizeWidth: Boolean = true
): Constraints {
if (prioritizeWidth) {
- val (minW, maxW) = calculatePriorityDimension(minWidth, maxWidth)
+ val minW = min(minWidth, MaxFocusMask - 1)
+ val maxW = if (maxWidth == Infinity) {
+ Infinity
+ } else {
+ min(maxWidth, MaxFocusMask - 1)
+ }
val consumed = if (maxW == Infinity) minW else maxW
- val bitsUsed = bitsNeedForSize(consumed)
- val (minH, maxH) = calculateOtherDimension(minHeight, maxHeight, bitsUsed)
+ val maxAllowed = maxAllowedForSize(consumed)
+ val maxH =
+ if (maxHeight == Infinity) Infinity else min(maxAllowed, maxHeight)
+ val minH = min(maxAllowed, minHeight)
return Constraints(minW, maxW, minH, maxH)
}
- val (minH, maxH) = calculatePriorityDimension(minHeight, maxHeight)
- val consumed = if (maxH == Infinity) minH else maxH
- val bitsUsed = bitsNeedForSize(consumed)
- val (minW, maxW) = calculateOtherDimension(minWidth, maxWidth, bitsUsed)
- return Constraints(minW, maxW, minH, maxH)
- }
- private fun calculatePriorityDimension(minValue: Int, maxValue: Int): IntIntPair {
- val newMinValue = min(minValue, MaxFocusMask - 1)
- val newMaxValue = if (maxValue == Infinity) {
+ val minH = min(minHeight, MaxFocusMask - 1)
+ val maxH = if (maxHeight == Infinity) {
Infinity
} else {
- min(maxValue, MaxFocusMask - 1)
+ min(maxHeight, MaxFocusMask - 1)
}
- return IntIntPair(newMinValue, newMaxValue)
+ val consumed = if (maxH == Infinity) minH else maxH
+ val maxAllowed = maxAllowedForSize(consumed)
+ val maxW = if (maxWidth == Infinity) Infinity else min(maxAllowed, maxWidth)
+ val minW = min(maxAllowed, minWidth)
+ return Constraints(minW, maxW, minH, maxH)
}
+ }
+}
- private fun calculateOtherDimension(
- minValue: Int,
- maxValue: Int,
- bitsUsed: Int
- ): IntIntPair {
- val allowedBits = 31 - bitsUsed
- val maxAllowed = (1 shl allowedBits) - 2
- val newMaxValue = if (maxValue == Infinity) Infinity else min(maxAllowed, maxValue)
- val newMinValue = min(maxAllowed, minValue)
- return IntIntPair(newMinValue, newMaxValue)
- }
+// Redefinition of Constraints.Infinity to bypass the companion object
+private const val Infinity = Int.MAX_VALUE
- /**
- * Creates a [Constraints], only checking that the values fit in the packed Long.
- */
- internal fun createConstraints(
- minWidth: Int,
- maxWidth: Int,
- minHeight: Int,
- maxHeight: Int
- ): Constraints {
- val heightVal = if (maxHeight == Infinity) minHeight else maxHeight
- val heightBits = bitsNeedForSize(heightVal)
+/**
+ * The bit distribution when the focus of the bits should be on the width, but only
+ * a minimal difference in focus.
+ *
+ * 16 bits assigned to width, 15 bits assigned to height.
+ */
+private const val MinFocusWidth = 0x02
- val widthVal = if (maxWidth == Infinity) minWidth else maxWidth
- val widthBits = bitsNeedForSize(widthVal)
+/**
+ * The bit distribution when the focus of the bits should be on the width, and a
+ * maximal number of bits assigned to the width.
+ *
+ * 18 bits assigned to width, 13 bits assigned to height.
+ */
+private const val MaxFocusWidth = 0x03
- if (widthBits + heightBits > 31) {
- throwIllegalArgumentException(
- "Can't represent a width of $widthVal and height of $heightVal in Constraints"
- )
- }
+/**
+ * The bit distribution when the focus of the bits should be on the height, but only
+ * a minimal difference in focus.
+ *
+ * 15 bits assigned to width, 16 bits assigned to height.
+ */
+private const val MinFocusHeight = 0x01
- val focus = when (widthBits) {
- MinNonFocusBits -> MinFocusHeight
- MinFocusBits -> MinFocusWidth
- MaxNonFocusBits -> MaxFocusHeight
- MaxFocusBits -> MaxFocusWidth
- else -> throw IllegalStateException("Should only have the provided constants.")
- }
+/**
+ * The bit distribution when the focus of the bits should be on the height, and a
+ * a maximal number of bits assigned to the height.
+ *
+ * 13 bits assigned to width, 18 bits assigned to height.
+ */
+private const val MaxFocusHeight = 0x00
- val maxWidthValue = if (maxWidth == Infinity) 0 else maxWidth + 1
- val maxHeightValue = if (maxHeight == Infinity) 0 else maxHeight + 1
+/**
+ * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth],
+ * [MinFocusHeight], [MaxFocusHeight]).
+ */
+private const val FocusMask = 0x03L
- val minHeightOffset = minHeightOffsets(indexToBitOffset(focus.toInt()))
- val maxHeightOffset = minHeightOffset + 31
+/**
+ * The number of bits used for the focused dimension when there is minimal focus.
+ */
+private const val MinFocusBits = 16
+private const val MaxAllowedForMinFocusBits = (1 shl (31 - MinFocusBits)) - 2
- val value = focus or
- (minWidth.toLong() shl 2) or
- (maxWidthValue.toLong() shl 33) or
- (minHeight.toLong() shl minHeightOffset) or
- (maxHeightValue.toLong() shl maxHeightOffset)
- return Constraints(value)
- }
+/**
+ * The mask to use for the focused dimension when there is minimal focus.
+ */
+private const val MinFocusMask = 0xFFFF // 64K (16 bits)
- private fun bitsNeedForSize(size: Int): Int {
- return when {
- size < MaxNonFocusMask -> MaxNonFocusBits
- size < MinNonFocusMask -> MinNonFocusBits
- size < MinFocusMask -> MinFocusBits
- size < MaxFocusMask -> MaxFocusBits
- else -> throw IllegalArgumentException(
- "Can't represent a size of $size in Constraints"
- )
- }
- }
+/**
+ * The number of bits used for the non-focused dimension when there is minimal focus.
+ */
+private const val MinNonFocusBits = 15
+private const val MaxAllowedForMinNonFocusBits = (1 shl (31 - MinNonFocusBits)) - 2
+
+/**
+ * The mask to use for the non-focused dimension when there is minimal focus.
+ */
+private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)
+
+/**
+ * The number of bits to use for the focused dimension when there is maximal focus.
+ */
+private const val MaxFocusBits = 18
+private const val MaxAllowedForMaxFocusBits = (1 shl (31 - MaxFocusBits)) - 2
+
+/**
+ * The mask to use for the focused dimension when there is maximal focus.
+ */
+private const val MaxFocusMask = 0x3FFFF // 256K (18 bits)
+
+/**
+ * The number of bits to use for the non-focused dimension when there is maximal focus.
+ */
+private const val MaxNonFocusBits = 13
+private const val MaxAllowedForMaxNonFocusBits = (1 shl (31 - MaxNonFocusBits)) - 2
+
+/**
+ * The mask to use for the non-focused dimension when there is maximal focus.
+ */
+private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
+
+// Wrap those throws in functions to avoid inlining the string building at the call sites
+private fun invalidConstraint(widthVal: Int, heightVal: Int) {
+ throw IllegalArgumentException(
+ "Can't represent a width of $widthVal and height of $heightVal in Constraints"
+ )
+}
+
+private fun invalidSize(size: Int): Nothing {
+ throw IllegalArgumentException(
+ "Can't represent a size of $size in Constraints"
+ )
+}
+
+/**
+ * Creates a [Constraints], only checking that the values fit in the packed Long.
+ */
+internal fun createConstraints(
+ minWidth: Int,
+ maxWidth: Int,
+ minHeight: Int,
+ maxHeight: Int
+): Constraints {
+ val heightVal = if (maxHeight == Infinity) minHeight else maxHeight
+ val heightBits = bitsNeedForSizeUnchecked(heightVal)
+
+ val widthVal = if (maxWidth == Infinity) minWidth else maxWidth
+ val widthBits = bitsNeedForSizeUnchecked(widthVal)
+
+ if (widthBits + heightBits > 31) {
+ invalidConstraint(widthVal, heightVal)
+ }
+
+ // Same as if (maxWidth == Infinity) 0 else maxWidth + 1 but branchless
+ // in DEX and saves 2 instructions on aarch64. Relies on integer overflow
+ // since Infinity == Int.MAX_VALUE
+ var maxWidthValue = maxWidth + 1
+ maxWidthValue = maxWidthValue and (maxWidthValue shr 31).inv()
+
+ var maxHeightValue = maxHeight + 1
+ maxHeightValue = maxHeightValue and (maxHeightValue shr 31).inv()
+
+ val focus = when (widthBits) {
+ MinNonFocusBits -> MinFocusHeight
+ MinFocusBits -> MinFocusWidth
+ MaxNonFocusBits -> MaxFocusHeight
+ MaxFocusBits -> MaxFocusWidth
+ else -> 0x00 // can't happen, widthBits is computed from bitsNeedForSizeUnchecked()
+ }
+
+ val minHeightOffset = minHeightOffsets(indexToBitOffset(focus))
+ val maxHeightOffset = minHeightOffset + 31
+
+ val value = focus.toLong() or
+ (minWidth.toLong() shl 2) or
+ (maxWidthValue.toLong() shl 33) or
+ (minHeight.toLong() shl minHeightOffset) or
+ (maxHeightValue.toLong() shl maxHeightOffset)
+ return Constraints(value)
+}
+
+private fun bitsNeedForSizeUnchecked(size: Int): Int {
+ return when {
+ size < MaxNonFocusMask -> MaxNonFocusBits
+ size < MinNonFocusMask -> MinNonFocusBits
+ size < MinFocusMask -> MinFocusBits
+ size < MaxFocusMask -> MaxFocusBits
+ else -> 255
+ }
+}
+
+private fun maxAllowedForSize(size: Int): Int {
+ return when {
+ size < MaxNonFocusMask -> MaxAllowedForMaxNonFocusBits
+ size < MinNonFocusMask -> MaxAllowedForMinNonFocusBits
+ size < MinFocusMask -> MaxAllowedForMinFocusBits
+ size < MaxFocusMask -> MaxAllowedForMaxFocusBits
+ else -> invalidSize(size)
}
}
@@ -487,7 +511,7 @@
requirePrecondition(minWidth >= 0 && minHeight >= 0) {
"minWidth($minWidth) and minHeight($minHeight) must be >= 0"
}
- return Constraints.createConstraints(minWidth, maxWidth, minHeight, maxHeight)
+ return createConstraints(minWidth, maxWidth, minHeight, maxHeight)
}
/**
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
index 42101b0..4d5cfec 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
@@ -29,6 +29,10 @@
throw IllegalArgumentException(message)
}
+internal fun throwIllegalArgumentExceptionNoReturn(message: String): Nothing {
+ throw IllegalArgumentException(message)
+}
+
// Like Kotlin's require() but without the .toString() call
@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's require()
@OptIn(ExperimentalContracts::class)
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ea780c4..e647af0 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -3637,7 +3637,7 @@
@androidx.compose.runtime.Immutable public final class PopupProperties {
ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled);
- ctor @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
+ ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled);
method public boolean getClippingEnabled();
method public boolean getDismissOnBackPress();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d9d8a86..df37d14 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -3697,7 +3697,7 @@
@androidx.compose.runtime.Immutable public final class PopupProperties {
ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled);
- ctor @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
+ ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled);
method public boolean getClippingEnabled();
method public boolean getDismissOnBackPress();
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml b/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
index 81be1d2..68a48d8 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
@@ -73,5 +73,15 @@
layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|
smallestScreenSize|touchscreen|uiMode"
android:label="Painter Resources Demo" />
+
+ <activity
+ android:name=".SimpleChatActivity"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name=".SIMPLECOMPOSECHAT"/>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
new file mode 100644
index 0000000..2e10cef
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.demos
+
+import android.annotation.SuppressLint
+import android.content.LocusId
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
+
+@SuppressLint("ClassVerificationFailure")
+class SimpleChatActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // in ActivityCompat the `setLocusContext` method is a no-op on lower version so we just
+ // check the version here instead of using the compat version of activity
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Always set locus id as 1
+ this.setLocusContext(LocusId("1"), null)
+ }
+ setContent { MaterialTheme { SimpleChatPage() } }
+ }
+}
+
+private data class Message(val content: String, val isReceived: Boolean = true)
+
+@SuppressLint("NullAnnotationGroup")
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun SimpleChatPage() {
+ val messages = remember { mutableStateListOf<Message>() }
+ val listState = rememberLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+ Scaffold(
+ modifier = Modifier.semantics { testTagsAsResourceId = true },
+ topBar = {
+ TopAppBar(
+ elevation = 4.dp,
+ title = {
+ Text(
+ "Conversation Page",
+ modifier = Modifier.testTag("tool_bar_name"),
+ fontSize = 30.sp
+ )
+ }
+ )
+ },
+ bottomBar = {
+ MessageUpdater(
+ onMessageAdded = { message, isReceived ->
+ messages.add(Message(message, isReceived = isReceived))
+ coroutineScope.launch { listState.animateScrollToItem(messages.size) }
+ }
+ )
+ }
+ ) { contentPadding ->
+ Box(modifier = Modifier.padding(contentPadding)) {
+ // testTagsAsResourceId and testTag is for compose to map testTag to resource-id.
+ // https://developer.android.com/jetpack/compose/testing#uiautomator-interop
+ SelectionContainer() {
+ Column {
+ Conversation(messages, listState)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Conversation(messages: List<Message>, state: LazyListState) {
+ LazyColumn(
+ modifier = Modifier
+ .testTag("messages")
+ .fillMaxSize(),
+ state = state,
+ verticalArrangement = Arrangement.Bottom,
+ ) {
+ items(messages) { MessageCard(it) }
+ }
+}
+
+@Composable
+private fun MessageCard(message: Message) {
+ Row {
+ if (!message.isReceived) {
+ Spacer(modifier = Modifier.weight(1.0f))
+ }
+ Column {
+ Text(
+ message.content,
+ fontSize = 20.sp,
+ modifier = Modifier.testTag(
+ if (message.isReceived) "message_received" else "message_sent"
+ )
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun MessageUpdater(onMessageAdded: (message: String, isReceived: Boolean) -> Unit) {
+ Row {
+ var text by remember { mutableStateOf("") }
+
+ TextField(
+ modifier = Modifier.weight(1.0f),
+ value = text,
+ onValueChange = { text = it },
+ placeholder = { Text("Input message here") }
+ )
+
+ Button(
+ onClick = {
+ if (text.isNotEmpty()) {
+ onMessageAdded(text, true)
+ text = ""
+ }
+ }
+ ) {
+ Text("Receive")
+ }
+
+ Button(
+ onClick = {
+ if (text.isNotEmpty()) {
+ onMessageAdded(text, false)
+ text = ""
+ }
+ }
+ ) {
+ Text("Send")
+ }
+ }
+}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 2bbfa78..e1461ae 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -275,6 +275,7 @@
RecyclerViewDemos,
AccessibilityDemos,
ComposableDemo("Screen coordinates") { ScreenCoordinatesDemo(it) },
- ComposableDemo("Clipboard") { ClipboardDemo() }
+ ComposableDemo("Clipboard") { ClipboardDemo() },
+ ActivityDemo("Simple chat", SimpleChatActivity::class)
)
)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index e9f3640..d9e4578 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -80,7 +80,9 @@
import androidx.compose.material.DrawerValue
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FabPosition
+import androidx.compose.material.FilterChip
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -236,6 +238,7 @@
import org.mockito.kotlin.verify
@LargeTest
+@OptIn(ExperimentalMaterialApi::class)
@RunWith(AndroidJUnit4::class)
class AndroidAccessibilityTest {
@get:Rule
@@ -459,13 +462,14 @@
with(AccessibilityNodeInfoCompat.wrap(info)) {
assertThat(className).isEqualTo("android.view.View")
assertThat(stateDescription).isEqualTo("Selected")
- assertThat(isClickable).isFalse()
+ assertThat(isClickable).isTrue()
assertThat(isCheckable).isTrue()
assertThat(isVisibleToUser).isTrue()
assertThat(actionList)
.containsExactly(
AccessibilityActionCompat(ACTION_ACCESSIBILITY_FOCUS, null),
AccessibilityActionCompat(ACTION_FOCUS, null),
+ AccessibilityActionCompat(ACTION_CLICK, null),
)
}
}
@@ -505,6 +509,67 @@
}
@Test
+ fun testCreateAccessibilityNodeInfo_forRadioButton() {
+ // Arrange.
+ setContent {
+ Box(
+ Modifier
+ .selectable(selected = true, onClick = {}, role = Role.RadioButton)
+ .testTag(tag)) {
+ BasicText("Text")
+ }
+ }
+ val virtualId = rule.onNodeWithTag(tag).semanticsId
+
+ // Act.
+ val info = rule.runOnIdle { createAccessibilityNodeInfo(virtualId) }
+
+ // Assert.
+ rule.runOnIdle {
+ with(AccessibilityNodeInfoCompat.wrap(info)) {
+ assertThat(className).isEqualTo("android.view.View")
+ assertThat(isClickable).isFalse()
+ assertThat(isVisibleToUser).isTrue()
+ assertThat(isChecked).isTrue()
+ assertThat(actionList)
+ .containsExactly(
+ AccessibilityActionCompat(ACTION_ACCESSIBILITY_FOCUS, null),
+ AccessibilityActionCompat(ACTION_FOCUS, null),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testCreateAccessibilityNodeInfo_forFilterButton() {
+ // Arrange.
+ setContent {
+ FilterChip(selected = true, onClick = {}, modifier = Modifier.testTag(tag)) {
+ Text("Filter chip")
+ }
+ }
+ val virtualId = rule.onNodeWithTag(tag).semanticsId
+
+ // Act.
+ val info = rule.runOnIdle { createAccessibilityNodeInfo(virtualId) }
+
+ // Assert.
+ rule.runOnIdle {
+ with(AccessibilityNodeInfoCompat.wrap(info)) {
+ // We don't check for a CheckBox role since the role is found in a fake descendant.
+ assertThat(stateDescription).isEqualTo("Selected")
+ assertThat(isClickable).isTrue()
+ assertThat(isCheckable).isTrue()
+ assertThat(isChecked).isTrue()
+ assertThat(isVisibleToUser).isTrue()
+ assertThat(actionList)
+ .contains(
+ AccessibilityActionCompat(ACTION_CLICK, null),
+ )
+ }
+ }
+ }
+ @Test
fun testCreateAccessibilityNodeInfo_progressIndicator_determinate() {
// Arrange.
setContent {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 015999c..6623a0f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -1042,6 +1042,7 @@
rule.runOnIdle { assertThat(info.childCount).isEqualTo(1) }
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321824038
@Test
fun testUncoveredNodes_zeroBoundsRoot_included() {
// Arrange.
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
index 04494ed..b7386f0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
@@ -61,6 +61,7 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -78,6 +79,7 @@
private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
private val accessibilityEventLoopIntervalMs = 100L
+ @SdkSuppress(maxSdkVersion = 33) // b/322354981
@Test
fun sendScrollEvent_byStateObservation_horizontal() {
// Arrange.
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
index 0777bc1..a845d2f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
@@ -44,6 +44,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
@@ -93,6 +94,7 @@
* Because clipToBounds is being used on the parent LayoutNode, only the 4 touches inside the
* parent LayoutNode should hit.
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clipToBounds_childrenOffsetViaLayout_onlyCorrectPointersHit() {
@@ -220,6 +222,7 @@
* Because clipToBounds is being used on the parent LayoutNode, only the 4 touches inside the
* parent LayoutNode should hit.
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clipToBounds_childrenOffsetViaModifier_onlyCorrectPointersHit() {
@@ -327,6 +330,7 @@
* This test creates a layout clipped to a rounded rectangle shape (circle).
* We'll touch in and out of the rounded area.
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clip_roundedRect() {
pokeAroundCircle(RoundedCornerShape(50))
@@ -337,6 +341,7 @@
* corners are defined as larger than the side length
* We'll touch in and out of the rounded area.
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clip_roundedRectLargeCorner() {
pokeAroundCircle(RoundedCornerShape(1.1f))
@@ -346,6 +351,7 @@
* This test creates a layout clipped to a generic shape (circle).
* We'll touch in and out of the rounded area.
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clip_genericShape() {
pokeAroundCircle(
@@ -446,6 +452,7 @@
* This creates a clipped rectangle that is smaller than the bounds and ensures that only the
* clipped area receives touches
*/
+ @SdkSuppress(maxSdkVersion = 33) // b/321823104
@Test
fun clip_smallRect() {
val rectangleShape: Shape = object : Shape {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
index df0dbf1..0ffc44f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
@@ -19,6 +19,8 @@
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
+import android.net.Uri
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
@@ -38,6 +40,7 @@
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
@@ -293,6 +296,44 @@
verify(clipboardManager, times(1)).setPrimaryClip(clipData)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun firstUriOrNull_returnsFirstItem_ifNotNull() {
+ val uri = Uri.parse("http://example.com")
+ val clipData = mock<ClipData> {
+ on { itemCount } doReturn 2
+ on { getItemAt(0) } doReturn ClipData.Item(uri)
+ on { getItemAt(1) } doReturn ClipData.Item("Hello")
+ }
+
+ assertThat(clipData.toClipEntry().firstUriOrNull()).isEqualTo(uri)
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun firstUriOrNull_returnsSecondItem_ifFirstIsNull() {
+ val uri = Uri.parse("http://example.com")
+ val clipData = mock<ClipData> {
+ on { itemCount } doReturn 2
+ on { getItemAt(0) } doReturn ClipData.Item("Hello")
+ on { getItemAt(1) } doReturn ClipData.Item(uri)
+ }
+
+ assertThat(clipData.toClipEntry().firstUriOrNull()).isEqualTo(uri)
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun firstUriOrNull_returnsNull_ifNoUri() {
+ val clipData = mock<ClipData> {
+ on { itemCount } doReturn 2
+ on { getItemAt(0) } doReturn ClipData.Item("Hello")
+ on { getItemAt(1) } doReturn ClipData.Item("World")
+ }
+
+ assertThat(clipData.toClipEntry().firstUriOrNull()).isNull()
+ }
+
private fun assertEncodeAndDecode(spanStyle: SpanStyle) {
val encoder = EncodeHelper()
encoder.encode(spanStyle)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
index 28504fb..a48ccc0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
@@ -45,6 +45,7 @@
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
@@ -92,6 +93,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun positionOnScreen_withNoComposableOffset() {
rule.runOnIdle {
@@ -107,6 +109,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun positionOnScreen_withComposableOffset() {
rule.runOnIdle {
@@ -123,6 +126,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun positionOnScreen_changesAfterUpdate() {
rule.runOnIdle {
@@ -150,6 +154,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun screenToLocal_withNoComposableOffset() {
rule.runOnIdle {
@@ -165,6 +170,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun screenToLocal_withComposableOffset() {
rule.runOnIdle {
@@ -181,6 +187,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun transformToScreen_fromIdentity_withNoComposableOffset() {
val matrix = Matrix()
@@ -198,6 +205,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun transformToScreen_fromIdentity_withComposableOffset() {
val matrix = Matrix()
@@ -216,6 +224,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun transformToScreen_changesAfterUpdate() {
val matrix = Matrix()
@@ -245,6 +254,7 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/321823937
@Test
fun transformToScreen_fromTransformedMatrix_includesExistingTransformation() {
val matrix = Matrix()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
index df8fecb..c8cde80 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
@@ -31,7 +31,6 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
@@ -363,7 +362,6 @@
rule.onNodeWithTag(testTag).assertIsDisplayed()
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
fun canFillScreenWidth_dependingOnProperty() {
var box1Width = 0
@@ -385,7 +383,6 @@
}
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
fun canChangeSize() {
var width by mutableStateOf(10.dp)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
index 6dc75ce..9ff7b8a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
@@ -37,13 +37,11 @@
import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.platform.AndroidComposeView
-import androidx.compose.ui.platform.ScrollObservationScope
import androidx.compose.ui.platform.SemanticsNodeCopy
import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
import androidx.compose.ui.platform.coreshims.ViewCompatShims
import androidx.compose.ui.platform.coreshims.ViewStructureCompat
-import androidx.compose.ui.platform.findById
import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToIntObjectMap
import androidx.compose.ui.platform.getTextLayoutResult
import androidx.compose.ui.platform.toLegacyClassName
@@ -56,7 +54,6 @@
import androidx.compose.ui.util.fastJoinToString
import androidx.compose.ui.util.fastMap
import androidx.core.util.keyIterator
-import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.util.function.Consumer
@@ -292,11 +289,6 @@
}
}
- private val scrollObservationScopes = mutableListOf<ScrollObservationScope>()
- private val scheduleScrollEventIfNeededLambda: (ScrollObservationScope) -> Unit = {
- this.scheduleScrollEventIfNeeded(it)
- }
-
// Analogous to `sendSemanticsPropertyChangeEvents`
private fun checkForContentCapturePropertyChanges(
newSemanticsNodes: IntObjectMap<SemanticsNodeWithAdjustedBounds>
@@ -331,81 +323,11 @@
sendContentCaptureTextUpdateEvent(newNode.id, newText.toString())
}
}
- SemanticsProperties.VerticalScrollAxisRange -> {
- notifySubtreeStateChangeIfNeeded(newNode.layoutNode)
- val scope = checkPreconditionNotNull(scrollObservationScopes.findById(id)) {
- "scroll observation scope does not exist"
- }
- scope.horizontalScrollAxisRange = newNode.unmergedConfig.getOrNull(
- SemanticsProperties.HorizontalScrollAxisRange
- )
- scope.verticalScrollAxisRange = newNode.unmergedConfig.getOrNull(
- SemanticsProperties.VerticalScrollAxisRange
- )
- scheduleScrollEventIfNeeded(scope)
- }
}
}
}
}
- // TODO(mnuzen): this code is copied over from a11y delegate, we want to centralize some
- // of it into `SemanticsManager` to keep the code cleaner.
- private fun scheduleScrollEventIfNeeded(scrollObservationScope: ScrollObservationScope) {
- if (!scrollObservationScope.isValidOwnerScope) {
- return
- }
- view.snapshotObserver.observeReads(
- scrollObservationScope,
- scheduleScrollEventIfNeededLambda
- ) {
- val newXState = scrollObservationScope.horizontalScrollAxisRange
- val newYState = scrollObservationScope.verticalScrollAxisRange
- val oldXValue = scrollObservationScope.oldXValue
- val oldYValue = scrollObservationScope.oldYValue
-
- val deltaX = if (newXState != null && oldXValue != null) {
- newXState.value() - oldXValue
- } else {
- 0f
- }
- val deltaY = if (newYState != null && oldYValue != null) {
- newYState.value() - oldYValue
- } else {
- 0f
- }
-
- if (deltaX != 0f || deltaY != 0f) {
- val scrollerId = semanticsNodeIdToAccessibilityVirtualNodeId(
- scrollObservationScope.semanticsNodeId
- )
-
- view.invalidate()
-
- currentSemanticsNodes[scrollerId]?.semanticsNode?.layoutNode?.let { layoutNode ->
- // Schedule a content subtree change event for the scroller. As side effects
- // this will also schedule a TYPE_VIEW_SCROLLED event, and suppress separate
- // events from being sent for each child whose bounds moved.
- notifySubtreeStateChangeIfNeeded(layoutNode)
- }
- }
-
- if (newXState != null) {
- scrollObservationScope.oldXValue = newXState.value()
- }
- if (newYState != null) {
- scrollObservationScope.oldYValue = newYState.value()
- }
- }
- }
-
- private fun semanticsNodeIdToAccessibilityVirtualNodeId(id: Int): Int {
- if (id == view.semanticsOwner.unmergedRootSemanticsNode.id) {
- return AccessibilityNodeProviderCompat.HOST_VIEW_ID
- }
- return id
- }
-
private fun sendContentCaptureTextUpdateEvent(id: Int, newText: String) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 064defc..c8f31f0 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -21,7 +21,12 @@
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
-import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.M
+import android.os.Build.VERSION_CODES.N
+import android.os.Build.VERSION_CODES.O
+import android.os.Build.VERSION_CODES.Q
+import android.os.Build.VERSION_CODES.S
import android.os.Looper
import android.os.SystemClock
import android.util.LongSparseArray
@@ -506,7 +511,7 @@
context.resources.configuration.fontWeightAdjustmentCompat
private val Configuration.fontWeightAdjustmentCompat: Int
- get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) fontWeightAdjustment else 0
+ get() = if (SDK_INT >= S) fontWeightAdjustment else 0
// Backed by mutableStateOf so that the ambient provider recomposes when it changes
override var layoutDirection by mutableStateOf(
@@ -643,11 +648,8 @@
}
}
- private val matrixToWindow = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- CalculateMatrixToWindowApi29()
- } else {
- CalculateMatrixToWindowApi21(tmpMatrix)
- }
+ private val matrixToWindow =
+ if (SDK_INT < Q) CalculateMatrixToWindowApi21(tmpMatrix) else CalculateMatrixToWindowApi29()
/**
* Keyboard modifiers state might be changed when window is not focused, so window doesn't
@@ -664,7 +666,7 @@
addOnAttachStateChangeListener(contentCaptureManager)
setWillNotDraw(false)
isFocusable = true
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (SDK_INT >= O) {
AndroidComposeViewVerificationHelperMethodsO.focusable(
this,
focusable = View.FOCUSABLE,
@@ -677,10 +679,9 @@
ViewRootForTest.onViewCreatedCallback?.invoke(this)
setOnDragListener(this.dragAndDropModifierOnDragListener)
root.attach(this)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- // Support for this feature in Compose is tracked here: b/207654434
- AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
- }
+
+ // Support for this feature in Compose is tracked here: b/207654434
+ if (SDK_INT >= Q) AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
}
/**
@@ -892,18 +893,20 @@
drawDragDecoration = drawDragDecoration,
)
@Suppress("DEPRECATION")
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ return if (SDK_INT >= N) {
AndroidComposeViewStartDragAndDropN.startDragAndDrop(
view = this,
transferData = transferData,
dragShadowBuilder = shadowBuilder,
)
- else startDrag(
- transferData.clipData,
- shadowBuilder,
- transferData.localState,
- transferData.flags,
- )
+ } else {
+ startDrag(
+ transferData.clipData,
+ shadowBuilder,
+ transferData.localState,
+ transferData.flags,
+ )
+ }
}
private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
@@ -1255,10 +1258,7 @@
// We can't be confident that RenderNode is supported, so we try and fail over to
// the ViewLayer implementation. We'll try even on on P devices, but it will fail
// until ART allows things on the unsupported list on P.
- if (isHardwareAccelerated &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
- isRenderNodeCompatible
- ) {
+ if (isHardwareAccelerated && SDK_INT >= M && isRenderNodeCompatible) {
try {
return RenderNodeLayer(
this,
@@ -1294,8 +1294,9 @@
// wasn't easy to decode, so this work-around keeps up to 10 Views active
// only for L. On other versions, it uses the WeakHashMap to retain as many
// as are convenient.
- val cacheValue = viewLayersContainer == null || ViewLayer.shouldUseDispatchDraw ||
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ||
+ val cacheValue = viewLayersContainer == null ||
+ ViewLayer.shouldUseDispatchDraw ||
+ SDK_INT >= M ||
layerCache.size < MaximumLayerCacheSize
if (cacheValue) {
layerCache.push(layer)
@@ -1500,7 +1501,7 @@
viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (SDK_INT >= S) {
AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(
this,
AndroidComposeViewTranslationCallback()
@@ -1525,9 +1526,7 @@
viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
- }
+ if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
}
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
@@ -1538,7 +1537,7 @@
if (autofillSupported()) _autofill?.performAutofill(values)
}
- @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresApi(S)
override fun onCreateVirtualViewTranslationRequests(
virtualIds: LongArray,
supportedFormats: IntArray,
@@ -1549,7 +1548,7 @@
)
}
- @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresApi(S)
override fun onVirtualViewTranslationResponses(
response: LongSparseArray<ViewTranslationResponse?>
) {
@@ -1930,7 +1929,7 @@
}
}
- private fun autofillSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ private fun autofillSupported() = SDK_INT >= O
public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
if (hoverExitReceived) {
@@ -1984,12 +1983,9 @@
if (!eventInvalid) {
// First event x,y is checked above if block, so we can skip index 0.
for (index in 1 until event.pointerCount) {
- eventInvalid = !event.getX(index).isFinite() || !event.getY(index).isFinite() ||
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- !isValidMotionEvent(event, index)
- } else {
- false
- }
+ eventInvalid = !event.getX(index).isFinite() ||
+ !event.getY(index).isFinite() ||
+ (SDK_INT >= Q && !isValidMotionEvent(event, index))
if (eventInvalid) break
}
@@ -2011,7 +2007,7 @@
accessibilityId: Int,
currentView: View
): View? {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (SDK_INT < Q) {
val getAccessibilityViewIdMethod = Class.forName("android.view.View")
.getDeclaredMethod("getAccessibilityViewId")
getAccessibilityViewIdMethod.isAccessible = true
@@ -2043,7 +2039,7 @@
override fun setIcon(value: PointerIcon?) {
currentIcon = value ?: PointerIcon.Default
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ if (SDK_INT >= N) {
AndroidComposeViewVerificationHelperMethodsN.setPointerIcon(
this@AndroidComposeView,
currentIcon
@@ -2070,7 +2066,7 @@
// invoke the hidden parent method after Android P. If in new android, the hidden method
// ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
// simply return null here because there will be no call to this method.
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ return if (SDK_INT >= Q) {
val findViewByAccessibilityIdTraversalMethod = Class.forName("android.view.View")
.getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
findViewByAccessibilityIdTraversalMethod.isAccessible = true
@@ -2125,7 +2121,7 @@
val savedStateRegistryOwner: SavedStateRegistryOwner
)
- @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresApi(S)
private class AndroidComposeViewTranslationCallback : ViewTranslationCallback {
override fun onShowTranslation(view: View): Boolean {
val androidComposeView = view as AndroidComposeView
@@ -2152,9 +2148,9 @@
* AOT compiled. It is expected that this class will soft-fail verification, but the classes
* which use this method will pass.
*/
-@RequiresApi(Build.VERSION_CODES.O)
+@RequiresApi(O)
private object AndroidComposeViewVerificationHelperMethodsO {
- @RequiresApi(Build.VERSION_CODES.O)
+ @RequiresApi(O)
@DoNotInline
fun focusable(view: View, focusable: Int, defaultFocusHighlightEnabled: Boolean) {
view.focusable = focusable
@@ -2163,10 +2159,10 @@
}
}
-@RequiresApi(Build.VERSION_CODES.N)
+@RequiresApi(N)
private object AndroidComposeViewVerificationHelperMethodsN {
@DoNotInline
- @RequiresApi(Build.VERSION_CODES.N)
+ @RequiresApi(N)
fun setPointerIcon(view: View, icon: PointerIcon?) {
val iconToSet = when (icon) {
is AndroidPointerIcon ->
@@ -2188,25 +2184,25 @@
}
}
-@RequiresApi(Build.VERSION_CODES.Q)
+@RequiresApi(Q)
private object AndroidComposeViewForceDarkModeQ {
@DoNotInline
- @RequiresApi(Build.VERSION_CODES.Q)
+ @RequiresApi(Q)
fun disallowForceDark(view: View) {
view.isForceDarkAllowed = false
}
}
-@RequiresApi(Build.VERSION_CODES.S)
+@RequiresApi(S)
internal object AndroidComposeViewTranslationCallbackS {
@DoNotInline
- @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresApi(S)
fun setViewTranslationCallback(view: View, translationCallback: ViewTranslationCallback) {
view.setViewTranslationCallback(translationCallback)
}
@DoNotInline
- @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresApi(S)
fun clearViewTranslationCallback(view: View) {
view.clearViewTranslationCallback()
}
@@ -2274,7 +2270,7 @@
fun calculateMatrixToWindow(view: View, matrix: Matrix)
}
-@RequiresApi(Build.VERSION_CODES.Q)
+@RequiresApi(Q)
private class CalculateMatrixToWindowApi29 : CalculateMatrixToWindow {
private val tmpMatrix = android.graphics.Matrix()
private val tmpPosition = IntArray(2)
@@ -2351,10 +2347,10 @@
}
}
-@RequiresApi(Build.VERSION_CODES.N)
+@RequiresApi(N)
private object AndroidComposeViewStartDragAndDropN {
@DoNotInline
- @RequiresApi(Build.VERSION_CODES.N)
+ @RequiresApi(N)
fun startDragAndDrop(
view: View,
transferData: DragAndDropTransferData,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 1ed409d..f3d1165 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -52,6 +52,7 @@
import androidx.collection.mutableIntListOf
import androidx.collection.mutableIntObjectMapOf
import androidx.collection.mutableIntSetOf
+import androidx.collection.mutableLongObjectMapOf
import androidx.collection.mutableObjectIntMapOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.R
@@ -95,6 +96,7 @@
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastJoinToString
import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.packInts
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE
@@ -887,11 +889,13 @@
}
info.isClickable = false
semanticsNode.unmergedConfig.getOrNull(SemanticsActions.OnClick)?.let {
- // Selectable items that are already selected should not announce it again
+ // Selectable tabs and radio buttons that are already selected cannot be selected again
+ // so they should not be exposed as clickable.
val isSelected =
semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected) == true
- info.isClickable = !isSelected
- if (semanticsNode.enabled() && !isSelected) {
+ val isRadioButtonOrTab = role == Role.Tab || role == Role.RadioButton
+ info.isClickable = !isRadioButtonOrTab || (isRadioButtonOrTab && !isSelected)
+ if (semanticsNode.enabled() && info.isClickable) {
info.addAction(
AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK,
@@ -1375,38 +1379,56 @@
// This needs to be here instead of around line 3000 because we need access to the `view`
// that is inside the `AndroidComposeViewAccessibilityDelegateCompat` class
- @OptIn(InternalTextApi::class)
private fun getInfoText(
node: SemanticsNode
+ ): AnnotatedString? {
+ val editableTextToAssign = node.unmergedConfig.getTextForTextField()
+ val textToAssign = node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
+ return editableTextToAssign ?: textToAssign
+ }
+
+ @OptIn(InternalTextApi::class)
+ private fun AnnotatedString.toSpannableString(
+ node: SemanticsNode
): SpannableString? {
val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
- val editableTextToAssign = trimToSize(
- node.unmergedConfig.getTextForTextField()
- ?.toAccessibilitySpannableString(
- density = view.density,
- fontFamilyResolver,
- urlSpanCache
- ),
- ParcelSafeTextLength
- )
- val textToAssign = trimToSize(
- node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
- ?.toAccessibilitySpannableString(
- density = view.density,
- fontFamilyResolver,
- urlSpanCache
- ),
+ val linkActions = if (hasLinkAnnotations(0, length)) {
+ // handle hyperlinks in text
+ val rangesToCustomActions = mutableLongObjectMapOf<() -> Unit>()
+ node.children.fastForEach { childNode ->
+ // hyperlink nodes pass onLinkClick actions through custom actions
+ val config = childNode.unmergedConfig
+ if (config.contains(SemanticsProperties.TextSelectionRange)) {
+ val range = config[SemanticsProperties.TextSelectionRange]
+ val action = config.getOrNull(CustomActions)?.firstOrNull()?.action
+ action?.let {
+ rangesToCustomActions[packInts(range.start, range.end)] = { it() }
+ }
+ }
+ }
+ rangesToCustomActions
+ } else {
+ null
+ }
+
+ return trimToSize(
+ toAccessibilitySpannableString(
+ density = view.density,
+ fontFamilyResolver = fontFamilyResolver,
+ urlSpanCache = urlSpanCache,
+ linkActions = linkActions,
+ accessibilityNodeId = node.id
+ ),
ParcelSafeTextLength
)
- return editableTextToAssign ?: textToAssign
}
private fun setText(
node: SemanticsNode,
info: AccessibilityNodeInfoCompat,
) {
- info.text = getInfoText(node)
+ info.text = getInfoText(node)?.toSpannableString(node)
}
/**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
index 9223794..32ec68d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
@@ -21,12 +21,17 @@
import androidx.compose.ui.ExperimentalComposeUiApi
/**
- * Returns the uri of the first item in this [ClipEntry].
+ * Returns the first non-null [Uri] from the list of [ClipData.Item]s in this [ClipEntry].
*
- * Do not forget that each [ClipEntry] can contain multiple items in its [ClipData], therefore it
- * can have multiple Uris. Always check whether you are processing all the items in a given
- * [ClipEntry].
+ * Do not forget that each [ClipEntry] can contain multiple [ClipData.Item]s in its [ClipData],
+ * therefore it can have multiple [Uri]s. Always check whether you are processing all the items in
+ * a given [ClipEntry].
*/
@ExperimentalComposeUiApi
-fun ClipEntry.firstUriOrNull(): Uri? =
- clipData.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri
+fun ClipEntry.firstUriOrNull(): Uri? {
+ for (i in 0 until clipData.itemCount) {
+ val uri = clipData.getItemAt(i).uri
+ if (uri != null) return uri
+ }
+ return null
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 818c02a..c2879c2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -54,7 +54,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.R
import androidx.compose.ui.UiComposable
@@ -113,7 +112,7 @@
* the platform default, which is smaller than the screen width.
*/
@Immutable
-actual class PopupProperties @ExperimentalComposeUiApi constructor(
+actual class PopupProperties constructor(
actual val focusable: Boolean = false,
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
@@ -136,7 +135,6 @@
clippingEnabled = clippingEnabled,
)
- @OptIn(ExperimentalComposeUiApi::class)
constructor(
focusable: Boolean = false,
dismissOnBackPress: Boolean = true,
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 157de10..0f54238 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -79,6 +79,24 @@
}
@Test
+ fun testNestedDelegatesHaveNodePointersCorrectlyUpdated() {
+ val d = DrawMod()
+ val c = DrawMod()
+ val b = object : DelegatingNode() {
+ val c = delegate(c)
+ }
+ val a = object : DelegatingNode() {
+ val b = delegate(b)
+ val d = delegate(d)
+ }
+
+ assert(a.node === a)
+ assert(b.node === a)
+ assert(c.node === a)
+ assert(d.node === a)
+ }
+
+ @Test
fun testAsKindReturnsNestedDelegate() {
val node = DelegatedWrapper { DelegatedWrapper { DrawMod() } }
assert(node.isKind(Nodes.Draw))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 4117eac3..bc482d1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -369,7 +369,7 @@
requireOwner().registerOnEndApplyChangesListener(effect)
}
- internal fun setAsDelegateTo(owner: Node) {
+ internal open fun setAsDelegateTo(owner: Node) {
node = owner
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
index b335162..693613b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
@@ -22,11 +22,15 @@
// This function exists so we do *not* inline the throw. It keeps
// the call site much smaller and since it's the slow path anyway,
// we don't mind the extra function call
-internal fun throwIllegalStateException(message: String): Nothing {
+internal fun throwIllegalStateException(message: String) {
throw IllegalStateException(message)
}
-internal fun throwIllegalArgumentException(message: String): Nothing {
+internal fun throwIllegalStateExceptionForNullCheck(message: String): Nothing {
+ throw IllegalStateException(message)
+}
+
+internal fun throwIllegalArgumentException(message: String) {
throw IllegalArgumentException(message)
}
@@ -64,7 +68,7 @@
}
if (value == null) {
- throwIllegalStateException(lazyMessage())
+ throwIllegalStateExceptionForNullCheck(lazyMessage())
}
return value
@@ -79,7 +83,7 @@
}
if (value == null) {
- throwIllegalStateException("Required value was null.")
+ throwIllegalStateExceptionForNullCheck("Required value was null.")
}
return value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 0bb8838..255fa31 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -35,8 +35,8 @@
interface DelegatableNode {
/**
* A reference of the [Modifier.Node] that holds this node's position in the node hierarchy. If
- * the node is a delegate of another node, this will point to that node. Otherwise, this will
- * point to itself.
+ * the node is a delegate of another node, this will point to the root delegating node that is
+ * actually part of the node tree. Otherwise, this will point to itself.
*/
val node: Modifier.Node
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 44963ed..099e391 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -56,6 +56,16 @@
@TestOnly
internal fun undelegateUnprotected(instance: DelegatableNode) = undelegate(instance)
+ override fun setAsDelegateTo(owner: Modifier.Node) {
+ super.setAsDelegateTo(owner)
+ // At this point _this_ node is being delegated to, however _this_ node may also
+ // have delegates of its own, and their current `node` pointers need to be updated
+ // so that they point to the right node in the tree.
+ forEachImmediateDelegate {
+ it.setAsDelegateTo(owner)
+ }
+ }
+
/**
* In order to properly delegate work to another [Modifier.Node], the delegated instance must
* be created and returned inside of a [delegate] call. Doing this will
diff --git a/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
index f7cba81..a6bd4f3 100644
--- a/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
+++ b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
@@ -24,4 +24,9 @@
add_library(shared_counter STATIC shared_counter.cc)
add_library(datastore_shared_counter SHARED jni/androidx_datastore_core_SharedCounter.cc)
-target_link_libraries(datastore_shared_counter shared_counter)
\ No newline at end of file
+target_link_libraries(datastore_shared_counter shared_counter)
+target_link_options(
+ datastore_shared_counter
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
index 005698f7..3c1ddca 100644
--- a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
+++ b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
@@ -9,4 +9,6 @@
set_property(TARGET <NAME>
APPEND_STRING PROPERTY
LINK_FLAGS
- " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_scripts/libname.map.txt")
\ No newline at end of file
+ " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_scripts/libname.map.txt")
+
+target_link_options(<NAME> PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 6abd071..7414d46 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -248,31 +248,31 @@
docs("androidx.media2:media2-widget:1.3.0")
docs("androidx.media:media:1.7.0")
// androidx.media3 is not hosted in androidx
- docsWithoutApiSince("androidx.media3:media3-cast:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-common:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-container:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-database:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-datasource:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-decoder:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-effect:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-extractor:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-muxer:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-session:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-test-utils:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-transformer:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-ui:1.3.0-alpha01")
- docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.3.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-cast:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-common:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-container:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-database:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-datasource:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-decoder:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-effect:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-extractor:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-muxer:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-session:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-transformer:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-ui:1.3.0-beta01")
+ docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.3.0-beta01")
docs("androidx.mediarouter:mediarouter:1.7.0-alpha01")
docs("androidx.mediarouter:mediarouter-testing:1.7.0-alpha01")
docs("androidx.metrics:metrics-performance:1.0.0-beta01")
@@ -356,25 +356,25 @@
docs("androidx.startup:startup-runtime:1.2.0-alpha02")
docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
// androidx.test is not hosted in androidx\
- docsWithoutApiSince("androidx.test:core:1.6.0-alpha03")
- docsWithoutApiSince("androidx.test:core-ktx:1.6.0-alpha03")
- docsWithoutApiSince("androidx.test:monitor:1.7.0-alpha03")
- docsWithoutApiSince("androidx.test:rules:1.6.0-alpha02")
- docsWithoutApiSince("androidx.test:runner:1.6.0-alpha05")
- docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.0-alpha07")
- docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.0-alpha02")
- docsWithoutApiSince("androidx.test.ext:junit:1.2.0-alpha02")
- docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.0-alpha02")
- docsWithoutApiSince("androidx.test.ext:truth:1.6.0-alpha02")
- docsWithoutApiSince("androidx.test.services:storage:1.5.0-alpha02")
+ docsWithoutApiSince("androidx.test:core:1.6.0-alpha05")
+ docsWithoutApiSince("androidx.test:core-ktx:1.6.0-alpha05")
+ docsWithoutApiSince("androidx.test:monitor:1.7.0-alpha04")
+ docsWithoutApiSince("androidx.test:rules:1.6.0-alpha03")
+ docsWithoutApiSince("androidx.test:runner:1.6.0-alpha06")
+ docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.0-alpha08")
+ docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.ext:junit:1.2.0-alpha03")
+ docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.0-alpha03")
+ docsWithoutApiSince("androidx.test.ext:truth:1.6.0-alpha03")
+ docsWithoutApiSince("androidx.test.services:storage:1.5.0-alpha03")
docsWithoutApiSince("androidx.test.uiautomator:uiautomator:2.3.0-beta01")
// androidx.textclassifier is not hosted in androidx
docsWithoutApiSince("androidx.textclassifier:textclassifier:1.0.0-alpha04")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index c7d7e44..7357f62 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -22,7 +22,6 @@
// ads-identifier is deprecated
kmpDocs(project(":annotation:annotation"))
docs(project(":annotation:annotation-experimental"))
- docs(project(":annotation:annotation-replacewith"))
docs(project(":appactions:builtintypes:builtintypes"))
samples(project(":appactions:builtintypes:builtintypes:builtintypes-samples"))
docs(project(":appactions:interaction:interaction-capabilities-communication"))
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt
new file mode 100644
index 0000000..6e78f50
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatImageView
+
+/**
+ * Emoji picker popup view with bidirectional UI design to switch emoji to face left or right.
+ */
+internal class EmojiPickerPopupBidirectionalDesign(
+ override val context: Context,
+ override val targetEmojiView: View,
+ override val variants: List<String>,
+ override val popupView: LinearLayout,
+ override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+ private var emojiFacingLeft = true
+
+ init {
+ updateTemplate()
+ }
+ override fun addLayoutHeader() {
+ val row = LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER
+ layoutParams = LinearLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ FrameLayout.inflate(context, R.layout.emoji_picker_popup_bidirectional, row)
+ .findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
+ .apply {
+ layoutParams = LinearLayout.LayoutParams(
+ targetEmojiView.width, targetEmojiView.height)
+ }
+ popupView.addView(row)
+ val imageView =
+ row.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
+ imageView.setOnClickListener {
+ emojiFacingLeft = !emojiFacingLeft
+ updateTemplate()
+ popupView.removeViews( /* start= */1, getActualNumberOfRows())
+ addRowsToPopupView()
+ }
+ }
+
+ override fun getNumberOfRows(): Int {
+ // Adding one row for the bidirectional switcher.
+ return variants.size / 2 / BIDIRECTIONAL_COLUMN_COUNT + 1
+ }
+ override fun getNumberOfColumns(): Int {
+ return BIDIRECTIONAL_COLUMN_COUNT
+ }
+
+ private fun getActualNumberOfRows(): Int {
+ // Removing one extra row of the bidirectional switcher.
+ return getNumberOfRows() - 1
+ }
+
+ private fun updateTemplate() {
+ template = if (emojiFacingLeft)
+ arrayOf((variants.indices.filter { it % 12 < 6 }.map { it + 1 }).toIntArray())
+ else
+ arrayOf((variants.indices.filter { it % 12 >= 6 }.map { it + 1 }).toIntArray())
+
+ val row = getActualNumberOfRows()
+ val column = getNumberOfColumns()
+ val overrideTemplate = Array(row) { IntArray(column) }
+ var index = 0
+ for (i in 0 until row) {
+ for (j in 0 until column) {
+ if (index < template[0].size) {
+ overrideTemplate[i][j] = template[0][index]
+ index++
+ }
+ }
+ }
+ template = overrideTemplate
+ }
+
+ companion object {
+ private const val BIDIRECTIONAL_COLUMN_COUNT = 6
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt
new file mode 100644
index 0000000..4688bf3
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view UI design. Each UI design needs to inherit this abstract class.
+ */
+internal abstract class EmojiPickerPopupDesign {
+ abstract val context: Context
+ abstract val targetEmojiView: View
+ abstract val variants: List<String>
+ abstract val popupView: LinearLayout
+ abstract val emojiViewOnClickListener: View.OnClickListener
+ lateinit var template: Array<IntArray>
+ open fun addLayoutHeader() {
+ // no-ops
+ }
+ open fun addRowsToPopupView() {
+ for (row in template) {
+ val rowLayout = LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ layoutParams = LinearLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ }
+ for (item in row) {
+ val cell =
+ if (item == 0) {
+ EmojiView(context)
+ } else {
+ EmojiView(context).apply {
+ willDrawVariantIndicator = false
+ emoji = variants[item - 1]
+ setOnClickListener(emojiViewOnClickListener)
+ if (item == 1) {
+ // Hover on the first emoji in the popup
+ popupView.post {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ }
+ }
+ }.apply {
+ layoutParams = ViewGroup.LayoutParams(
+ targetEmojiView.width, targetEmojiView.height)
+ }
+ rowLayout.addView(cell)
+ }
+ popupView.addView(rowLayout)
+ }
+ }
+
+ open fun addLayoutFooter() {
+ // no-ops
+ }
+
+ abstract fun getNumberOfRows(): Int
+ abstract fun getNumberOfColumns(): Int
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt
new file mode 100644
index 0000000..916a473
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view with flat design to list emojis.
+ */
+internal class EmojiPickerPopupFlatDesign(
+ override val context: Context,
+ override val targetEmojiView: View,
+ override val variants: List<String>,
+ override val popupView: LinearLayout,
+ override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+ init {
+ template = arrayOf(variants.indices.map { it + 1 }.toIntArray())
+ var row = getNumberOfRows()
+ var column = getNumberOfColumns()
+ val overrideTemplate = Array(row) { IntArray(column) }
+ var index = 0
+ for (i in 0 until row) {
+ for (j in 0 until column) {
+ if (index < template[0].size) {
+ overrideTemplate[i][j] = template[0][index]
+ index++
+ }
+ }
+ }
+ template = overrideTemplate
+ }
+ override fun getNumberOfRows(): Int {
+ val column = getNumberOfColumns()
+ return variants.size / column + if (variants.size % column == 0) 0 else 1
+ }
+ override fun getNumberOfColumns(): Int {
+ return minOf(FLAT_COLUMN_MAX_COUNT, template[0].size)
+ }
+
+ companion object {
+ private const val FLAT_COLUMN_MAX_COUNT = 6
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt
new file mode 100644
index 0000000..84f4595
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view with square design.
+ */
+internal class EmojiPickerPopupSquareDesign(
+ override val context: Context,
+ override val targetEmojiView: View,
+ override val variants: List<String>,
+ override val popupView: LinearLayout,
+ override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+ init {
+ template = SQUARE_LAYOUT_TEMPLATE
+ }
+ override fun getNumberOfRows(): Int {
+ return SQUARE_LAYOUT_TEMPLATE.size
+ }
+ override fun getNumberOfColumns(): Int {
+ return SQUARE_LAYOUT_TEMPLATE[0].size
+ }
+
+ companion object {
+ /**
+ * Square variant layout template without skin tone.
+ * 0 : a place holder
+ * Positive number is the index + 1 in the variant array
+ */
+ private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
+ intArrayOf(0, 2, 3, 4, 5, 6),
+ intArrayOf(0, 7, 8, 9, 10, 11),
+ intArrayOf(0, 12, 13, 14, 15, 16),
+ intArrayOf(0, 17, 18, 19, 20, 21),
+ intArrayOf(1, 22, 23, 24, 25, 26)
+ )
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt
new file mode 100644
index 0000000..3bac29a
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+
+/**
+ * Emoji picker popup view with square design which has skin tone circles to indicate the
+ * combination of the skin tones.
+ */
+internal class EmojiPickerPopupSquareWithSkintoneCircleDesign(
+ override val context: Context,
+ override val targetEmojiView: View,
+ override val variants: List<String>,
+ override val popupView: LinearLayout,
+ override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+ override fun addRowsToPopupView() {
+ for (row in SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE) {
+ val rowLayout = LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ layoutParams = LinearLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ }
+ for (item in row) {
+ val cell = when (item) {
+ in 1..variants.size ->
+ EmojiView(context).apply {
+ willDrawVariantIndicator = false
+ emoji = variants[item - 1]
+ setOnClickListener(emojiViewOnClickListener)
+ if (item == 1) {
+ // Hover on the first emoji in the popup
+ popupView.post {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ }
+ }
+
+ 0 -> EmojiView(context)
+
+ else -> SkinToneCircleView(context).apply {
+ paint = Paint().apply {
+ color = ContextCompat.getColor(
+ context, SKIN_TONE_COLOR_RES_IDS[item + 5])
+ style = Paint.Style.FILL
+ }
+ }
+ }.apply {
+ layoutParams = ViewGroup.LayoutParams(
+ targetEmojiView.width, targetEmojiView.height)
+ }
+ rowLayout.addView(cell)
+ }
+ popupView.addView(rowLayout)
+ }
+ }
+
+ override fun getNumberOfRows(): Int {
+ return SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE.size
+ }
+ override fun getNumberOfColumns(): Int {
+ return SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE[0].size
+ }
+
+ companion object {
+ private val SKIN_TONE_COLOR_RES_IDS = listOf(
+ R.color.light_skin_tone,
+ R.color.medium_light_skin_tone,
+ R.color.medium_skin_tone,
+ R.color.medium_dark_skin_tone,
+ R.color.dark_skin_tone
+ )
+
+ /**
+ * Square variant layout template with skin tone.
+ * 0 : a place holder
+ * -5: light skin tone circle
+ * -4: medium-light skin tone circle
+ * -3: medium skin tone circle
+ * -2: medium-dark skin tone circle
+ * -1: dark skin tone circle
+ * Positive number is the index + 1 in the variant array
+ */
+ private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
+ intArrayOf(0, 0, -5, -4, -3, -2, -1),
+ intArrayOf(0, -5, 2, 3, 4, 5, 6),
+ intArrayOf(0, -4, 7, 8, 9, 10, 11),
+ intArrayOf(0, -3, 12, 13, 14, 15, 16),
+ intArrayOf(0, -2, 17, 18, 19, 20, 21),
+ intArrayOf(1, -1, 22, 23, 24, 25, 26)
+ )
+ }
+}
+
+internal class SkinToneCircleView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : View(context, attrs) {
+ private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
+ var paint: Paint? = null
+
+ override fun draw(canvas: Canvas) {
+ super.draw(canvas)
+ canvas.apply {
+ paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
+ }
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
index 3c1f944..4921a38 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
@@ -17,17 +17,10 @@
package androidx.emoji2.emojipicker
import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
import android.util.AttributeSet
-import android.view.Gravity
import android.view.View
-import android.view.ViewGroup
-import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import android.widget.LinearLayout
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.core.content.ContextCompat
/** Popup view for emoji picker to show emoji variants. */
internal class EmojiPickerPopupView @JvmOverloads constructor(
@@ -40,150 +33,47 @@
) :
FrameLayout(context, attrs, defStyleAttr) {
private val popupView: LinearLayout
- private var layoutTemplate: LayoutTemplate
- private var emojiFacingLeft = true
-
+ private val popupDesign: EmojiPickerPopupDesign
init {
popupView = inflate(context, R.layout.variant_popup, /* root= */ null)
.findViewById<LinearLayout>(R.id.variant_popup)
- layoutTemplate = getLayoutTemplate(variants)
- if (layoutTemplate.layout == Layout.BIDIRECTIONAL) {
- addBidirectionalLayoutHeader(popupView)
+ val layout = getLayout()
+ popupDesign = when (layout) {
+ Layout.FLAT -> EmojiPickerPopupFlatDesign(
+ context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+ Layout.SQUARE -> EmojiPickerPopupSquareDesign(
+ context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+ Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> EmojiPickerPopupSquareWithSkintoneCircleDesign(
+ context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+ Layout.BIDIRECTIONAL -> EmojiPickerPopupBidirectionalDesign(
+ context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
}
- addRowsToPopupView()
+ popupDesign.addLayoutHeader()
+ popupDesign.addRowsToPopupView()
+ popupDesign.addLayoutFooter()
addView(popupView)
}
- private fun addRowsToPopupView() {
- for (row in layoutTemplate.template) {
- val rowLayout = LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- layoutParams = LinearLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
- )
- }
- for (item in row) {
- val cell = when (item) {
- in 1..variants.size ->
- EmojiView(context).apply {
- willDrawVariantIndicator = false
- emoji = variants[item - 1]
- setOnClickListener(emojiViewOnClickListener)
- if (item == 1) {
- // Hover on the first emoji in the popup
- popupView.post {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
- }
- }
- }
-
- 0 -> EmojiView(context)
-
- else -> SkinToneCircleView(context).apply {
- paint = Paint().apply {
- color = ContextCompat.getColor(
- context, SKIN_TONE_COLOR_RES_IDS[item + 5])
- style = Paint.Style.FILL
- }
- }
- }.apply {
- layoutParams = ViewGroup.LayoutParams(
- targetEmojiView.width, targetEmojiView.height)
- }
- rowLayout.addView(cell)
- }
- popupView.addView(rowLayout)
- }
- }
-
fun getPopupViewWidth(): Int {
- return layoutTemplate.numberOfColumns * targetEmojiView.width +
+ return popupDesign.getNumberOfColumns() * targetEmojiView.width +
popupView.paddingStart + popupView.paddingEnd
}
fun getPopupViewHeight(): Int {
- val numberOfRows = if (layoutTemplate.layout == Layout.BIDIRECTIONAL)
- layoutTemplate.numberOfRows + 1 else layoutTemplate.numberOfRows
- return numberOfRows * targetEmojiView.height +
+ return popupDesign.getNumberOfRows() * targetEmojiView.height +
popupView.paddingTop + popupView.paddingBottom
}
- private fun getLayoutTemplate(variants: List<String>): LayoutTemplate {
- val layout =
- if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
- if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
- Layout.SQUARE
- else Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
- else if (variants.size == BIDIRECTIONAL_VARIANTS_COUNT)
- Layout.BIDIRECTIONAL
+ private fun getLayout(): Layout {
+ if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
+ if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
+ return Layout.SQUARE
else
- Layout.FLAT
- var template = when (layout) {
- Layout.SQUARE -> SQUARE_LAYOUT_TEMPLATE
- Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE
- Layout.FLAT -> arrayOf(variants.indices.map { it + 1 }.toIntArray())
- Layout.BIDIRECTIONAL ->
- if (emojiFacingLeft)
- arrayOf((variants.indices.filter { it % 12 < 6 }.map { it + 1 }).toIntArray())
- else
- arrayOf((variants.indices.filter { it % 12 >= 6 }.map { it + 1 }).toIntArray())
- }
- val column = when (layout) {
- Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template[0].size
- Layout.FLAT, Layout.BIDIRECTIONAL -> minOf(6, template[0].size)
- }
- val row = when (layout) {
- Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template.size
- Layout.FLAT -> variants.size / column + if (variants.size % column == 0) 0 else 1
- Layout.BIDIRECTIONAL -> variants.size / 2 / column
- }
-
- // Rewrite template when the number of row mismatch
- if (row != template.size) {
- val overrideTemplate = Array(row) { IntArray(column) }
- var index = 0
- for (i in 0 until row) {
- for (j in 0 until column) {
- if (index < template[0].size) {
- overrideTemplate[i][j] = template[0][index]
- index++
- }
- }
- }
- template = overrideTemplate
- }
- return LayoutTemplate(layout, template, row, column)
- }
-
- private data class LayoutTemplate(
- var layout: Layout,
- val template: Array<IntArray>,
- val numberOfRows: Int,
- val numberOfColumns: Int
- )
-
- private fun addBidirectionalLayoutHeader(popupView: LinearLayout) {
- val row = LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER
- layoutParams = LinearLayout.LayoutParams(
- LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
- }
- inflate(context, R.layout.emoji_picker_popup_bidirectional, row)
- .findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
- .apply {
- layoutParams = LinearLayout.LayoutParams(
- targetEmojiView.width, targetEmojiView.height)
- }
- popupView.addView(row)
- val imageView =
- row.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
- imageView.setOnClickListener {
- emojiFacingLeft = !emojiFacingLeft
- layoutTemplate = getLayoutTemplate(variants)
- popupView.removeViews( /* start= */1, layoutTemplate.numberOfRows)
- addRowsToPopupView()
- }
+ return Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
+ else if (variants.size == BIDIRECTIONAL_VARIANTS_COUNT)
+ return Layout.BIDIRECTIONAL
+ else
+ return Layout.FLAT
}
companion object {
@@ -205,60 +95,5 @@
// Set of emojis that use the square layout without skin tone swatches.
private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")
-
- private val SKIN_TONE_COLOR_RES_IDS = listOf(
- R.color.light_skin_tone,
- R.color.medium_light_skin_tone,
- R.color.medium_skin_tone,
- R.color.medium_dark_skin_tone,
- R.color.dark_skin_tone
- )
-
- /**
- * Square variant layout template with skin tone.
- * 0 : a place holder
- * -5: light skin tone circle
- * -4: medium-light skin tone circle
- * -3: medium skin tone circle
- * -2: medium-dark skin tone circle
- * -1: dark skin tone circle
- * Positive number is the index + 1 in the variant array
- */
- private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
- intArrayOf(0, 0, -5, -4, -3, -2, -1),
- intArrayOf(0, -5, 2, 3, 4, 5, 6),
- intArrayOf(0, -4, 7, 8, 9, 10, 11),
- intArrayOf(0, -3, 12, 13, 14, 15, 16),
- intArrayOf(0, -2, 17, 18, 19, 20, 21),
- intArrayOf(1, -1, 22, 23, 24, 25, 26)
- )
-
- /**
- * Square variant layout template without skin tone.
- * 0 : a place holder
- * Positive number is the index + 1 in the variant array
- */
- private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
- intArrayOf(0, 2, 3, 4, 5, 6),
- intArrayOf(0, 7, 8, 9, 10, 11),
- intArrayOf(0, 12, 13, 14, 15, 16),
- intArrayOf(0, 17, 18, 19, 20, 21),
- intArrayOf(1, 22, 23, 24, 25, 26)
- )
- }
-}
-
-internal class SkinToneCircleView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null
-) : View(context, attrs) {
- private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
- var paint: Paint? = null
-
- override fun draw(canvas: Canvas) {
- super.draw(canvas)
- canvas.apply {
- paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
- }
}
}
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
index ab3be47..8216c83 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emosiekone beskikbaar nie"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jy het nog geen emosiekone gebruik nie"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
index 57a237b..f42beb0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ባንዲራዎች"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ምንም ስሜት ገላጭ ምስሎች አይገኙም"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ምንም ስሜት ገላጭ ምስሎችን እስካሁን አልተጠቀሙም"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index f286364..3567ded 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"الأعلام"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
index 60ef2a9..dea449f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"পতাকা"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনো ইম’জি উপলব্ধ নহয়"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপুনি এতিয়ালৈকে কোনো ইম’জি ব্যৱহাৰ কৰা নাই"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
index 2668f55..41d1392 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAQLAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Əlçatan emoji yoxdur"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hələ heç bir emojidən istifadə etməməsiniz"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
index baf2211..6b1da7f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emodžiji nisu dostupni"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili emodžije"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
index 29f27f4..771b4d0e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"СЦЯГІ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма даступных эмодзі"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы пакуль не выкарыстоўвалі эмодзі"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
index 0bc423e..9dd5152 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМЕНА"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма налични емоджи"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Все още не сте използвали емоджита"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
index 02e9c03..115e0c4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ফ্ল্যাগ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনও ইমোজি উপলভ্য নেই"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপনি এখনও কোনও ইমোজি ব্যবহার করেননি"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
index 29770bc..73d1e87 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji sličice nisu dostupne"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili nijednu emoji sličicu"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 251972d..7e1d139 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERES"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hi ha cap emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Encara no has fet servir cap emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
index d1c8ad9..dc70333 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nejsou k dispozici žádné smajlíky"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatím jste žádná emodži nepoužili"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
index 428d994..b09dbe4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAG"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Der er ingen tilgængelige emojis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brugt nogen emojis endnu"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
index 3bb43cb..9071a38 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGEN"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Keine Emojis verfügbar"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du hast noch keine Emojis verwendet"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
index 79d4060..e12a7b7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ΣΗΜΑΙΕΣ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Δεν υπάρχουν διαθέσιμα emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Δεν έχετε χρησιμοποιήσει κανένα emoji ακόμα"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
index ad9f1c2..7e406c9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
@@ -29,4 +29,5 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emojis yet"</string>
+ <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
index 976e527..c1af752 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
@@ -29,4 +29,5 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emojis yet"</string>
+ <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
index 1d745ad..3879270 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay ningún emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Todavía no usaste ningún emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
index bb95933..f438f76 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay emojis disponibles"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aún no has usado ningún emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
index 4ef5ed4..da92264 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"LIPUD"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ühtegi emotikoni pole saadaval"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Te pole veel ühtegi emotikoni kasutanud"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
index b72b3e6..d54498e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAK"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ez dago emotikonorik erabilgarri"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ez duzu erabili emojirik oraingoz"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
index fe48f4d..4d5c29f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"پرچمها"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"اموجی دردسترس نیست"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"هنوز از هیچ اموجیای استفاده نکردهاید"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
index f1c636e..c0c08c4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"LIPUT"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ei emojeita saatavilla"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Et ole vielä käyttänyt emojeita"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
index 1b3be2b..13e9ac7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun émoji proposé"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez encore utilisé aucun émoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
index 57fb951..3cb7ac7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez pas encore utilisé d\'emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
index 1330a9e..2bb0803 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Non hai ningún emoji dispoñible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aínda non utilizaches ningún emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
index 3ba3807..a316e9f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ઝંડા"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"કોઈ ઇમોજી ઉપલબ્ધ નથી"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"તમે હજી સુધી કોઈ ઇમોજીનો ઉપયોગ કર્યો નથી"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
index 9527a6a..a09653d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"झंडे"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोई इमोजी उपलब्ध नहीं है"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"आपने अब तक किसी भी इमोजी का इस्तेमाल नहीं किया है"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
index 3d59f54..d2ee647 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nije dostupan nijedan emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste upotrijebili emojije"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
index fcd1d30..3303bef 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ZÁSZLÓK"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nincsenek rendelkezésre álló emojik"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Még nem használt emojikat"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
index 2be01fd..86852f1 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ԴՐՈՇՆԵՐ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Հասանելի էմոջիներ չկան"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Դուք դեռ չեք օգտագործել էմոջիներ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
index e942428..053a26f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tidak ada emoji yang tersedia"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan emoji apa pun"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
index c13a86c..8986e91 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FÁNAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Engin emoji-tákn í boði"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Þú hefur ekki notað nein emoji enn"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
index 20dacd9..4307ffd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDIERE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nessuna emoji disponibile"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Non hai ancora usato alcuna emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
index 069d4bf..e155d4c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"דגלים"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"אין סמלי אמוג\'י זמינים"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"עדיין לא השתמשת באף אמוג\'י"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
index 8d6f69b..63094ef 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"旗"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"使用できる絵文字がありません"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"まだ絵文字を使用していません"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
index e256126..3662c4f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"დროშები"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji-ები მიუწვდომელია"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Emoji-ებით ჯერ არ გისარგებლიათ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
index 7537ddb..f172f31 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ЖАЛАУШАЛАР"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Эмоджи жоқ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Әлі ешқандай эмоджи пайдаланылған жоқ."</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
index a8a57f4..5b3c2e4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ទង់"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"មិនមានរូបអារម្មណ៍ទេ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"អ្នកមិនទាន់បានប្រើរូបអារម្មណ៍ណាមួយនៅឡើយទេ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
index ff09e10..c36e87d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ಫ್ಲ್ಯಾಗ್ಗಳು"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ಯಾವುದೇ ಎಮೊಜಿಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಎಮೋಜಿಗಳನ್ನು ಬಳಸಿಲ್ಲ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
index d5e4d35..2ac1df7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"깃발"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"사용 가능한 그림 이모티콘 없음"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"아직 사용한 이모티콘이 없습니다."</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
index c134949..2ca75f5b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ЖЕЛЕКТЕР"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Жеткиликтүү быйтыкчалар жок"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Бир да быйтыкча колдоно элексиз"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
index 8392941..71da66c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ທຸງ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ບໍ່ມີອີໂມຈິໃຫ້ນຳໃຊ້"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ທ່ານຍັງບໍ່ໄດ້ໃຊ້ອີໂມຈິໃດເທື່ອ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
index d4b99f1..5ab16c7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"VĖLIAVOS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nėra jokių pasiekiamų jaustukų"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Dar nenaudojote jokių jaustukų"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
index 33a7097..b163279 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"KAROGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nav pieejamu emocijzīmju"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jūs vēl neesat izmantojis nevienu emocijzīmi"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
index 5216964..c65fcfb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМИЊА"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нема достапни емоџија"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Сѐ уште не сте користеле емоџија"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
index 3c54aa0..327d8a0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"പതാകകൾ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ഇമോജികളൊന്നും ലഭ്യമല്ല"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"നിങ്ങൾ ഇതുവരെ ഇമോജികളൊന്നും ഉപയോഗിച്ചിട്ടില്ല"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
index 8fee383..f36da6c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ТУГ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Боломжтой эможи алга"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Та ямар нэгэн эможи ашиглаагүй байна"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
index 2c4584f..10853bd4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ध्वज"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोणतेही इमोजी उपलब्ध नाहीत"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"तुम्ही अद्याप कोणतेही इमोजी वापरलेले नाहीत"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
index c30d0ee..7153b7b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tiada emoji tersedia"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan mana-mana emoji lagi"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
index f4f4239..0ebe3f6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"အလံများ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"အီမိုဂျီ မရနိုင်ပါ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"အီမိုဂျီ အသုံးမပြုသေးပါ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
index 3f3f2af..909485c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGG"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ingen emojier er tilgjengelige"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brukt noen emojier ennå"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
index 559a47f..6fdffd0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"झन्डाहरू"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कुनै पनि इमोजी उपलब्ध छैन"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"तपाईंले हालसम्म कुनै पनि इमोजी प्रयोग गर्नुभएको छैन"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
index a86b322..c9b3e3c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAGGEN"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emoji\'s beschikbaar"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Je hebt nog geen emoji\'s gebruikt"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
index b2609fe..55ba407 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ଫ୍ଲାଗଗୁଡ଼ିକ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"କୌଣସି ଇମୋଜି ଉପଲବ୍ଧ ନାହିଁ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ଆପଣ ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଇମୋଜି ବ୍ୟବହାର କରିନାହାଁନ୍ତି"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
index 661dce8..f9ddc80 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ਝੰਡੇ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ਕੋਈ ਇਮੋਜੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕਿਸੇ ਵੀ ਇਮੋਜੀ ਦੀ ਵਰਤੋਂ ਨਹੀਂ ਕੀਤੀ ਹੈ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
index 3ec6876..cc87d09 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Brak dostępnych emotikonów"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Żadne emotikony nie zostały jeszcze użyte"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
index 69c3ff5..14187a3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
index cd448a3..8af463d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nenhum emoji disponível"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ainda não utilizou emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
index 69c3ff5..14187a3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
index 6365913..4491530 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"STEAGURI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nu sunt disponibile emoji-uri"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Încă nu ai folosit emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
index e4d406d..79317ee 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ФЛАГИ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нет доступных эмодзи"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы ещё не использовали эмодзи"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
index e00199a..b451d73 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ධජ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ඉමොජි කිසිවක් නොලැබේ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ඔබ තවමත් කිසිදු ඉමෝජියක් භාවිතා කර නැත"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
index 57e00cd..c4e79a5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nie sú k dispozícii žiadne emodži"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatiaľ ste nepoužili žiadne emodži"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
index addf0c7..a104b96 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ni emodžijev"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Uporabili niste še nobenega emodžija."</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
index 64b2631..c1d871b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAMUJ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nuk ofrohen emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Nuk ke përdorur ende asnjë emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
index 52a8c9b..a85908b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ЗАСТАВЕ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Емоџији нису доступни"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Још нисте користили емоџије"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
index 38fece4..4fb6a64 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGOR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Inga emojier tillgängliga"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ännu inte använt emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
index bd5436b..6a95579 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hakuna emoji zinazopatikana"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bado hujatumia emoji zozote"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
index f2c6053..80a6dca 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"கொடிகள்"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ஈமோஜிகள் எதுவுமில்லை"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"இதுவரை ஈமோஜி எதையும் நீங்கள் பயன்படுத்தவில்லை"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
index 22d722f..f872dc9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ఫ్లాగ్లు"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ఎమోజీలు ఏవీ అందుబాటులో లేవు"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"మీరు ఇంకా ఎమోజీలు ఏవీ ఉపయోగించలేదు"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
index 9118893..31ea744 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ธง"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ไม่มีอีโมจิ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"คุณยังไม่ได้ใช้อีโมจิเลย"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
index 318025e..7216dbb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"MGA BANDILA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Walang available na emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hindi ka pa gumamit ng anumang emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
index be6c132..a3f6281 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAKLAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Kullanılabilir emoji yok"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Henüz emoji kullanmadınız"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
index e346eb9..1f54b13 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"ПРАПОРИ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Немає смайлів"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ви ще не використовували смайли"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
index 8bcd99b..207b0c7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"جھنڈے"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"کوئی بھی ایموجی دستیاب نہیں ہے"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"آپ نے ابھی تک کوئی بھی ایموجی استعمال نہیں کی ہے"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
index ab3f343..a6de5d3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"BAYROQCHALAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hech qanday emoji mavjud emas"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hanuz birorta emoji ishlatmagansiz"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
index 1026037..691cb25 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"CỜ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Không có biểu tượng cảm xúc nào"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bạn chưa sử dụng biểu tượng cảm xúc nào"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
index 8d6e26e..30c282f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"旗帜"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"没有可用的表情符号"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用过任何表情符号"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
index d2befe8..99faec7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的 Emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何 Emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
index d71ace8..228d97b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的表情符號"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何表情符號"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
index e2b449e..2ec492c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
@@ -29,4 +29,6 @@
<string name="emoji_category_flags" msgid="6185639503532784871">"AMAFULEGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Awekho ama-emoji atholakalayo"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Awukasebenzisi noma yimaphi ama-emoji okwamanje"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+ <skip />
</resources>
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 6bf1dae..f1b7014 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -35,7 +35,8 @@
defaultConfig {
externalNativeBuild {
cmake {
- arguments "-DCMAKE_POLICY_DEFAULT_CMP0064=NEW", "-DCMAKE_VERBOSE_MAKEFILE=ON"
+ arguments "-DCMAKE_POLICY_DEFAULT_CMP0064=NEW",
+ "-DCMAKE_VERBOSE_MAKEFILE=ON"
// Build only the static library target
targets "yuv"
}
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
index b852d05..080b10b 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
@@ -30,7 +30,6 @@
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.getParentOfType
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
/**
* Lint check for detecting calls to the suspend `repeatOnLifecycle` APIs using `lifecycleOwner`
@@ -73,7 +72,7 @@
if (!isCalledInViewLifecycleFunction(node.getParentOfType())) return
// Look at the entire launch scope
- var launchScope = node.getParentOfType<KotlinUFunctionCallExpression>()?.receiver
+ var launchScope = node.getParentOfType<UCallExpression>()?.receiver
while (
launchScope != null && !containsViewLifecycleOwnerCall(launchScope.sourcePsi?.text)
) {
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
index 0e8b728c..193833b 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
@@ -38,7 +38,6 @@
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
import org.jetbrains.uast.skipParenthesizedExprDown
import org.jetbrains.uast.skipParenthesizedExprUp
import org.jetbrains.uast.toUElement
@@ -218,7 +217,6 @@
) = enclosingMethodCall.parameterList.parametersCount == 1 ||
(
isKotlin &&
- nearestNonQualifiedRefParent is KotlinUFunctionCallExpression &&
nearestNonQualifiedRefParent.getArgumentForParameter(1) == null
)
diff --git a/glance/glance-appwidget/src/main/res/values-af/strings.xml b/glance/glance-appwidget/src/main/res/values-af/strings.xml
index 1e3098b..ffbcebb 100644
--- a/glance/glance-appwidget/src/main/res/values-af/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-af/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance-applegstukfout"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Gaan die presiese fout na deur "<b><tt>"adb logcat"</tt></b>" te gebruik en te soek na "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kan nie inhoud wys nie"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ar/strings.xml b/glance/glance-appwidget/src/main/res/values-ar/strings.xml
index a4a0d45..7a610e1 100644
--- a/glance/glance-appwidget/src/main/res/values-ar/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ar/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531">""<b>"خطأ في التطبيق المصغّر Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"تأكَّد من الخطأ بالضبط باستخدام "<b><tt>"أداة adb logcat"</tt></b>"، من خلال البحث عن "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"يتعذّر عرض المحتوى."</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-az/strings.xml b/glance/glance-appwidget/src/main/res/values-az/strings.xml
index 3244dd9..fb38a58 100644
--- a/glance/glance-appwidget/src/main/res/values-az/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-az/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance tətbiq vidceti xətası"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"Adb logcat"</tt></b>" istifadə edərək, "<b><tt>"GlanceAppWidget"</tt></b>" axtararaq dəqiq xətanı yoxlayın"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kontenti göstərmək olmur"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml b/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
index 2194879..9395e03 100644
--- a/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Greška u vezi sa vidžetom aplikacije Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Proverite u čemu je tačno greška pomoću stavke"<b><tt>"adb logcat"</tt></b>", tražeći stavku "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sadržaj ne može da se prikaže"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-bg/strings.xml b/glance/glance-appwidget/src/main/res/values-bg/strings.xml
index c71e9d6..996380eb 100644
--- a/glance/glance-appwidget/src/main/res/values-bg/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-bg/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка в приспособлението за приложението Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Проверете точната грешка посредством "<b><tt>"adb logcat"</tt></b>", като потърсите "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Съдържанието не може да се покаже"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-bs/strings.xml b/glance/glance-appwidget/src/main/res/values-bs/strings.xml
index 0975fca..648882a 100644
--- a/glance/glance-appwidget/src/main/res/values-bs/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-bs/strings.xml
@@ -19,5 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Greška vidžeta aplikacije Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Provjerite konkretnu grešku koristeći "<b><tt>"adb logcat"</tt></b>" i pretraživanjem "<b><tt>"GlanceAppWidget"</tt></b></string>
- <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sadržaj se ne može prikazati"</string>
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nije moguće prikazati sadržaj"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ca/strings.xml b/glance/glance-appwidget/src/main/res/values-ca/strings.xml
index 5b2cf5c..ce802c4 100644
--- a/glance/glance-appwidget/src/main/res/values-ca/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ca/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error de Glance App Widget"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Consulta l\'error exacte fent servir "<b><tt>"adb logcat"</tt></b>" i cercant "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"No es pot mostrar el contingut"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-cs/strings.xml b/glance/glance-appwidget/src/main/res/values-cs/strings.xml
index 872b32b..d0a2742 100644
--- a/glance/glance-appwidget/src/main/res/values-cs/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-cs/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Chyba widgetu aplikace Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Pomocí "<b><tt>"adb logcat"</tt></b>" zkontrolujte přesné znění chyby: vyhledejte "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Obsah nelze zobrazit"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-da/strings.xml b/glance/glance-appwidget/src/main/res/values-da/strings.xml
index b01ca60..c436f2c 100644
--- a/glance/glance-appwidget/src/main/res/values-da/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-da/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Fejl i appwidgetten Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Brug "<b><tt>"adb logcat"</tt></b>" til at tjekke den specifikke fejl ved at søge efter "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Indholdet kan ikke vises"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-el/strings.xml b/glance/glance-appwidget/src/main/res/values-el/strings.xml
index b148f7c..3d258e9 100644
--- a/glance/glance-appwidget/src/main/res/values-el/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-el/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Σφάλμα γραφικού στοιχείου εφαρμογής Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Ελέγξτε το ακριβές σφάλμα χρησιμοποιώντας το "<b><tt>"adb logcat"</tt></b>" και αναζητώντας το στοιχείο "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Δεν είναι δυνατή η εμφάνιση περιεχομένου"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-es/strings.xml b/glance/glance-appwidget/src/main/res/values-es/strings.xml
index 1791bc0..3d45dd1 100644
--- a/glance/glance-appwidget/src/main/res/values-es/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-es/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error del widget de la aplicación Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Revisa el error exacto mediante "<b><tt>"adb logcat"</tt></b>" y buscando "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"No se puede mostrar el contenido"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-et/strings.xml b/glance/glance-appwidget/src/main/res/values-et/strings.xml
index 3b9ad9a..e1563ad 100644
--- a/glance/glance-appwidget/src/main/res/values-et/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-et/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Rakenduse Ülevaade vidina viga"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollige konkreetset viga tööriistaga "<b><tt>"adb logcat"</tt></b>", sisestades otsingu "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sisu ei saa kuvada"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-eu/strings.xml b/glance/glance-appwidget/src/main/res/values-eu/strings.xml
index e67b27f..40ab942 100644
--- a/glance/glance-appwidget/src/main/res/values-eu/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-eu/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance aplikazioaren widgetaren errorea"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Errore zehatza ikusteko, erabili "<b><tt>"adb logcat"</tt></b>" bertan "<b><tt>"GlanceAppWidget"</tt></b>" bilatzeko"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Ezin da erakutsi edukia"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml b/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
index 7b97e4f..e948735 100644
--- a/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Erreur provenant de la librairie Glance App Widget"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Vérifiez l\'erreur exacte en utilisant "<b><tt>"adb logcat"</tt></b>", en recherchant "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Impossible d\'afficher le contenu"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-gl/strings.xml b/glance/glance-appwidget/src/main/res/values-gl/strings.xml
index c223cba..d255bb4 100644
--- a/glance/glance-appwidget/src/main/res/values-gl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-gl/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Erro do widget da aplicación Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Comproba o erro exacto usando "<b><tt>"adb logcat"</tt></b>" e buscando "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Non se puido mostrar o contido"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-gu/strings.xml b/glance/glance-appwidget/src/main/res/values-gu/strings.xml
index a378b09..c1f6e06 100644
--- a/glance/glance-appwidget/src/main/res/values-gu/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-gu/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ઍપ વિજેટમાં ભૂલ"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>"નો ઉપયોગ કરીને, "<b><tt>"GlanceAppWidget"</tt></b>" શોધીને ચોક્કસ ભૂલ ચેક કરો"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"કન્ટેન્ટ બતાવી શકતા નથી"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-hi/strings.xml b/glance/glance-appwidget/src/main/res/values-hi/strings.xml
index 574e22b..a41f5fa 100644
--- a/glance/glance-appwidget/src/main/res/values-hi/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-hi/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance App Widget की गड़बड़ी"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" की मदद से, "<b><tt>"GlanceAppWidget"</tt></b>" को ढूंढकर असल गड़बड़ी का पता लगाएं"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"कॉन्टेंट नहीं दिखाया जा सकता"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-hy/strings.xml b/glance/glance-appwidget/src/main/res/values-hy/strings.xml
index 3409cd3..f02aceb 100644
--- a/glance/glance-appwidget/src/main/res/values-hy/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-hy/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"«Հակիրճ գլխավորի մասին» վիջեթի սխալ"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Ստուգեք, թե իրականում ինչ սխալ է տեղի ունեցել, "<b><tt>"adb logcat"</tt></b>"-ի միջոցով՝ որոնելով "<b><tt>"GlanceAppWidget"</tt></b>" բառը"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Հնարավոր չէ ցույց տալ բովանդակությունը"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-in/strings.xml b/glance/glance-appwidget/src/main/res/values-in/strings.xml
index 995c8f9..2279d9f 100644
--- a/glance/glance-appwidget/src/main/res/values-in/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-in/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error library Widget Aplikasi Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Periksa error yang tepat menggunakan "<b><tt>"logcat adb"</tt></b>" saat menelusuri "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Tidak dapat menampilkan konten"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-is/strings.xml b/glance/glance-appwidget/src/main/res/values-is/strings.xml
index af6d0fb..7dd56b0 100644
--- a/glance/glance-appwidget/src/main/res/values-is/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-is/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Villa í forritsgræju „Í fljótu bragði“"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Skoðaðu villuna með "<b><tt>"adb logcat"</tt></b>" og leitaðu að "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Ekki er hægt að sýna efnið"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-it/strings.xml b/glance/glance-appwidget/src/main/res/values-it/strings.xml
index a169b36..392f1eb 100644
--- a/glance/glance-appwidget/src/main/res/values-it/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-it/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Errore del widget dell\'app Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Controlla l\'errore esatto con "<b><tt>"adb logcat"</tt></b>", cercando "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Impossibile mostrare i contenuti"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-iw/strings.xml b/glance/glance-appwidget/src/main/res/values-iw/strings.xml
index cbfc3b8..f1525fd 100644
--- a/glance/glance-appwidget/src/main/res/values-iw/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-iw/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"שגיאה בווידג\'ט של האפליקציה \'בקצרה\'"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"יש לבדוק את השגיאה המדויקת באמצעות "<b><tt>"adb Logcat"</tt></b>", ולחפש את "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"לא ניתן להציג את התוכן"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-kk/strings.xml b/glance/glance-appwidget/src/main/res/values-kk/strings.xml
index 8268784..6737835 100644
--- a/glance/glance-appwidget/src/main/res/values-kk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-kk/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance App Widget қатесі"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Қатені дәл анықтау үшін "<b><tt>"adb logcat"</tt></b>" мәнін қолданыңыз және "<b><tt>"GlanceAppWidget"</tt></b>" іздеңіз."</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контентті көрсету мүмкін емес."</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-kn/strings.xml b/glance/glance-appwidget/src/main/res/values-kn/strings.xml
index f02fd27..caee2e0 100644
--- a/glance/glance-appwidget/src/main/res/values-kn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-kn/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ಆ್ಯಪ್ ವಿಜೆಟ್ ದೋಷ"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" ಬಳಸಿಕೊಂಡು ನಿಖರವಾದ ದೋಷವನ್ನು ಪರಿಶೀಲಿಸಿ, "<b><tt>"GlanceAppWidget"</tt></b>" ಅನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ಕಂಟೆಂಟ್ ಅನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ko/strings.xml b/glance/glance-appwidget/src/main/res/values-ko/strings.xml
index 527abf1..5decae6 100644
--- a/glance/glance-appwidget/src/main/res/values-ko/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ko/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 앱 위젯 오류"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>"을 사용하여 정확한 오류 확인, "<b><tt>"GlanceAppWidget"</tt></b>" 검색"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"콘텐츠를 표시할 수 없음"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ky/strings.xml b/glance/glance-appwidget/src/main/res/values-ky/strings.xml
index 8946e1c..78ad931 100644
--- a/glance/glance-appwidget/src/main/res/values-ky/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ky/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance колдонмосунун виджет катасы"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" cурамы менен издеп, "<b><tt>"adb logcat"</tt></b>" аркылуу катаны аныктаңыз"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контент көрсөтүлбөй жатат"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-lt/strings.xml b/glance/glance-appwidget/src/main/res/values-lt/strings.xml
index d2032a8..6576dc2 100644
--- a/glance/glance-appwidget/src/main/res/values-lt/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-lt/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Programos „Glance“ valdiklio klaida"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Patikrinkite tikslią klaidą naudodami "<b><tt>"ADB Logcat"</tt></b>", kai ieškote "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nepavyko parodyti turinio"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-lv/strings.xml b/glance/glance-appwidget/src/main/res/values-lv/strings.xml
index a132297..eca03a1 100644
--- a/glance/glance-appwidget/src/main/res/values-lv/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-lv/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance lietotnes logrīka kļūda"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Atrodiet precīzu kļūdu ar rīka "<b><tt>"adb logcat"</tt></b>" palīdzību, izmantojot meklēšanas vaicājumu "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nevar parādīt saturu."</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mk/strings.xml b/glance/glance-appwidget/src/main/res/values-mk/strings.xml
index da90e36..b514004 100644
--- a/glance/glance-appwidget/src/main/res/values-mk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mk/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка на Glance App Widget"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Проверете ја точната грешка со "<b><tt>"adb logcat"</tt></b>" при пребарување "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Не може да се прикажат содржините"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mn/strings.xml b/glance/glance-appwidget/src/main/res/values-mn/strings.xml
index 8e40cac..48444c1 100644
--- a/glance/glance-appwidget/src/main/res/values-mn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mn/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Гялс харах аппын виджетийн алдаа"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"Adb logcat"</tt></b>" ашиглан, "<b><tt>"GlanceAppWidget"</tt></b>"-г хайж тодорхой алдааг шалгана уу"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контентыг харуулах боломжгүй"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mr/strings.xml b/glance/glance-appwidget/src/main/res/values-mr/strings.xml
index 9334dde..523bc89 100644
--- a/glance/glance-appwidget/src/main/res/values-mr/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mr/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance अॅप विजेट एरर"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" शोधून, "<b><tt>"adb logcat"</tt></b>" वापरून नेमकी एरर तपासा"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"आशय दाखवू शकत नाही"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-my/strings.xml b/glance/glance-appwidget/src/main/res/values-my/strings.xml
index 662b0b3..db2dcbe 100644
--- a/glance/glance-appwidget/src/main/res/values-my/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-my/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance အက်ပ်ဝိဂျက်အမှား"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" ရှာဖွေရာတွင် "<b><tt>"adb logcat"</tt></b>" သုံးပြီး အမှားအတိအကျကို ရှာနိုင်သည်"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"အကြောင်းအရာကို ပြ၍မရပါ"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-nb/strings.xml b/glance/glance-appwidget/src/main/res/values-nb/strings.xml
index 8a31582..d71569f 100644
--- a/glance/glance-appwidget/src/main/res/values-nb/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-nb/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Modulfeil med Kort fortalt-appen"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Sjekk den nøyaktige feilen ved å bruke "<b><tt>"adb logcat"</tt></b>" og søke etter "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kan ikke vise innhold"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ne/strings.xml b/glance/glance-appwidget/src/main/res/values-ne/strings.xml
index bf4dc05..c945179 100644
--- a/glance/glance-appwidget/src/main/res/values-ne/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ne/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance एपको विजेटसम्बन्धी त्रुटि"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" प्रयोग गरी "<b><tt>"GlanceAppWidget"</tt></b>" खोजेर यथार्थ त्रुटि पत्ता लगाउनुहोस्"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"सामग्री देखाउन सकिएन"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-or/strings.xml b/glance/glance-appwidget/src/main/res/values-or/strings.xml
index 82a8829..96b460a 100644
--- a/glance/glance-appwidget/src/main/res/values-or/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-or/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ଆପ ୱିଜେଟରେ ତ୍ରୁଟି"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" ବ୍ୟବହାର କରି, "<b><tt>"GlanceAppWidget"</tt></b>" ସନ୍ଧାନ କରି ପ୍ରକୃତ ତ୍ରୁଟି ଯାଞ୍ଚ କରନ୍ତୁ"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ବିଷୟବସ୍ତୁ ଦେଖାଯାଇପାରିବ ନାହିଁ"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-pa/strings.xml b/glance/glance-appwidget/src/main/res/values-pa/strings.xml
index f2d0ab9..15bf9ca 100644
--- a/glance/glance-appwidget/src/main/res/values-pa/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-pa/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ਐਪ ਵਿਜੇਟ ਸੰਬੰਧੀ ਗੜਬੜ"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt>" ਦੀ ਵਰਤੋਂ ਨਾਲ "</b>", "<b><tt>"GlanceAppWidget"</tt></b>" ਦੀ ਖੋਜ ਕਰ ਕੇ ਸਟੀਕ ਗੜਬੜ ਦੀ ਜਾਂਚ ਕਰੋ"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ਸਮੱਗਰੀ ਨਹੀਂ ਦਿਖਾਈ ਜਾ ਸਕਦੀ"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-pl/strings.xml b/glance/glance-appwidget/src/main/res/values-pl/strings.xml
index 7482a96..1c58f02 100644
--- a/glance/glance-appwidget/src/main/res/values-pl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-pl/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Błąd widżetu aplikacji W skrócie"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Sprawdź dokładny błąd w "<b><tt>"adb logcat"</tt></b>" (wyszukaj "<b><tt>"GlanceAppWidget"</tt></b>")"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nie można wyświetlić zawartości"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ro/strings.xml b/glance/glance-appwidget/src/main/res/values-ro/strings.xml
index a7e1041..8a0751f 100644
--- a/glance/glance-appwidget/src/main/res/values-ro/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ro/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Eroare legată de widgetul aplicației Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Vezi eroarea exactă folosind "<b><tt>"adb logcat"</tt></b>" și căutând "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nu se poate afișa conținutul"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ru/strings.xml b/glance/glance-appwidget/src/main/res/values-ru/strings.xml
index 29efc59..109ae4b 100644
--- a/glance/glance-appwidget/src/main/res/values-ru/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ru/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Ошибка, связанная с виджетом приложения в Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Выявите ошибку с помощью "<b><tt>"adb logcat"</tt></b>". В этом инструменте выполните поиск по запросу "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Невозможно показать контент."</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-si/strings.xml b/glance/glance-appwidget/src/main/res/values-si/strings.xml
index 31bb754..5a2e4b1 100644
--- a/glance/glance-appwidget/src/main/res/values-si/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-si/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance යෙදුම් විජට්ටු දෝෂය"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"නිවැරදි දෝෂය "<b><tt>"GlanceAppWidget"</tt></b>" සොයන "<b><tt>"adb logcat"</tt></b>" භාවිත කර පරීක්ෂා කරන්න"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"අන්තර්ගතය පෙන්විය නොහැක"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sq/strings.xml b/glance/glance-appwidget/src/main/res/values-sq/strings.xml
index 3d04098..2322305 100644
--- a/glance/glance-appwidget/src/main/res/values-sq/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sq/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Gabim i Glance App Widget"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollo gabimin ekzakt duke përdorur "<b><tt>"adb logcat"</tt></b>", duke kërkuar për "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Përmbajtja nuk mund të shfaqet"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sr/strings.xml b/glance/glance-appwidget/src/main/res/values-sr/strings.xml
index 3fbcc79..96e2038 100644
--- a/glance/glance-appwidget/src/main/res/values-sr/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sr/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка у вези са виџетом апликације Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Проверите у чему је тачно грешка помоћу ставке"<b><tt>"adb logcat"</tt></b>", тражећи ставку "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Садржај не може да се прикаже"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sv/strings.xml b/glance/glance-appwidget/src/main/res/values-sv/strings.xml
index efcc2fb..840f5c8 100644
--- a/glance/glance-appwidget/src/main/res/values-sv/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sv/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Fel i appwidget som skapats med Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollera exakt vad som är fel genom att söka efter "<b><tt>"GlanceAppWidget"</tt></b>" i utdata från "<b><tt>"adb logcat"</tt></b>"."</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Det gick inte att visa innehållet"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ta/strings.xml b/glance/glance-appwidget/src/main/res/values-ta/strings.xml
index 35e91f5..bc993ea 100644
--- a/glance/glance-appwidget/src/main/res/values-ta/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ta/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ஆப்ஸ் விட்ஜெட் பிழை"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" என்பதைப் பயன்படுத்தி சரியான பிழையைக் கண்டறியவும், "<b><tt>"GlanceAppWidget"</tt></b>" தேடப்படுகிறது"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"உள்ளடக்கத்தைக் காட்ட முடியவில்லை"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-tl/strings.xml b/glance/glance-appwidget/src/main/res/values-tl/strings.xml
index 95d2331..5a80654 100644
--- a/glance/glance-appwidget/src/main/res/values-tl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-tl/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error sa Widget ng Glance App"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Tingnan ang eksaktong error gamit ang "<b><tt>"adb logcat"</tt></b>", at paghahanap sa "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Hindi makapagpakita ng content"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-uk/strings.xml b/glance/glance-appwidget/src/main/res/values-uk/strings.xml
index ecd6171..f873a04 100644
--- a/glance/glance-appwidget/src/main/res/values-uk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-uk/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Помилка віджета додатка Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Щоб отримати точні відомості про помилку, скористайтеся командою "<b><tt>"adb logcat"</tt></b>" під час пошуку за запитом "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Не вдається відобразити контент"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-uz/strings.xml b/glance/glance-appwidget/src/main/res/values-uz/strings.xml
index 71690c5..43c3c56 100644
--- a/glance/glance-appwidget/src/main/res/values-uz/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-uz/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ilova vidjeti xatosi"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Muayyan xatoni "<b><tt>"adb logcat"</tt></b>" yordamida tekshiring va "<b><tt>"GlanceAppWidget"</tt>" qatorini qidiring"</b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kontent chiqmaydi"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-vi/strings.xml b/glance/glance-appwidget/src/main/res/values-vi/strings.xml
index db87a72..c88ad6f 100644
--- a/glance/glance-appwidget/src/main/res/values-vi/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-vi/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Lỗi Glance App Widget (Tiện ích xem nhanh ứng dụng)"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"Xem đúng lỗi bằng cách dùng "<b><tt>"logcat adb"</tt></b>" để tìm "<b><tt>"GlanceAppWidget"</tt></b></string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Không hiện được nội dung"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml b/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
index 5bcdb2b..38f190e 100644
--- a/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 應用程式小工具錯誤"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"使用 "<b><tt>"adb logcat"</tt></b>" 搜尋 "<b><tt>"GlanceAppWidget"</tt></b>",查看確實的錯誤資料"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"無法顯示內容"</string>
</resources>
diff --git a/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml b/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
index 02605dc..cf1113d 100644
--- a/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
@@ -19,6 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 應用程式小工具錯誤"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"搜尋 "<b><tt>"GlanceAppWidget"</tt></b>" 時,你可以使用 "<b><tt>"adb logcat"</tt></b>" 查看確切的錯誤資訊"</string>
- <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
- <skip />
+ <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"無法顯示內容"</string>
</resources>
diff --git a/gradle.properties b/gradle.properties
index 304a327..7194ebd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,8 +17,7 @@
# The following entries are workarounds
# fullsdk-linux/**/package.xml -> b/291331139
# androidx/compose/lint/common/build/libs/common.jar -> b/295395616
-# .konan/kotlin-native-prebuilt-linux-x86_64-1.9.10 -> https://youtrack.jetbrains.com/issue/KT-61154/
-org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/klib/common/stdlib;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/konan/lib/*
+org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar
android.lint.baselineOmitLineNumbers=true
android.lint.printStackTrace=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 51d3b26..aa65f2a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,12 +14,12 @@
androidGradlePluginMin = "7.0.4"
androidLintMin = "30.0.4"
androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.6.0-alpha05"
-androidxTestRules = "1.6.0-alpha02"
-androidxTestMonitor = "1.7.0-alpha03"
-androidxTestCore = "1.6.0-alpha03"
-androidxTestExtJunit = "1.2.0-alpha02"
-androidxTestExtTruth = "1.6.0-alpha02"
+androidxTestRunner = "1.6.0-alpha06"
+androidxTestRules = "1.6.0-alpha03"
+androidxTestMonitor = "1.7.0-alpha04"
+androidxTestCore = "1.6.0-alpha05"
+androidxTestExtJunit = "1.2.0-alpha03"
+androidxTestExtTruth = "1.6.0-alpha03"
annotationVersion = "1.7.0"
atomicFu = "0.17.0"
autoService = "1.0-rc6"
@@ -31,8 +31,8 @@
dagger = "2.49"
dexmaker = "2.28.3"
dokka = "1.8.20-dev-214"
-espresso = "3.6.0-alpha02"
-espressoDevice = "1.0.0-alpha07"
+espresso = "3.6.0-alpha03"
+espressoDevice = "1.0.0-alpha08"
grpc = "1.52.0"
guavaJre = "31.1-jre"
hilt = "2.49"
diff --git a/gradlew b/gradlew
index 11901b7e..64c7da7 100755
--- a/gradlew
+++ b/gradlew
@@ -303,14 +303,26 @@
fi
done
+raiseMemory=false
if [[ " ${@} " =~ " -Pandroidx.highMemory " ]]; then
- #Set the initial heap size to match the max heap size,
- #by replacing a string like "-Xmx1g" with one like "-Xms1g -Xmx1g"
- MAX_MEM=32g
- ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s/-Xmx\([^ ]*\)/-Xms$MAX_MEM -Xmx$MAX_MEM/")"
+ raiseMemory=true
+fi
+if [[ " ${@} " =~ " -Pandroidx.lowMemory " ]]; then
+ if [ "$raiseMemory" == "true" ]; then
+ echo "androidx.lowMemory overriding androidx.highMemory"
+ echo
+ fi
+ raiseMemory=false
+fi
- # Increase the compiler cache size: b/260643754 . Remove when updating to JDK 20 ( https://bugs.openjdk.org/browse/JDK-8295724 )
- ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s|$| -XX:ReservedCodeCacheSize=576M|")"
+if [ "$raiseMemory" == "true" ]; then
+ # Set the initial heap size to match the max heap size,
+ # by replacing a string like "-Xmx1g" with one like "-Xms1g -Xmx1g"
+ MAX_MEM=32g
+ ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s/-Xmx\([^ ]*\)/-Xms$MAX_MEM -Xmx$MAX_MEM/")"
+
+ # Increase the compiler cache size: b/260643754 . Remove when updating to JDK 20 ( https://bugs.openjdk.org/browse/JDK-8295724 )
+ ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s|$| -XX:ReservedCodeCacheSize=576M|")"
fi
# check whether the user has requested profiling via yourkit
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
index 9420dab..f77ccb5 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
@@ -324,11 +324,10 @@
}
assertNotNull(renderResult)
assertEquals(SUCCESS, renderResult!!.status)
- val fence = renderResult?.fence
- assertNotNull(fence)
- assertTrue(fence!!.isValid())
- fence.awaitForever()
- fence.close()
+ renderResult?.fence?.let { fence ->
+ fence.awaitForever()
+ fence.close()
+ }
val hardwareBuffer = renderResult!!.hardwareBuffer
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 463b140..c9cbc3f 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -1014,7 +1014,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q, maxSdkVersion = 33) // maxSdk 33 b/315994268
fun testRenderFrontBufferSeveralTimes() {
val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
index 1d745ca..a902170 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
@@ -37,6 +37,7 @@
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -76,6 +77,7 @@
)
}
+ @Ignore // b/321800558
@Test
fun testRenderFrameRotate90() {
testRenderWithTransform(
@@ -114,6 +116,7 @@
)
}
+ @Ignore // b/321800558
@Test
fun testRenderFrameRotate270() {
testRenderWithTransform(
@@ -183,6 +186,7 @@
}
}
+ @Ignore // b/321800558
@Test
fun testCancelPending() {
val transformer = BufferTransformer().apply {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index 31fb4cd..a32525c 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -289,11 +289,13 @@
}
}
+ @SdkSuppress(maxSdkVersion = 33) // b/315169745
@Test
fun testSurfaceContentsWithBackBuffer() {
verifySurfaceContentsWithWindowConfig()
}
+ @SdkSuppress(maxSdkVersion = 33) // b/315169745
@Test
fun testSurfaceContentsWithFrontBuffer() {
verifySurfaceContentsWithWindowConfig(true)
diff --git a/graphics/graphics-core/src/main/cpp/CMakeLists.txt b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
index 5bd2dc3..bc21166 100644
--- a/graphics/graphics-core/src/main/cpp/CMakeLists.txt
+++ b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
@@ -60,3 +60,7 @@
GLESv2
android)
+target_link_options(graphics-core
+ PRIVATE
+ "-Wl,-z,max-page-size=16384")
+
diff --git a/graphics/graphics-path/src/main/cpp/CMakeLists.txt b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
index 8d99cce..722eb2d 100644
--- a/graphics/graphics-path/src/main/cpp/CMakeLists.txt
+++ b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
@@ -14,5 +14,5 @@
target_link_options(
androidx.graphics.path
PRIVATE
- "-Wl,--version-script=${VERSION_SCRIPT}"
+ "-Wl,--version-script=${VERSION_SCRIPT},-z,max-page-size=16384"
)
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index 7bcd452..2b5bf0a 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -102,7 +102,7 @@
}
project.apply(plugin = "com.google.protobuf")
- project.plugins.all {
+ project.plugins.configureEach {
if (it is ProtobufPlugin) {
val protobufExtension = project.extensions.getByType(ProtobufExtension::class.java)
protobufExtension.apply {
@@ -189,7 +189,7 @@
generateProguardDetectionFile(libraryProject)
val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
- libExtension.libraryVariants.all { variant ->
+ libExtension.libraryVariants.configureEach { variant ->
variant.packageLibraryProvider.configure { zip ->
zip.from(consumeInspectorFiles)
zip.rename {
@@ -259,7 +259,7 @@
@ExperimentalStdlibApi
private fun generateProguardDetectionFile(libraryProject: Project) {
val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
- libExtension.libraryVariants.all { variant ->
+ libExtension.libraryVariants.configureEach { variant ->
libraryProject.registerGenerateProguardDetectionFileTask(variant)
}
}
diff --git a/inspection/inspection/src/main/native/CMakeLists.txt b/inspection/inspection/src/main/native/CMakeLists.txt
index 2a7f747..0897442 100644
--- a/inspection/inspection/src/main/native/CMakeLists.txt
+++ b/inspection/inspection/src/main/native/CMakeLists.txt
@@ -51,3 +51,4 @@
target_include_directories(art_tooling
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/external_jvmti)
target_link_libraries(art_tooling ${log-lib})
+target_link_options(art_tooling PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml
new file mode 100644
index 0000000..e1213cc
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:alpha="?android:attr/disabledAlpha" android:color="@android:color/white" />
+</selector>
diff --git a/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml
new file mode 100644
index 0000000..c10a7f6e
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="false"
+ android:color="?android:attr/colorForeground"
+ android:alpha="?android:attr/disabledAlpha" />
+ <item android:state_checked="true" android:color="?android:attr/colorControlActivated" />
+ <item android:color="?android:attr/colorForeground" />
+</selector>
diff --git a/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml b/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml
new file mode 100644
index 0000000..92dd51c
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 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.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:end="4dp"
+ android:gravity="center_vertical|fill_horizontal"
+ android:start="4dp">
+ <shape
+ android:shape="rectangle"
+ android:tint="@color/lb_switch_compat_track_tint">
+ <corners android:radius="7dp" />
+ <size android:height="14dp" />
+ <solid android:color="@color/lb_switch_compat_track_color" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml b/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml
new file mode 100644
index 0000000..b6c9662
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+
+<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="NewApi"
+ android:id="@+id/switchWidget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/switchStyle"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" />
diff --git a/leanback/leanback-preference/src/main/res/values-v28/themes.xml b/leanback/leanback-preference/src/main/res/values-v28/themes.xml
index dbac03b..d260a6a 100644
--- a/leanback/leanback-preference/src/main/res/values-v28/themes.xml
+++ b/leanback/leanback-preference/src/main/res/values-v28/themes.xml
@@ -88,10 +88,16 @@
<item name="android:radioButtonStyle">@android:style/Widget.Material.CompoundButton.RadioButton</item>
- <item name="android:switchStyle">@android:style/Widget.Material.CompoundButton.Switch</item>
+ <item name="android:switchStyle">@style/LeanbackSwitchCompatStyle</item>
<item name="android:seekBarStyle">@android:style/Widget.Material.SeekBar</item>
</style>
+ <!-- Extends Switch style to SwitchCompat. -->
+ <style name="LeanbackSwitchCompatStyle" parent="@android:style/Widget.Material.CompoundButton.Switch">
+ <item name="showText">false</item>
+ <item name="track">@drawable/leanback_switch_compat_track</item>
+ </style>
+
</resources>
\ No newline at end of file
diff --git a/leanback/leanback-preference/src/main/res/values/styles.xml b/leanback/leanback-preference/src/main/res/values/styles.xml
index 3a773e5..ca08ff9 100644
--- a/leanback/leanback-preference/src/main/res/values/styles.xml
+++ b/leanback/leanback-preference/src/main/res/values/styles.xml
@@ -64,7 +64,7 @@
</style>
<style name="LeanbackPreference.SwitchPreferenceCompat">
- <item name="android:widgetLayout">@layout/leanback_preference_widget_switch</item>
+ <item name="android:widgetLayout">@layout/leanback_preference_widget_switch_compat</item>
<item name="android:switchTextOn">@string/v7_preference_on</item>
<item name="android:switchTextOff">@string/v7_preference_off</item>
</style>
diff --git a/leanback/leanback/src/main/res/values-zh-rCN/strings.xml b/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
index 8c773e4..f6bb7f24d 100644
--- a/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
+++ b/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
@@ -36,8 +36,8 @@
<string name="lb_playback_controls_more_actions" msgid="8730341244454469032">"更多操作"</string>
<string name="lb_playback_controls_thumb_up" msgid="3458671378107738666">"取消选择顶操作"</string>
<string name="lb_playback_controls_thumb_up_outline" msgid="1385865732502550659">"选择顶操作"</string>
- <string name="lb_playback_controls_thumb_down" msgid="3544533410444618518">"取消选择踩操作"</string>
- <string name="lb_playback_controls_thumb_down_outline" msgid="8475278766138652105">"选择踩操作"</string>
+ <string name="lb_playback_controls_thumb_down" msgid="3544533410444618518">"取消不喜欢"</string>
+ <string name="lb_playback_controls_thumb_down_outline" msgid="8475278766138652105">"选择不喜欢"</string>
<string name="lb_playback_controls_repeat_none" msgid="1614290959784265209">"不重复播放"</string>
<string name="lb_playback_controls_repeat_all" msgid="8429099206716245199">"重复播放全部"</string>
<string name="lb_playback_controls_repeat_one" msgid="676658705837320560">"重复播放一项"</string>
diff --git a/libraryversions.toml b/libraryversions.toml
index cade605..0d49506 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -2,7 +2,6 @@
ACTIVITY = "1.9.0-alpha03"
ANNOTATION = "1.8.0-alpha01"
ANNOTATION_EXPERIMENTAL = "1.4.0-rc01"
-ANNOTATION_REPLACEWITH = "1.0.0-alpha01"
APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
APPACTIONS_INTERACTION = "1.0.0-alpha01"
APPCOMPAT = "1.7.0-alpha04"
@@ -23,11 +22,11 @@
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-alpha01"
COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.7.0-alpha01"
+COMPOSE = "1.7.0-alpha02"
COMPOSE_COMPILER = "1.5.8"
COMPOSE_MATERIAL3 = "1.3.0-alpha01"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha05"
-COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha02"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha06"
+COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha03"
COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -35,7 +34,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.13.0-alpha04"
+CORE = "1.13.0-alpha05"
CORE_ANIMATION = "1.0.0-rc01"
CORE_ANIMATION_TESTING = "1.0.0-rc01"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -64,7 +63,7 @@
EMOJI2 = "1.5.0-alpha01"
ENTERPRISE = "1.1.0-rc01"
EXIFINTERFACE = "1.4.0-alpha01"
-FRAGMENT = "1.7.0-alpha09"
+FRAGMENT = "1.7.0-alpha10"
FUTURES = "1.2.0-alpha02"
GLANCE = "1.1.0-alpha01"
GLANCE_PREVIEW = "1.0.0-alpha06"
@@ -78,9 +77,9 @@
HEALTH_CONNECT = "1.1.0-alpha07"
HEALTH_SERVICES_CLIENT = "1.1.0-alpha02"
HEIFWRITER = "1.1.0-alpha03"
-HILT = "1.2.0-beta01"
-HILT_NAVIGATION = "1.2.0-beta01"
-HILT_NAVIGATION_COMPOSE = "1.2.0-beta01"
+HILT = "1.2.0-rc01"
+HILT_NAVIGATION = "1.2.0-rc01"
+HILT_NAVIGATION_COMPOSE = "1.2.0-rc01"
INPUT_MOTIONPREDICTION = "1.0.0-beta03"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
@@ -95,6 +94,7 @@
LIBYUV = "0.1.0-dev01"
LIFECYCLE = "2.8.0-alpha01"
LIFECYCLE_EXTENSIONS = "2.2.0"
+LINT = "1.0.0-alpha01"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIA2 = "1.3.0-rc01"
@@ -111,7 +111,7 @@
PRIVACYSANDBOX_ADS = "1.1.0-beta04"
PRIVACYSANDBOX_PLUGINS = "1.0.0-alpha03"
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha12"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha06"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha07"
PRIVACYSANDBOX_UI = "1.0.0-alpha07"
PROFILEINSTALLER = "1.4.0-alpha01"
RECOMMENDATION = "1.1.0-alpha01"
@@ -154,8 +154,8 @@
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.1.0-beta03"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.4.0-alpha01"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha16"
+WEAR_COMPOSE = "1.4.0-alpha02"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha17"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha02"
@@ -245,6 +245,7 @@
LIBYUV = { group = "libyuv", atomicGroupVersion = "versions.LIBYUV" }
LIFECYCLE = { group = "androidx.lifecycle", atomicGroupVersion = "versions.LIFECYCLE" }
LIFECYCLE_EXTENSIONS = { group = "androidx.lifecycle", atomicGroupVersion = "versions.LIFECYCLE_EXTENSIONS", overrideInclude = [ ":lifecycle:lifecycle-extensions" ] }
+LINT = { group = "androidx.lint", atomicGroupVersion = "versions.LINT" }
LOADER = { group = "androidx.loader", atomicGroupVersion = "versions.LOADER" }
MEDIA = { group = "androidx.media" }
MEDIA2 = { group = "androidx.media2", atomicGroupVersion = "versions.MEDIA2" }
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index bc25ff6..b145a44 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -21,26 +21,85 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
+import androidx.build.KmpPlatformsKt
+import androidx.build.PlatformIdentifier
import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.konan.target.Family
plugins {
id("AndroidXPlugin")
- id("java-library")
- id("kotlin")
}
-dependencies {
- api("androidx.annotation:annotation:1.1.0")
- api(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
- api(libs.kotlinCoroutinesAndroid) {
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
- }
+def macEnabled = KmpPlatformsKt.enableMac(project)
+def linuxEnabled = KmpPlatformsKt.enableLinux(project)
- testImplementation(libs.junit)
- testImplementation(libs.mockitoCore4)
+androidXMultiplatform {
+ jvm {
+ withJava()
+ }
+ mac()
+ linux()
+ ios()
+
+ defaultPlatform(PlatformIdentifier.JVM)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(project(":annotation:annotation"))
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ }
+
+ jvmTest {
+ dependencies {
+ implementation(libs.junit)
+ implementation(libs.mockitoCore4)
+ }
+ }
+
+ if (macEnabled || linuxEnabled) {
+ nativeMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.atomicFu)
+ }
+ }
+ }
+ if (macEnabled) {
+ darwinMain {
+ dependsOn(nativeMain)
+ }
+ }
+ if (linuxEnabled) {
+ linuxMain {
+ dependsOn(nativeMain)
+ }
+ }
+
+ targets.all { target ->
+ if (target.platformType == org.jetbrains.kotlin.gradle.plugin.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}")
+ }
+ }
+ target.compilations["test"].defaultSourceSet {
+ dependsOn(nativeTest)
+ }
+ }
+ }
+ }
}
androidx {
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserverAdapter.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserverAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserverAdapter.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserverAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
similarity index 82%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
index 66ec94d..3812ec2 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
+++ b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
@@ -17,12 +17,10 @@
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
-import androidx.lifecycle.Lifecycle.Event
-import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
+import kotlin.jvm.JvmStatic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
@@ -61,7 +59,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()
+ public var internalScopeRef: AtomicReference<Any?> = AtomicReference(null)
/**
* Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
@@ -305,6 +303,12 @@
}
}
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public expect class AtomicReference<V>(value: V) {
+ public fun get(): V
+ public fun compareAndSet(expect: V, newValue: V): Boolean
+}
+
/**
* [CoroutineScope] tied to this [Lifecycle].
*
@@ -336,69 +340,9 @@
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
- *
- * This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted],
- * [launchWhenResumed]
*/
-public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
+public expect abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
internal abstract val lifecycle: Lifecycle
-
- /**
- * Launches and runs the given block when the [Lifecycle] controlling this
- * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state.
- *
- * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
- *
- * @see Lifecycle.whenCreated
- * @see Lifecycle.coroutineScope
- */
- @Deprecated(
- message = "launchWhenCreated is deprecated as it can lead to wasted resources " +
- "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
- "the Lifecycle state is at least Lifecycle.State.CREATED."
- )
- @Suppress("DEPRECATION")
- public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
- lifecycle.whenCreated(block)
- }
-
- /**
- * Launches and runs the given block when the [Lifecycle] controlling this
- * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
- *
- * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
- *
- * @see Lifecycle.whenStarted
- * @see Lifecycle.coroutineScope
- */
- @Deprecated(
- message = "launchWhenStarted is deprecated as it can lead to wasted resources " +
- "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
- "the Lifecycle state is at least Lifecycle.State.STARTED."
- )
- @Suppress("DEPRECATION")
- public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
- lifecycle.whenStarted(block)
- }
-
- /**
- * Launches and runs the given block when the [Lifecycle] controlling this
- * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
- *
- * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
- *
- * @see Lifecycle.whenResumed
- * @see Lifecycle.coroutineScope
- */
- @Deprecated(
- message = "launchWhenResumed is deprecated as it can lead to wasted resources " +
- "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
- "the Lifecycle state is at least Lifecycle.State.RESUMED."
- )
- @Suppress("DEPRECATION")
- public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
- lifecycle.whenResumed(block)
- }
}
internal class LifecycleCoroutineScopeImpl(
@@ -433,9 +377,9 @@
}
/**
- * Creates a [Flow] of [Event]s containing values dispatched by this [Lifecycle].
+ * Creates a [Flow] of [Lifecycle.Event]s containing values dispatched by this [Lifecycle].
*/
-public val Lifecycle.eventFlow: Flow<Event>
+public val Lifecycle.eventFlow: Flow<Lifecycle.Event>
get() = callbackFlow {
val observer = LifecycleEventObserver { _, event ->
trySend(event)
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleEventObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleEventObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleOwner.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleOwner.kt
diff --git a/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt
new file mode 100644
index 0000000..1bca08b
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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.RestrictTo
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public expect object Lifecycling {
+
+ public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver
+
+ /**
+ * Create a name for an adapter class.
+ */
+ public fun getAdapterName(className: String): String
+}
diff --git a/lifecycle/lifecycle-common/src/main/baseline-prof.txt b/lifecycle/lifecycle-common/src/jvmMain/baseline-prof.txt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/baseline-prof.txt
rename to lifecycle/lifecycle-common/src/jvmMain/baseline-prof.txt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ClassesInfoCache.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ClassesInfoCache.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/GenericLifecycleObserver.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/GenericLifecycleObserver.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/OnLifecycleEvent.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/OnLifecycleEvent.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/CompositeGeneratedAdaptersObserver.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/CompositeGeneratedAdaptersObserver.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DispatchQueue.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/DispatchQueue.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DispatchQueue.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/DispatchQueue.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/GeneratedAdapter.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/GeneratedAdapter.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt
new file mode 100644
index 0000000..cf33d38
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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 java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
+public actual typealias AtomicReference<V> = AtomicReference<V>
+
+/**
+ * [CoroutineScope] tied to a [Lifecycle] and
+ * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
+ *
+ * This scope will be cancelled when the [Lifecycle] is destroyed.
+ *
+ * This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted],
+ * [launchWhenResumed]
+ */
+public actual abstract class LifecycleCoroutineScope internal actual constructor() :
+ CoroutineScope {
+ internal actual abstract val lifecycle: Lifecycle
+
+ /**
+ * Launches and runs the given block when the [Lifecycle] controlling this
+ * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state.
+ *
+ * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+ *
+ * @see Lifecycle.whenCreated
+ * @see Lifecycle.coroutineScope
+ */
+ @Deprecated(
+ message = "launchWhenCreated is deprecated as it can lead to wasted resources " +
+ "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+ "the Lifecycle state is at least Lifecycle.State.CREATED."
+ )
+ @Suppress("DEPRECATION")
+ public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
+ lifecycle.whenCreated(block)
+ }
+
+ /**
+ * Launches and runs the given block when the [Lifecycle] controlling this
+ * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
+ *
+ * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+ *
+ * @see Lifecycle.whenStarted
+ * @see Lifecycle.coroutineScope
+ */
+ @Deprecated(
+ message = "launchWhenStarted is deprecated as it can lead to wasted resources " +
+ "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+ "the Lifecycle state is at least Lifecycle.State.STARTED."
+ )
+ @Suppress("DEPRECATION")
+ public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
+ lifecycle.whenStarted(block)
+ }
+
+ /**
+ * Launches and runs the given block when the [Lifecycle] controlling this
+ * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
+ *
+ * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+ *
+ * @see Lifecycle.whenResumed
+ * @see Lifecycle.coroutineScope
+ */
+ @Deprecated(
+ message = "launchWhenResumed is deprecated as it can lead to wasted resources " +
+ "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+ "the Lifecycle state is at least Lifecycle.State.RESUMED."
+ )
+ @Suppress("DEPRECATION")
+ public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
+ lifecycle.whenResumed(block)
+ }
+}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleController.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/LifecycleController.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleController.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/LifecycleController.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
similarity index 96%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
index 51efb21..1c39212 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
@@ -23,7 +23,7 @@
* Internal class to handle lifecycle conversion etc.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public object Lifecycling {
+public actual object Lifecycling {
private const val REFLECTIVE_CALLBACK = 1
private const val GENERATED_CALLBACK = 2
private val callbackCache: MutableMap<Class<*>, Int> = HashMap()
@@ -32,7 +32,7 @@
@JvmStatic
@Suppress("DEPRECATION")
- public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+ public actual fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
val isLifecycleEventObserver = `object` is LifecycleEventObserver
val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
@@ -170,7 +170,7 @@
* Create a name for an adapter class.
*/
@JvmStatic
- public fun getAdapterName(className: String): String {
+ public actual fun getAdapterName(className: String): String {
return className.replace(".", "_") + "_LifecycleAdapter"
}
}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/MethodCallsLogger.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/MethodCallsLogger.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
similarity index 98%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
index 7b388ed..fd67635 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:JvmName("PausingDispatcherKt")
+
package androidx.lifecycle
import kotlin.coroutines.CoroutineContext
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/SingleGeneratedAdapterObserver.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/SingleGeneratedAdapterObserver.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/LifecyclingTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/LifecyclingTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl3.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl3.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/NoOpLifecycle.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/NoOpLifecycle.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/NoOpLifecycle.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/NoOpLifecycle.kt
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
new file mode 100644
index 0000000..4798c50
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+
+public actual class AtomicReference<V> actual constructor(value: V) {
+ private val delegate = atomic(value)
+ public actual fun get() = delegate.value
+ public actual fun compareAndSet(expect: V, newValue: V) =
+ delegate.compareAndSet(expect, newValue)
+}
+
+public actual abstract class LifecycleCoroutineScope internal actual constructor() :
+ CoroutineScope {
+ internal actual abstract val lifecycle: Lifecycle
+}
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt
new file mode 100644
index 0000000..dc7ab57
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.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 androidx.lifecycle
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public actual object Lifecycling {
+ public actual fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+ val isLifecycleEventObserver = `object` is LifecycleEventObserver
+ val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
+ if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
+ return DefaultLifecycleObserverAdapter(
+ `object` as DefaultLifecycleObserver,
+ `object` as LifecycleEventObserver
+ )
+ }
+ if (isDefaultLifecycleObserver) {
+ return DefaultLifecycleObserverAdapter(`object` as DefaultLifecycleObserver, null)
+ }
+ if (isLifecycleEventObserver) {
+ return `object` as LifecycleEventObserver
+ }
+ throw IllegalArgumentException()
+ }
+
+ /**
+ * Create a name for an adapter class.
+ */
+ public actual fun getAdapterName(className: String): String {
+ return className.replace(".", "_") + "_LifecycleAdapter"
+ }
+}
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index a7a8099..ad4d5df 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -41,11 +41,11 @@
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UField
import org.jetbrains.uast.UReferenceExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getUastParentOfType
import org.jetbrains.uast.isNullLiteral
-import org.jetbrains.uast.kotlin.KotlinUField
-import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression
import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
@@ -81,24 +81,21 @@
val methods = listOf("setValue", "postValue")
- override fun getApplicableUastTypes(): List<Class<out UElement>>? {
- return listOf(UCallExpression::class.java, UClass::class.java)
+ override fun getApplicableUastTypes(): List<Class<out UElement>> {
+ return listOf(UCallExpression::class.java, UField::class.java)
}
- override fun createUastHandler(context: JavaContext): UElementHandler? {
+ override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
- override fun visitClass(node: UClass) {
- for (element in node.uastDeclarations) {
- if (element is KotlinUField) {
- getFieldTypeReference(element)?.let {
- // map the variable name to the type reference of its expression.
- typesMap.put(element.name, it)
- }
- }
+ override fun visitField(node: UField) {
+ if (!isKotlin(node.lang)) return
+ getFieldTypeReference(node)?.let {
+ // map the variable name to the type reference of its expression.
+ typesMap.put(node.name, it)
}
}
- private fun getFieldTypeReference(element: KotlinUField): KtTypeReference? {
+ private fun getFieldTypeReference(element: UField): KtTypeReference? {
// If field has type reference, we need to use type reference
// Given the field `val liveDataField: MutableLiveData<Boolean> = MutableLiveData()`
// reference: `MutableLiveData<Boolean>`
@@ -132,7 +129,7 @@
var liveDataType =
if (receiverType != null && receiverType.hasParameters()) {
val receiver =
- (node.receiver as? KotlinUSimpleReferenceExpression)?.resolve()
+ (node.receiver as? USimpleNameReferenceExpression)?.resolve()
val variable = (receiver as? PsiVariable)
val assignment = variable?.let {
UastLintUtils.findLastAssignment(it, node)
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
index 167a9f3..a75e2e6 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
@@ -44,8 +44,8 @@
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.USwitchClauseExpression
+import org.jetbrains.uast.USwitchClauseExpressionWithBody
import org.jetbrains.uast.UTryExpression
-import org.jetbrains.uast.kotlin.KotlinUSwitchEntry
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
import org.jetbrains.uast.visitor.AbstractUastVisitor
@@ -200,7 +200,7 @@
if (method.isLifecycleIsAtLeastMethod(context)) {
// If the case containing the lifecycle check evaluates to true, check the body
withNewState(checkUIAccess = false) {
- (node as? KotlinUSwitchEntry)?.body?.expressions?.forEach {
+ (node as? USwitchClauseExpressionWithBody)?.body?.expressions?.forEach {
it.accept(this)
}
}
diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle
index 408efad..4028590 100644
--- a/lint-checks/integration-tests/build.gradle
+++ b/lint-checks/integration-tests/build.gradle
@@ -32,7 +32,6 @@
dependencies {
implementation(project(":annotation:annotation"))
implementation("androidx.annotation:annotation-experimental:1.4.0")
- implementation(project(":annotation:annotation-replacewith"))
implementation(libs.kotlinStdlib)
}
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
index 88368c4..312e982 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
/**
* Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
index d663e67..9233153 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
/**
* Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
index fb62a31..248fd0e 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
/**
* Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java b/lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java
rename to lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
index 0bab04c..d297597 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
import androidx.annotation.ReplaceWith;
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java b/lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java
rename to lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
index 6cbc0a6..4d00f70 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
import androidx.annotation.ReplaceWith;
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
similarity index 98%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
index 80da9c9..7afdee3 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
import android.view.View;
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
similarity index 98%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt
rename to lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
index 375a0e2..3ce93d5 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
@@ -16,7 +16,7 @@
@file:Suppress("unused")
-package sample
+package replacewith
import android.view.View
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
index 34cd81f..4a6ed12 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
/**
* Usage of a static method with an explicit class.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
similarity index 90%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
index 5611277..ae0d6f5 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
-import static sample.ReplaceWithUsageJava.AUTOFILL_HINT_NAME;
+import static replacewith.ReplaceWithUsageJava.AUTOFILL_HINT_NAME;
/**
* Usage of a static method with an explicit class.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
index c5e54fe..de9b8cb 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package sample;
+package replacewith;
/**
* Usage of a static method with an explicit class.
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index e36df4f..c9e0da4 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -79,6 +79,7 @@
DeprecationMismatchDetector.ISSUE,
RestrictToDetector.RESTRICTED,
ObsoleteCompatDetector.ISSUE,
+ ReplaceWithDetector.ISSUE,
)
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index 503243f..33b8853 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -35,11 +35,12 @@
import java.io.FileNotFoundException
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UClassLiteralExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
-import org.jetbrains.uast.kotlin.KotlinUVarargExpression
+import org.jetbrains.uast.UastCallKind
import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
@@ -113,11 +114,14 @@
private fun getUElementsFromOptInMarkerClass(markerClass: UExpression): List<UElement> {
val elements = ArrayList<UElement?>()
- when (markerClass) {
- is UClassLiteralExpression -> { // opting in to single annotation
+ when {
+ markerClass is UClassLiteralExpression -> {
+ // opting in to single annotation
elements.add(markerClass.toUElement())
}
- is KotlinUVarargExpression -> { // opting in to multiple annotations
+ markerClass is UCallExpression &&
+ markerClass.kind == UastCallKind.NESTED_ARRAY_INITIALIZER -> {
+ // opting in to multiple annotations
val expressions: List<UExpression> = markerClass.valueArguments
for (expression in expressions) {
val uElement = (expression as UClassLiteralExpression).toUElement()
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 7fe685c..6057cc1 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -73,13 +73,13 @@
import org.jetbrains.uast.UInstanceExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParenthesizedExpression
+import org.jetbrains.uast.UReturnExpression
import org.jetbrains.uast.USuperExpression
import org.jetbrains.uast.UThisExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getContainingUMethod
import org.jetbrains.uast.getParentOfType
-import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.util.isConstructorCall
import org.jetbrains.uast.util.isMethodCall
@@ -677,7 +677,7 @@
if (psi == null) {
// If there is no psi, test for the one known case where there should be an
// expected type, an implicit Kotlin return.
- if (element is KotlinUImplicitReturnExpression) {
+ if (element is UReturnExpression && isKotlin(element.lang)) {
return (element.getParentOfType<UMethod>())?.returnType
}
return null
diff --git a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
index 4aba06a..68ad1afd 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
@@ -34,6 +34,7 @@
import com.intellij.psi.impl.source.PsiClassReferenceType
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.uast.UAnonymousClass
+import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UExpression
@@ -43,7 +44,6 @@
import org.jetbrains.uast.UReturnExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.java.JavaUCodeBlockExpression
import org.jetbrains.uast.skipParenthesizedExprDown
class ObsoleteCompatDetector : Detector(), SourceCodeScanner {
@@ -67,7 +67,6 @@
private class CompatMethodHandler(val context: JavaContext) : UElementHandler() {
- @Suppress("UnstableApiUsage", "UnstableApiUsage") // UMethod.parameters
override fun visitMethod(node: UMethod) {
// If this method probably a compat method?
if (!node.isMaybeJetpackUtilityMethod()) return
@@ -84,10 +83,10 @@
if (hasDeprecated && hasReplaceWith && hasDeprecatedDoc) return
// Compat methods take the wrapped class as the first parameter.
- val firstParameter = node.parameters.firstOrNull() ?: return
+ val firstParameter = node.javaPsi.parameterList.parameters.firstOrNull() ?: return
// Ensure that we're dealing with a single-line method that operates on the wrapped class.
- val expression = (node.uastBody as? JavaUCodeBlockExpression)
+ val expression = (node.uastBody as? UBlockExpression)
?.expressions
?.singleOrNull()
?.unwrapReturnExpression()
diff --git a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
similarity index 89%
rename from annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt
rename to lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
index ba130c81..94b55d2 100644
--- a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
@@ -16,8 +16,10 @@
@file:Suppress("UnstableApiUsage")
-package androidx.annotation.replacewith.lint
+package androidx.build.lint
+import com.android.tools.lint.detector.api.AnnotationInfo
+import com.android.tools.lint.detector.api.AnnotationUsageInfo
import com.android.tools.lint.detector.api.AnnotationUsageType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
@@ -30,19 +32,17 @@
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiNewExpression
import com.intellij.psi.impl.source.tree.TreeElement
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
import org.jetbrains.kotlin.psi.psiUtil.endOffset
-import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.java.JavaConstructorUCallExpression
class ReplaceWithDetector : Detector(), SourceCodeScanner {
@@ -53,17 +53,16 @@
override fun visitAnnotationUsage(
context: JavaContext,
- usage: UElement,
- type: AnnotationUsageType,
- annotation: UAnnotation,
- qualifiedName: String,
- method: PsiMethod?,
- referenced: PsiElement?,
- annotations: List<UAnnotation>,
- allMemberAnnotations: List<UAnnotation>,
- allClassAnnotations: List<UAnnotation>,
- allPackageAnnotations: List<UAnnotation>
+ element: UElement,
+ annotationInfo: AnnotationInfo,
+ usageInfo: AnnotationUsageInfo
) {
+ val qualifiedName = annotationInfo.qualifiedName
+ val annotation = annotationInfo.annotation
+ val referenced = usageInfo.referenced
+ val usage = usageInfo.usage
+ val type = usageInfo.type
+
// Ignore callbacks for assignment on the original declaration of an annotated field.
if (type == AnnotationUsageType.ASSIGNMENT_RHS && usage.uastParent == referenced) return
@@ -77,11 +76,11 @@
val includeReceiver = Regex("^\\w+\\.\\w+.*\$").matches(expression)
val includeArguments = Regex("^.*\\w+\\(.*\\)$").matches(expression)
- if (method != null && usage is UCallExpression) {
+ if (referenced is PsiMethod && usage is UCallExpression) {
// Per Kotlin documentation for ReplaceWith: For function calls, the replacement
// expression may contain argument names of the deprecated function, which will
// be substituted with actual parameters used in the call being updated.
- val argumentNamesToActualParams = method.parameters.mapIndexed { index, param ->
+ val argsToParams = referenced.parameters.mapIndexed { index, param ->
param.name to usage.getArgumentForParameter(index)?.sourcePsi?.text
}.associate { it }
@@ -91,7 +90,7 @@
var index = 0
do {
val matchResult = search.find(expression, index) ?: break
- val replacement = argumentNamesToActualParams[matchResult.value]
+ val replacement = argsToParams[matchResult.value]
if (replacement != null) {
expression = expression.replaceRange(matchResult.range, replacement)
index += replacement.length
@@ -100,13 +99,13 @@
}
} while (index < expression.length)
- location = when (usage) {
- is JavaConstructorUCallExpression -> {
+ location = when (val sourcePsi = usage.sourcePsi) {
+ is PsiNewExpression -> {
// The expression should never specify "new", but if it specifies a
// receiver then we should replace the call to "new". For example, if
// we're replacing `new Clazz("arg")` with `ClazzCompat.create("arg")`.
context.getConstructorLocation(
- usage, includeReceiver, includeArguments
+ usage, sourcePsi, includeReceiver, includeArguments
)
}
else -> {
@@ -171,14 +170,15 @@
* `receiver` to handle trimming the `new` keyword from the start of a Java constructor call.
*/
fun JavaContext.getConstructorLocation(
- call: JavaConstructorUCallExpression,
+ call: UCallExpression,
+ newExpression: PsiNewExpression,
includeNew: Boolean,
includeArguments: Boolean
): Location {
if (includeArguments) {
call.valueArguments.lastOrNull()?.let { lastArgument ->
val argumentsEnd = lastArgument.sourcePsi?.endOffset
- val callEnds = call.sourcePsi.endOffset
+ val callEnds = newExpression.endOffset
if (argumentsEnd != null && argumentsEnd > callEnds) {
// The call element has arguments that are outside of its own range.
// This typically means users are making a function call using
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
similarity index 71%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
index 013239e..c8cb288 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.annotation.replacewith.lint;
+package androidx.build.lint.replacewith;
import org.junit.Test
import org.junit.runner.RunWith
@@ -26,20 +26,20 @@
@Test
fun constructorStaticClass() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.ConstructorStaticClass")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.ConstructorStaticClass")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/ConstructorStaticClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorStaticClass.java:25: Information: Replacement available [ReplaceWith]
new ReplaceWithUsageJava("parameter");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/ConstructorStaticClass.java line 25: Replace with `StringBuffer("parameter")`:
+Fix for src/replacewith/ConstructorStaticClass.java line 25: Replace with `StringBuffer("parameter")`:
@@ -25 +25
- new ReplaceWithUsageJava("parameter");
+ new StringBuffer("parameter");
@@ -52,20 +52,20 @@
@Test
fun constructorNonStaticClass() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.ConstructorNonStaticClass")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.ConstructorNonStaticClass")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/ConstructorNonStaticClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorNonStaticClass.java:25: Information: Replacement available [ReplaceWith]
new ReplaceWithUsageJava().new InnerClass("param");
~~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/ConstructorNonStaticClass.java line 25: Replace with `InnerClass()`:
+Fix for src/replacewith/ConstructorNonStaticClass.java line 25: Replace with `InnerClass()`:
@@ -25 +25
- new ReplaceWithUsageJava().new InnerClass("param");
+ new ReplaceWithUsageJava().new InnerClass();
@@ -78,20 +78,20 @@
@Test
fun constructorToStaticMethod() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.ConstructorToStaticMethod")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.ConstructorToStaticMethod")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/ConstructorToStaticMethod.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorToStaticMethod.java:25: Information: Replacement available [ReplaceWith]
new ReplaceWithUsageJava(10000);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/ConstructorToStaticMethod.java line 25: Replace with `ReplaceWithUsageJava.newInstance(10000)`:
+Fix for src/replacewith/ConstructorToStaticMethod.java line 25: Replace with `ReplaceWithUsageJava.newInstance(10000)`:
@@ -25 +25
- new ReplaceWithUsageJava(10000);
+ ReplaceWithUsageJava.newInstance(10000);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
similarity index 75%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
index 22e983a..61474a2 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test
@@ -27,20 +27,20 @@
@Test
fun staticFieldExplicitClass() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.StaticFieldExplicitClass")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.StaticFieldExplicitClass")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/StaticFieldExplicitClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticFieldExplicitClass.java:25: Information: Replacement available [ReplaceWith]
System.out.println(ReplaceWithUsageJava.AUTOFILL_HINT_NAME);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/StaticFieldExplicitClass.java line 25: Replace with `View.AUTOFILL_HINT_NAME`:
+Fix for src/replacewith/StaticFieldExplicitClass.java line 25: Replace with `View.AUTOFILL_HINT_NAME`:
@@ -25 +25
- System.out.println(ReplaceWithUsageJava.AUTOFILL_HINT_NAME);
+ System.out.println(View.AUTOFILL_HINT_NAME);
@@ -53,20 +53,20 @@
@Test
fun staticFieldImplicitClass() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.StaticFieldImplicitClass")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.StaticFieldImplicitClass")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/StaticFieldImplicitClass.java:27: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticFieldImplicitClass.java:27: Information: Replacement available [ReplaceWith]
System.out.println(AUTOFILL_HINT_NAME);
~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/StaticFieldImplicitClass.java line 27: Replace with `View.AUTOFILL_HINT_NAME`:
+Fix for src/replacewith/StaticFieldImplicitClass.java line 27: Replace with `View.AUTOFILL_HINT_NAME`:
@@ -27 +27
- System.out.println(AUTOFILL_HINT_NAME);
+ System.out.println(View.AUTOFILL_HINT_NAME);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
similarity index 73%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
index e1b4e28..33d5845 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
import org.junit.Test
import org.junit.runner.RunWith
@@ -26,20 +26,20 @@
@Test
fun staticMethodExplicitClass() {
val input = arrayOf(
- javaSample("sample.ReplaceWithUsageJava"),
- javaSample("sample.StaticMethodExplicitClass")
+ javaSample("replacewith.ReplaceWithUsageJava"),
+ javaSample("replacewith.StaticMethodExplicitClass")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/StaticMethodExplicitClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticMethodExplicitClass.java:25: Information: Replacement available [ReplaceWith]
ReplaceWithUsageJava.toString(this);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/StaticMethodExplicitClass.java line 25: Replace with `this.toString()`:
+Fix for src/replacewith/StaticMethodExplicitClass.java line 25: Replace with `this.toString()`:
@@ -25 +25
- ReplaceWithUsageJava.toString(this);
+ this.toString();
@@ -52,19 +52,19 @@
@Test
fun methodImplicitThis() {
val input = arrayOf(
- javaSample("sample.MethodImplicitThis")
+ javaSample("replacewith.MethodImplicitThis")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/MethodImplicitThis.java:33: Information: Replacement available [ReplaceWith]
+src/replacewith/MethodImplicitThis.java:33: Information: Replacement available [ReplaceWith]
oldMethod(null);
~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/MethodImplicitThis.java line 33: Replace with `newMethod(null)`:
+Fix for src/replacewith/MethodImplicitThis.java line 33: Replace with `newMethod(null)`:
@@ -33 +33
- oldMethod(null);
+ newMethod(null);
@@ -77,19 +77,19 @@
@Test
fun methodExplicitThis() {
val input = arrayOf(
- javaSample("sample.MethodExplicitThis")
+ javaSample("replacewith.MethodExplicitThis")
)
/* ktlint-disable max-line-length */
val expected = """
-src/sample/MethodExplicitThis.java:33: Information: Replacement available [ReplaceWith]
+src/replacewith/MethodExplicitThis.java:33: Information: Replacement available [ReplaceWith]
this.oldMethod(null);
~~~~~~~~~~~~~~~
0 errors, 0 warnings
""".trimIndent()
val expectedFixDiffs = """
-Fix for src/sample/MethodExplicitThis.java line 33: Replace with `newMethod(null)`:
+Fix for src/replacewith/MethodExplicitThis.java line 33: Replace with `newMethod(null)`:
@@ -33 +33
- this.oldMethod(null);
+ this.newMethod(null);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
similarity index 96%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
index bcb1dc6..15dcffb 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
+import androidx.build.lint.ReplaceWithDetector
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintResult
diff --git a/lint/OWNERS b/lint/OWNERS
new file mode 100644
index 0000000..bc5d456
--- /dev/null
+++ b/lint/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 461356
+aurimas@google.com
+jeffrygaston@google.com
+juliamcclellan@google.com
diff --git a/annotation/annotation-replacewith-lint/build.gradle b/lint/lint-gradle/build.gradle
similarity index 62%
rename from annotation/annotation-replacewith-lint/build.gradle
rename to lint/lint-gradle/build.gradle
index 918162c..808e484 100644
--- a/annotation/annotation-replacewith-lint/build.gradle
+++ b/lint/lint-gradle/build.gradle
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,6 +14,13 @@
* 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.
+ */
import androidx.build.LibraryType
plugins {
@@ -21,15 +28,9 @@
id("kotlin")
}
-sourceSets {
- test.resources.srcDirs(
- project(":annotation:annotation-replacewith-lint-integration-tests")
- .projectDir.toString() + "/src/main"
- )
-}
-
dependencies {
- compileOnly(libs.androidLintMinApi)
+ compileOnly(libs.androidLintApi)
+ compileOnly(libs.androidLintChecks)
compileOnly(libs.kotlinStdlib)
testImplementation(libs.kotlinStdlib)
@@ -39,10 +40,9 @@
}
androidx {
- name = "ReplaceWith annotation lint checks"
+ name = "Gradle lint checks"
type = LibraryType.LINT
- mavenVersion = LibraryVersions.ANNOTATION_REPLACEWITH
inceptionYear = "2024"
- description = "Lint checks for the ReplaceWith annotation library. Also enforces the " +
- "semantics of Kotlin @Deprecated API replacements from within Java source code."
+ description = "Lint checks to verify usage of Gradle APIs."
}
+
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt
new file mode 100644
index 0000000..b904f17
--- /dev/null
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.lint.gradle
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Checks for usages of [eager APIs](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html).
+ */
+class EagerTaskConfigurationDetector : Detector(), Detector.UastScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
+ UCallExpression::class.java
+ )
+
+ override fun createUastHandler(context: JavaContext): UElementHandler = object :
+ UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ val (containingClassName, replacementMethod) = REPLACEMENTS[node.methodName] ?: return
+ val method = node.resolve() ?: return
+ val containingClass = method.containingClass ?: return
+ // Check that the called method is from the expected class (or a child class) and not an
+ // unrelated method with the same name).
+ if (
+ containingClass.qualifiedName != containingClassName &&
+ containingClass.supers.none { it.qualifiedName == containingClassName }
+ ) return
+
+ val fix = fix()
+ .replace()
+ .with(replacementMethod)
+ .reformat(true)
+ // Don't auto-fix from the command line because the replacement methods don't have
+ // the same return types, so the fixed code likely won't compile.
+ .autoFix(robot = false, independent = false)
+ .build()
+ val incident = Incident(context)
+ .issue(ISSUE)
+ .location(context.getNameLocation(node))
+ .message("Use $replacementMethod instead of ${method.name}")
+ .fix(fix)
+ .scope(node)
+ context.report(incident)
+ }
+ }
+
+ companion object {
+ private const val TASK_CONTAINER = "org.gradle.api.tasks.TaskContainer"
+ private const val DOMAIN_OBJECT_COLLECTION = "org.gradle.api.DomainObjectCollection"
+ private const val TASK_COLLECTION = "org.gradle.api.tasks.TaskCollection"
+
+ // A map from eager method name to the containing class of the method and the name of the
+ // replacement method.
+ private val REPLACEMENTS = mapOf(
+ "create" to Pair(TASK_CONTAINER, "register"),
+ "getByName" to Pair(TASK_CONTAINER, "named"),
+ "all" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
+ "whenTaskAdded" to Pair(TASK_CONTAINER, "configureEach"),
+ "whenObjectAdded" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
+ "getAt" to Pair(TASK_COLLECTION, "named"),
+ )
+
+ val ISSUE = Issue.create(
+ "EagerGradleTaskConfiguration",
+ "Avoid using eager task APIs",
+ """
+ Lazy APIs defer task configuration until the task is needed instead of doing
+ unnecessary work in the configuration phase.
+ See https://docs.gradle.org/current/userguide/task_configuration_avoidance.html for
+ more details.
+ """,
+ Category.CORRECTNESS, 5, Severity.ERROR,
+ Implementation(
+ EagerTaskConfigurationDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
similarity index 66%
rename from annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt
rename to lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
index da3620f..ea34e6f 100644
--- a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 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,20 +14,26 @@
* limitations under the License.
*/
-package androidx.annotation.replacewith.lint
+package androidx.lint.gradle
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
-@Suppress("UnstableApiUsage")
-class ReplaceWithIssueRegistry : IssueRegistry() {
- override val minApi = CURRENT_API
- override val api = 14
- override val issues get() = listOf(ReplaceWithDetector.ISSUE)
+/**
+ * Collection of Gradle lint check issues.
+ */
+class GradleIssueRegistry : IssueRegistry() {
+ override val api = CURRENT_API
+
+ override val issues = listOf(
+ EagerTaskConfigurationDetector.ISSUE,
+ )
+
override val vendor = Vendor(
- feedbackUrl = "https://issuetracker.google.com/issues/new?component=459778",
- identifier = "androidx.annotation.replacewith",
+ // TODO: Update component (or the issue template)
+ feedbackUrl = "https://issuetracker.google.com/issues/new?component=1147525",
+ identifier = "androidx.gradle-lint:gradle-lint-checks",
vendorName = "Android Open Source Project",
)
}
diff --git a/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..e362395
--- /dev/null
+++ b/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.lint.gradle.GradleIssueRegistry
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt
new file mode 100644
index 0000000..d52b3d8
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.lint.gradle
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class EagerTaskConfigurationDetectorTest : GradleLintDetectorTest(
+ detector = EagerTaskConfigurationDetector(),
+ issues = listOf(EagerTaskConfigurationDetector.ISSUE)
+) {
+ @Test
+ fun `Test usage of TaskContainer#create`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.tasks.create("example")
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:4: Error: Use register instead of create [EagerGradleTaskConfiguration]
+ project.tasks.create("example")
+ ~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 4: Replace with register:
+ @@ -4 +4
+ - project.tasks.create("example")
+ + project.tasks.register("example")
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of TaskContainer#getByName`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.tasks.getByName("example")
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:4: Error: Use named instead of getByName [EagerGradleTaskConfiguration]
+ project.tasks.getByName("example")
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 4: Replace with named:
+ @@ -4 +4
+ - project.tasks.getByName("example")
+ + project.tasks.named("example")
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of DomainObjectCollection#all`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Action
+ import org.gradle.api.Project
+ import org.gradle.api.Task
+
+ fun configure(project: Project, action: Action<Task>) {
+ project.tasks.all(action)
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:6: Error: Use configureEach instead of all [EagerGradleTaskConfiguration]
+ project.tasks.all(action)
+ ~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 6: Replace with configureEach:
+ @@ -6 +6
+ - project.tasks.all(action)
+ + project.tasks.configureEach(action)
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of TaskContainer#whenTaskAdded`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Action
+ import org.gradle.api.Project
+ import org.gradle.api.Task
+
+ fun configure(project: Project, action: Action<Task>) {
+ project.tasks.whenTaskAdded(action)
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:6: Error: Use configureEach instead of whenTaskAdded [EagerGradleTaskConfiguration]
+ project.tasks.whenTaskAdded(action)
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 6: Replace with configureEach:
+ @@ -6 +6
+ - project.tasks.whenTaskAdded(action)
+ + project.tasks.configureEach(action)
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of DomainObjectCollection#whenObjectAdded`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Action
+ import org.gradle.api.Project
+ import org.gradle.api.Task
+
+ fun configure(project: Project, action: Action<Task>) {
+ project.tasks.whenObjectAdded(action)
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:6: Error: Use configureEach instead of whenObjectAdded [EagerGradleTaskConfiguration]
+ project.tasks.whenObjectAdded(action)
+ ~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 6: Replace with configureEach:
+ @@ -6 +6
+ - project.tasks.whenObjectAdded(action)
+ + project.tasks.configureEach(action)
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of TaskCollection#getAt`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.tasks.getAt("example")
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:4: Error: Use named instead of getAt [EagerGradleTaskConfiguration]
+ project.tasks.getAt("example")
+ ~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 4: Replace with named:
+ @@ -4 +4
+ - project.tasks.getAt("example")
+ + project.tasks.named("example")
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt
new file mode 100644
index 0000000..64dff60
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.lint.gradle
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+/** Base test setup for lint checks in this project, providing the defined Gradle [STUBS]. */
+abstract class GradleLintDetectorTest(
+ private val detector: Detector,
+ private val issues: List<Issue>
+) : LintDetectorTest() {
+ override fun getDetector(): Detector = detector
+ override fun getIssues(): List<Issue> = issues
+
+ /** Convenience method for running a lint test over the input [files] and [STUBS]. */
+ fun check(vararg files: TestFile): TestLintResult {
+ return lint().files(*STUBS, *files).run()
+ }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
new file mode 100644
index 0000000..245e6c6
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.lint.gradle
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
+
+/**
+ * Common-use stubs of the Gradle API for use in tests. If a test requires additional definitions to
+ * run, these should be added to.
+ */
+internal val STUBS =
+ arrayOf(
+ kotlin(
+ """
+ package org.gradle.api.tasks
+
+ import org.gradle.api.DomainObjectCollection
+ import org.gradle.api.Task
+
+ class TaskContainer : DomainObjectCollection<Task>, TaskCollection<Task> {
+ fun create(name: String) = Unit
+ fun register(name: String) = Unit
+ fun getByName(name: String) = Unit
+ fun named(name: String) = Unit
+ fun whenTaskAdded(action: Action<in T>)
+ }
+
+ interface TaskCollection<T : Task> {
+ fun getAt(name: String) = Unit
+ }
+ """.trimIndent()
+ ),
+ kotlin(
+ """
+ package org.gradle.api
+
+ import org.gradle.api.tasks.TaskContainer
+
+ class Project {
+ val tasks: TaskContainer
+ }
+
+ interface DomainObjectCollection<T> {
+ fun all(action: Action<in T>)
+ fun configureEach(action: Action<in T>)
+ fun whenObjectAdded(action: Action<in T>)
+ }
+
+ interface Action<T>
+
+ interface Task
+ """.trimIndent()
+ )
+ )
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
index 5606b24..0793ef5d 100644
--- a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
@@ -27,9 +27,9 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.kotlin.KotlinUBlockExpression
-import org.jetbrains.uast.kotlin.KotlinULambdaExpression
+import org.jetbrains.uast.ULambdaExpression
/**
* Lint for checking for empty construction of NavDeepLink in the Kotlin DSL,
@@ -57,8 +57,8 @@
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
// valueArgumentCount should be 1 when navDeepLink is called
if (node.valueArgumentCount > 0) {
- val lam = node.valueArguments[0] as KotlinULambdaExpression
- val body = lam.body as KotlinUBlockExpression
+ val lam = node.valueArguments[0] as ULambdaExpression
+ val body = lam.body as UBlockExpression
if (body.expressions.isEmpty()) {
context.report(
EmptyNavDeepLink,
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
index e24917a..a13bd56 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -50,9 +50,9 @@
action: (com.android.build.gradle.api.BaseVariant) -> Unit
) {
when {
- extension is AppExtension -> extension.applicationVariants.all(action)
+ extension is AppExtension -> extension.applicationVariants.configureEach(action)
extension is LibraryExtension -> {
- extension.libraryVariants.all(action)
+ extension.libraryVariants.configureEach(action)
}
else -> throw GradleException(
"safeargs plugin must be used with android app," +
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 9640e44..149fe1c 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -12,13 +12,21 @@
method public androidx.paging.LoadState getPrepend();
method public androidx.paging.LoadState getRefresh();
method public androidx.paging.LoadStates getSource();
+ method public boolean hasError();
+ method public boolean isIdle();
property public final androidx.paging.LoadState append;
+ property public final boolean hasError;
+ property public final boolean isIdle;
property public final androidx.paging.LoadStates? mediator;
property public final androidx.paging.LoadState prepend;
property public final androidx.paging.LoadState refresh;
property public final androidx.paging.LoadStates source;
}
+ public final class CombinedLoadStatesKt {
+ 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> {
method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback onInvalidatedCallback);
method @AnyThread public void invalidate();
@@ -131,7 +139,11 @@
method public androidx.paging.LoadState getAppend();
method public androidx.paging.LoadState getPrepend();
method public androidx.paging.LoadState getRefresh();
+ method public boolean hasError();
+ method public boolean isIdle();
property public final androidx.paging.LoadState append;
+ property public final boolean hasError;
+ property public final boolean isIdle;
property public final androidx.paging.LoadState prepend;
property public final androidx.paging.LoadState refresh;
}
@@ -319,6 +331,77 @@
method public <T> androidx.paging.PagingData<T> from(java.util.List<? extends T> data, androidx.paging.LoadStates sourceLoadStates, optional androidx.paging.LoadStates? mediatorLoadStates);
}
+ public abstract sealed class PagingDataEvent<T> {
+ }
+
+ public static final class PagingDataEvent.Append<T> extends androidx.paging.PagingDataEvent<T> {
+ method public java.util.List<T> getInserted();
+ method public int getNewPlaceholdersAfter();
+ method public int getOldPlaceholdersAfter();
+ method public int getStartIndex();
+ property public final java.util.List<T> inserted;
+ property public final int newPlaceholdersAfter;
+ property public final int oldPlaceholdersAfter;
+ property public final int startIndex;
+ }
+
+ public static final class PagingDataEvent.DropAppend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public int getDropCount();
+ method public int getNewPlaceholdersAfter();
+ method public int getOldPlaceholdersAfter();
+ method public int getStartIndex();
+ property public final int dropCount;
+ property public final int newPlaceholdersAfter;
+ property public final int oldPlaceholdersAfter;
+ property public final int startIndex;
+ }
+
+ public static final class PagingDataEvent.DropPrepend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public int getDropCount();
+ method public int getNewPlaceholdersBefore();
+ method public int getOldPlaceholdersBefore();
+ property public final int dropCount;
+ property public final int newPlaceholdersBefore;
+ property public final int oldPlaceholdersBefore;
+ }
+
+ public static final class PagingDataEvent.Prepend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public java.util.List<T> getInserted();
+ method public int getNewPlaceholdersBefore();
+ method public int getOldPlaceholdersBefore();
+ property public final java.util.List<T> inserted;
+ property public final int newPlaceholdersBefore;
+ property public final int oldPlaceholdersBefore;
+ }
+
+ public static final class PagingDataEvent.Refresh<T> extends androidx.paging.PagingDataEvent<T> {
+ method public androidx.paging.PlaceholderPaddedList<T> getNewList();
+ method public androidx.paging.PlaceholderPaddedList<T> getPreviousList();
+ property public final androidx.paging.PlaceholderPaddedList<T> newList;
+ property public final androidx.paging.PlaceholderPaddedList<T> previousList;
+ }
+
+ public abstract class PagingDataPresenter<T> {
+ ctor public PagingDataPresenter(optional kotlin.coroutines.CoroutineContext mainContext, optional androidx.paging.PagingData<T>? cachedPagingData);
+ method public final void addLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+ method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+ method public final suspend Object? collectFrom(androidx.paging.PagingData<T> pagingData, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method @MainThread public final operator T? get(@IntRange(from=0L) int index);
+ method public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> getLoadStateFlow();
+ method public final kotlinx.coroutines.flow.Flow<kotlin.Unit> getOnPagesUpdatedFlow();
+ method public final int getSize();
+ method @MainThread public final T? peek(@IntRange(from=0L) int index);
+ method public abstract suspend Object? presentPagingDataEvent(androidx.paging.PagingDataEvent<T> event, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public final void refresh();
+ method public final void removeLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+ method public final void removeOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+ method public final void retry();
+ method public final androidx.paging.ItemSnapshotList<T> snapshot();
+ property public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> loadStateFlow;
+ property public final kotlinx.coroutines.flow.Flow<kotlin.Unit> onPagesUpdatedFlow;
+ property public final int size;
+ }
+
public final class PagingDataTransforms {
method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method @CheckResult @kotlin.jvm.JvmSynthetic public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> predicate);
@@ -438,6 +521,18 @@
property public final java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages;
}
+ public interface PlaceholderPaddedList<T> {
+ method public int getDataCount();
+ method public T getItem(int index);
+ method public int getPlaceholdersAfter();
+ method public int getPlaceholdersBefore();
+ method public int getSize();
+ property public abstract int dataCount;
+ property public abstract int placeholdersAfter;
+ property public abstract int placeholdersBefore;
+ property public abstract int size;
+ }
+
@Deprecated public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
ctor @Deprecated public PositionalDataSource();
method @Deprecated public static final int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams params, int totalCount);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 9640e44..149fe1c 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -12,13 +12,21 @@
method public androidx.paging.LoadState getPrepend();
method public androidx.paging.LoadState getRefresh();
method public androidx.paging.LoadStates getSource();
+ method public boolean hasError();
+ method public boolean isIdle();
property public final androidx.paging.LoadState append;
+ property public final boolean hasError;
+ property public final boolean isIdle;
property public final androidx.paging.LoadStates? mediator;
property public final androidx.paging.LoadState prepend;
property public final androidx.paging.LoadState refresh;
property public final androidx.paging.LoadStates source;
}
+ public final class CombinedLoadStatesKt {
+ 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> {
method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback onInvalidatedCallback);
method @AnyThread public void invalidate();
@@ -131,7 +139,11 @@
method public androidx.paging.LoadState getAppend();
method public androidx.paging.LoadState getPrepend();
method public androidx.paging.LoadState getRefresh();
+ method public boolean hasError();
+ method public boolean isIdle();
property public final androidx.paging.LoadState append;
+ property public final boolean hasError;
+ property public final boolean isIdle;
property public final androidx.paging.LoadState prepend;
property public final androidx.paging.LoadState refresh;
}
@@ -319,6 +331,77 @@
method public <T> androidx.paging.PagingData<T> from(java.util.List<? extends T> data, androidx.paging.LoadStates sourceLoadStates, optional androidx.paging.LoadStates? mediatorLoadStates);
}
+ public abstract sealed class PagingDataEvent<T> {
+ }
+
+ public static final class PagingDataEvent.Append<T> extends androidx.paging.PagingDataEvent<T> {
+ method public java.util.List<T> getInserted();
+ method public int getNewPlaceholdersAfter();
+ method public int getOldPlaceholdersAfter();
+ method public int getStartIndex();
+ property public final java.util.List<T> inserted;
+ property public final int newPlaceholdersAfter;
+ property public final int oldPlaceholdersAfter;
+ property public final int startIndex;
+ }
+
+ public static final class PagingDataEvent.DropAppend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public int getDropCount();
+ method public int getNewPlaceholdersAfter();
+ method public int getOldPlaceholdersAfter();
+ method public int getStartIndex();
+ property public final int dropCount;
+ property public final int newPlaceholdersAfter;
+ property public final int oldPlaceholdersAfter;
+ property public final int startIndex;
+ }
+
+ public static final class PagingDataEvent.DropPrepend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public int getDropCount();
+ method public int getNewPlaceholdersBefore();
+ method public int getOldPlaceholdersBefore();
+ property public final int dropCount;
+ property public final int newPlaceholdersBefore;
+ property public final int oldPlaceholdersBefore;
+ }
+
+ public static final class PagingDataEvent.Prepend<T> extends androidx.paging.PagingDataEvent<T> {
+ method public java.util.List<T> getInserted();
+ method public int getNewPlaceholdersBefore();
+ method public int getOldPlaceholdersBefore();
+ property public final java.util.List<T> inserted;
+ property public final int newPlaceholdersBefore;
+ property public final int oldPlaceholdersBefore;
+ }
+
+ public static final class PagingDataEvent.Refresh<T> extends androidx.paging.PagingDataEvent<T> {
+ method public androidx.paging.PlaceholderPaddedList<T> getNewList();
+ method public androidx.paging.PlaceholderPaddedList<T> getPreviousList();
+ property public final androidx.paging.PlaceholderPaddedList<T> newList;
+ property public final androidx.paging.PlaceholderPaddedList<T> previousList;
+ }
+
+ public abstract class PagingDataPresenter<T> {
+ ctor public PagingDataPresenter(optional kotlin.coroutines.CoroutineContext mainContext, optional androidx.paging.PagingData<T>? cachedPagingData);
+ method public final void addLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+ method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+ method public final suspend Object? collectFrom(androidx.paging.PagingData<T> pagingData, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method @MainThread public final operator T? get(@IntRange(from=0L) int index);
+ method public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> getLoadStateFlow();
+ method public final kotlinx.coroutines.flow.Flow<kotlin.Unit> getOnPagesUpdatedFlow();
+ method public final int getSize();
+ method @MainThread public final T? peek(@IntRange(from=0L) int index);
+ method public abstract suspend Object? presentPagingDataEvent(androidx.paging.PagingDataEvent<T> event, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public final void refresh();
+ method public final void removeLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+ method public final void removeOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+ method public final void retry();
+ method public final androidx.paging.ItemSnapshotList<T> snapshot();
+ property public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> loadStateFlow;
+ property public final kotlinx.coroutines.flow.Flow<kotlin.Unit> onPagesUpdatedFlow;
+ property public final int size;
+ }
+
public final class PagingDataTransforms {
method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method @CheckResult @kotlin.jvm.JvmSynthetic public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> predicate);
@@ -438,6 +521,18 @@
property public final java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages;
}
+ public interface PlaceholderPaddedList<T> {
+ method public int getDataCount();
+ method public T getItem(int index);
+ method public int getPlaceholdersAfter();
+ method public int getPlaceholdersBefore();
+ method public int getSize();
+ property public abstract int dataCount;
+ property public abstract int placeholdersAfter;
+ property public abstract int placeholdersBefore;
+ property public abstract int size;
+ }
+
@Deprecated public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
ctor @Deprecated public PositionalDataSource();
method @Deprecated public static final int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams params, int totalCount);
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
index c78f513..a9b5655 100644
--- a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
@@ -932,7 +932,7 @@
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // protected otherwise
- public fun getNullPaddedList(): NullPaddedList<T> = storage
+ public fun getPlaceholderPaddedList(): PlaceholderPaddedList<T> = storage
internal var refreshRetryCallback: Runnable? = null
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
index b727765..c6d537b 100644
--- a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
@@ -29,7 +29,7 @@
internal class PagedStorage<T : Any> :
AbstractList<T>,
LegacyPageFetcher.KeyProvider<Any>,
- NullPaddedList<T> {
+ PlaceholderPaddedList<T> {
private val pages = mutableListOf<Page<*, T>>()
internal val firstLoadedItem: T
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
index 43bec5a2..7cb35cd 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
@@ -16,6 +16,15 @@
package androidx.paging
+import androidx.paging.LoadState.NotLoading
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.firstOrNull
+
/**
* Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator].
*
@@ -105,4 +114,47 @@
op(type, true, state)
}
}
+
+ /**
+ * Returns true when [source] and [mediator] is in [NotLoading] for all [LoadType]
+ */
+ public val isIdle = source.isIdle && mediator?.isIdle ?: true
+
+ /**
+ * Returns true if either [source] or [mediator] has a [LoadType] that is in [LoadState.Error]
+ */
+ @get:JvmName("hasError")
+ public val hasError = source.hasError || mediator?.hasError ?: false
+}
+
+/**
+ * Function to wait on a Flow<CombinedLoadStates> until a load has completed.
+ *
+ * It collects on the Flow<CombinedLoadStates> and suspends until it collects and returns the
+ * firstOrNull [CombinedLoadStates] where all [LoadStates] have settled into a non-loading state
+ * i.e. [LoadState.NotLoading] or [LoadState.Error].
+ *
+ * A use case could be scrolling to a position after refresh has completed:
+ * ```
+ * override fun onCreate(savedInstanceState: Bundle?) {
+ * ...
+ * refreshButton.setOnClickListener {
+ * pagingAdapter.refresh()
+ * lifecycleScope.launch {
+ * // wait for refresh to complete
+ * pagingAdapter.loadStateFlow.awaitNotLoading()
+ * // do work after refresh
+ * recyclerView.scrollToPosition(position)
+ * }
+ * }
+ * }
+ * ```
+ */
+@OptIn(FlowPreview::class)
+public suspend fun Flow<CombinedLoadStates>.awaitNotLoading():
+ @JvmSuppressWildcards CombinedLoadStates? {
+
+ return debounce(1).filter {
+ it.isIdle || it.hasError
+ }.firstOrNull()
}
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
index 442148f..c0626db 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
@@ -18,6 +18,7 @@
import androidx.annotation.RestrictTo
import androidx.paging.LoadState.NotLoading
+import kotlin.jvm.JvmName
/**
* Collection of pagination [LoadState]s - refresh, prepend, and append.
@@ -57,6 +58,20 @@
LoadType.PREPEND -> prepend
}
+ /**
+ * Returns true if either one of [refresh], [append], or [prepend] is in [Error] state.
+ */
+ @get:JvmName("hasError")
+ public val hasError = refresh is LoadState.Error || append is LoadState.Error ||
+ prepend is LoadState.Error
+
+ /**
+ * Returns true if all three LoadState [refresh], [append], and [prepend] are
+ * in [NotLoading] state.
+ */
+ public val isIdle = refresh is NotLoading && append is NotLoading &&
+ prepend is NotLoading
+
internal companion object {
val IDLE = LoadStates(
refresh = NotLoading.Incomplete,
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
index 3167150..500e6e5 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
@@ -30,7 +30,7 @@
pages: List<TransformablePage<T>>,
placeholdersBefore: Int,
placeholdersAfter: Int,
-) : NullPaddedList<T> {
+) : PlaceholderPaddedList<T> {
constructor(
insertEvent: PageEvent.Insert<T>
) : this(
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
index 68a0183..1cf0858 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
@@ -22,7 +22,6 @@
/**
* Events captured from a [PagingData] that was submitted to the [PagingDataPresenter]
*/
-@RestrictTo(LIBRARY_GROUP)
public sealed class PagingDataEvent<T : Any> {
/**
* A prepend load event
@@ -108,14 +107,14 @@
/**
* A refresh load event
*
- * @param [newList] A [NullPaddedList] that contains the metadata of the new list
+ * @param [newList] A [PlaceholderPaddedList] that contains the metadata of the new list
* that is presented upon this refresh event
- * @param [previousList] A [NullPaddedList] that contains the metadata of the list
+ * @param [previousList] A [PlaceholderPaddedList] that contains the metadata of the list
* presented prior to this refresh load event
*/
public class Refresh<T : Any> @RestrictTo(LIBRARY_GROUP) constructor(
- val newList: NullPaddedList<T>,
- val previousList: NullPaddedList<T>,
+ val newList: PlaceholderPaddedList<T>,
+ val previousList: PlaceholderPaddedList<T>,
) : PagingDataEvent<T>() {
override fun equals(other: Any?): Boolean {
return other is Refresh<*> &&
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
index 94d8c9d..17fa56a 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
@@ -27,6 +27,7 @@
import androidx.paging.internal.CopyOnWriteArrayList
import androidx.paging.internal.appendMediatorStatesIfNotNull
import kotlin.coroutines.CoroutineContext
+import kotlin.jvm.JvmSuppressWildcards
import kotlin.jvm.Volatile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
@@ -35,10 +36,31 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.withContext
-import kotlinx.coroutines.yield
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class PagingDataPresenter<T : Any>(
+/**
+ * The class that connects the UI layer to the underlying Paging operations. Takes input from
+ * UI presenters and outputs Paging events (Loads, LoadStateUpdate) in response.
+ *
+ * Paging front ends that implement this class will be able to access loaded data, LoadStates,
+ * and callbacks from LoadState or Page updates. This class also exposes the
+ * [PagingDataEvent] from a [PagingData] for custom logic on how to present Loads, Drops, and
+ * other Paging events.
+ *
+ * For implementation examples, refer to [AsyncPagingDataDiffer] for RecyclerView,
+ * or [LazyPagingItems] for Compose.
+ *
+ * @param [mainContext] The coroutine context that core Paging operations will run on.
+ * Defaults to [Dispatchers.Main]. Main operations executed within this context include
+ * but are not limited to:
+ * 1. flow collection on a [PagingData] for Loads, LoadStateUpdate etc.
+ * 2. emitting [CombinedLoadStates] to the [loadStateFlow]
+ * 3. invoking LoadState and PageUpdate listeners
+ * 4. invoking [presentPagingDataEvent]
+ *
+ * @param [cachedPagingData] a [PagingData] that will initialize this PagingDataPresenter with
+ * any LoadStates or loaded data contained within it.
+ */
+public abstract class PagingDataPresenter<T : Any> (
private val mainContext: CoroutineContext = Dispatchers.Main,
cachedPagingData: PagingData<T>? = null,
) {
@@ -73,16 +95,23 @@
private var lastAccessedIndex: Int = 0
/**
- * Handler for [PagingDataEvent] emitted by a [PagingData] that was submitted to
- * this [PagingDataPresenter]
+ * Handler for [PagingDataEvent] emitted by [PagingData].
+ *
+ * When a [PagingData] is submitted to this PagingDataPresenter through [collectFrom],
+ * page loads, drops, or LoadStateUpdates will be emitted to presenters as [PagingDataEvent]
+ * through this method.
+ *
+ * Presenter layers that communicate directly with [PagingDataPresenter] should override
+ * this method to handle the [PagingDataEvent] accordingly. For example by diffing two
+ * [PagingDataEvent.Refresh] lists, or appending the inserted list of data from
+ * [PagingDataEvent.Prepend] or [PagingDataEvent.Append].
+ *
*/
public abstract suspend fun presentPagingDataEvent(
event: PagingDataEvent<T>,
- )
+ ): @JvmSuppressWildcards Unit
- public open fun postEvents(): Boolean = false
-
- public suspend fun collectFrom(pagingData: PagingData<T>) {
+ public suspend fun collectFrom(pagingData: PagingData<T>): @JvmSuppressWildcards Unit {
collectFromRunner.runInIsolation {
uiReceiver = pagingData.uiReceiver
pagingData.flow.collect { event ->
@@ -133,10 +162,6 @@
)
}
event is Insert -> {
- if (postEvents()) {
- yield()
- }
-
// Process APPEND/PREPEND and send to presenter
presentPagingDataEvent(pageStore.processEvent(event))
@@ -189,10 +214,6 @@
}
}
event is Drop -> {
- if (postEvents()) {
- yield()
- }
-
// Process DROP and send to presenter
presentPagingDataEvent(pageStore.processEvent(event))
@@ -207,11 +228,12 @@
// infinite loops when maxSize is insufficiently large.
lastAccessedIndexUnfulfilled = false
}
- event is PageEvent.LoadStateUpdate ->
+ event is PageEvent.LoadStateUpdate -> {
combinedLoadStatesCollection.set(
sourceLoadStates = event.source,
remoteLoadStates = event.mediator,
)
+ }
}
// Notify page updates after pageStore processes them.
//
@@ -395,7 +417,7 @@
*
* @sample androidx.paging.samples.addLoadStateListenerSample
*/
- public fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
+ public fun addLoadStateListener(listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit) {
combinedLoadStatesCollection.addListener(listener)
}
@@ -405,7 +427,9 @@
* @param listener Previously registered listener.
* @see addLoadStateListener
*/
- public fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
+ public fun removeLoadStateListener(
+ listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit
+ ) {
combinedLoadStatesCollection.removeListener(listener)
}
@@ -430,7 +454,7 @@
placeholdersAfter = placeholdersAfter,
)
// must capture previousList states here before we update pageStore
- val previousList = pageStore as NullPaddedList<T>
+ val previousList = pageStore as PlaceholderPaddedList<T>
// update the store here before event is sent to ensure that snapshot() returned in
// UI update callbacks (onChanged, onInsert etc) reflects the new list
@@ -440,7 +464,7 @@
// send event to UI
presentPagingDataEvent(
PagingDataEvent.Refresh(
- newList = newPageStore as NullPaddedList<T>,
+ newList = newPageStore as PlaceholderPaddedList<T>,
previousList = previousList,
)
)
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
similarity index 68%
rename from paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
index f434749..f9dc9ec 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
@@ -16,15 +16,15 @@
package androidx.paging
-import androidx.annotation.RestrictTo
-
/**
- * Interface to partially-loaded, paged data (generally an immutable snapshot).
+ * Interface to paged list that could contain placeholders.
*
- * Used for diffing in paging-runtime.
+ * Contains a paged list's snapshot state. For example, in the context of
+ * [PagingDataEvent.Refresh] exposed by [PagingDataPresenter], each [PlaceholderPaddedList]
+ * represents a generation of paged data whereby a new generation is distinguished with
+ * a refresh load.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface NullPaddedList<T> {
+public interface PlaceholderPaddedList<T> {
public val placeholdersBefore: Int
public val placeholdersAfter: Int
public val size: Int
diff --git a/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
index fdb2710..96166ac 100644
--- a/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
@@ -1444,12 +1444,12 @@
),
placeholdersBefore = 0,
placeholdersAfter = 0,
- ) as NullPaddedList<Int>
+ ) as PlaceholderPaddedList<Int>
assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
assertThat(presenter.newEvents()).containsExactly(
PagingDataEvent.Refresh(
- previousList = PageStore.initial<Int>(null) as NullPaddedList<Int>,
+ previousList = PageStore.initial<Int>(null) as PlaceholderPaddedList<Int>,
newList = event
)
)
@@ -1475,7 +1475,7 @@
),
placeholdersBefore = 0,
placeholdersAfter = 0,
- ) as NullPaddedList<Int>
+ ) as PlaceholderPaddedList<Int>
)
)
}
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index 3f072d0..d6d8fa0 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -53,6 +53,7 @@
androidTestImplementation(project(":internal-testutils-common"))
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation(project(":internal-testutils-paging"))
+ androidTestImplementation(project(":paging:paging-testing"))
androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
index d95155c..98b8b06 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
@@ -40,12 +40,15 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Rule
@@ -716,6 +719,400 @@
}
@Test
+ fun loadStateListenerYieldsToRecyclerView() {
+ Dispatchers.resetMain() // reset MainDispatcherRule
+ // collection on immediate dispatcher to simulate real lifecycle dispatcher
+ val mainDispatcher = Dispatchers.Main.immediate
+ runTest {
+ val events = mutableListOf<String>()
+ val asyncDiffer = AsyncPagingDataDiffer(
+ diffCallback = object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ // override default Dispatcher.Main with Dispatchers.main.immediate so that
+ // main tasks run without queueing, we need this to simulate real life order of
+ // events
+ mainDispatcher = mainDispatcher,
+ updateCallback = listUpdateCapture,
+ workerDispatcher = backgroundScope.coroutineContext
+ )
+
+ val pager = Pager(
+ config = PagingConfig(
+ pageSize = 10,
+ enablePlaceholders = false,
+ prefetchDistance = 3,
+ initialLoadSize = 10,
+ )
+ ) { TestPagingSource() }
+
+ asyncDiffer.addLoadStateListener {
+ events.add(it.toString())
+ }
+
+ val collectPager = launch(mainDispatcher) {
+ pager.flow.collectLatest { asyncDiffer.submitData(it) }
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ assertThat(events).containsExactly(
+ localLoadStatesOf(refreshLocal = Loading).toString(),
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+ events.clear()
+
+ // Simulate RV dispatch layout which calls multi onBind --> getItem. LoadStateUpdates
+ // from upstream should yield until dispatch layout completes or else
+ // LoadState-based RV updates will crash. See original bug b/150162465.
+ withContext(mainDispatcher) {
+ events.add("start dispatchLayout")
+ asyncDiffer.getItem(6)
+ asyncDiffer.getItem(7) // this triggers load
+ asyncDiffer.getItem(8)
+ events.add("end dispatchLayout")
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ // make sure we received the LoadStateUpdate only after dispatchLayout ended
+ assertThat(events).containsExactly(
+ "start dispatchLayout",
+ "end dispatchLayout",
+ localLoadStatesOf(
+ appendLocal = Loading,
+ prependLocal = NotLoading(true)
+ ).toString(),
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ collectPager.cancel()
+ }
+ }
+
+ @Test
+ fun loadStateFlowYieldsToRecyclerView() {
+ Dispatchers.resetMain() // reset MainDispatcherRule
+ // collection on immediate dispatcher to simulate real lifecycle dispatcher
+ val mainDispatcher = Dispatchers.Main.immediate
+ runTest {
+ val events = mutableListOf<String>()
+ val asyncDiffer = AsyncPagingDataDiffer(
+ diffCallback = object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ // override default Dispatcher.Main with Dispatchers.main.immediate so that
+ // main tasks run without queueing, we need this to simulate real life order of
+ // events
+ mainDispatcher = mainDispatcher,
+ updateCallback = listUpdateCapture,
+ workerDispatcher = backgroundScope.coroutineContext
+ )
+
+ val pager = Pager(
+ config = PagingConfig(
+ pageSize = 10,
+ enablePlaceholders = false,
+ prefetchDistance = 3,
+ initialLoadSize = 10,
+ )
+ ) { TestPagingSource() }
+
+ val collectLoadState = launch(mainDispatcher) {
+ asyncDiffer.loadStateFlow.collect {
+ events.add(it.toString())
+ }
+ }
+
+ val collectPager = launch(mainDispatcher) {
+ pager.flow.collectLatest { asyncDiffer.submitData(it) }
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ // withContext prevents flake in API 28 where sometimes we start asserting before
+ // the NotLoading state is added to events list
+ assertThat(events).containsExactly(
+ localLoadStatesOf(refreshLocal = Loading).toString(),
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ events.clear()
+
+ // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+ // from upstream should yield until dispatch layout completes or else
+ // LoadState-based RV updates will crash. See original bug b/150162465.
+ withContext(mainDispatcher) {
+ events.add("start dispatchLayout")
+ asyncDiffer.getItem(6)
+ asyncDiffer.getItem(7) // this triggers load
+ asyncDiffer.getItem(8)
+ events.add("end dispatchLayout")
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ // make sure we received the LoadStateUpdate only after dispatchLayout ended
+ assertThat(events).containsExactly(
+ "start dispatchLayout",
+ "end dispatchLayout",
+ localLoadStatesOf(
+ appendLocal = Loading,
+ prependLocal = NotLoading(true)
+ ).toString(),
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ collectLoadState.cancel()
+ collectPager.cancel()
+ }
+ }
+
+ @Test
+ fun loadStateFlowYieldsToGetItem() {
+ Dispatchers.resetMain() // reset MainDispatcherRule
+ // collection on immediate dispatcher to simulate real lifecycle dispatcher
+ val mainDispatcher = Dispatchers.Main.immediate
+ runTest {
+ val events = mutableListOf<String>()
+ val asyncDiffer = AsyncPagingDataDiffer(
+ diffCallback = object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ // override default Dispatcher.Main with Dispatchers.main.immediate so that
+ // main tasks run without queueing, we need this to simulate real life order of
+ // events
+ mainDispatcher = mainDispatcher,
+ updateCallback = listUpdateCapture,
+ workerDispatcher = backgroundScope.coroutineContext
+ )
+
+ val pager = Pager(
+ config = PagingConfig(
+ pageSize = 10,
+ enablePlaceholders = false,
+ prefetchDistance = 3,
+ initialLoadSize = 10,
+ )
+ ) { TestPagingSource() }
+
+ val collectInGetItem = launch(mainDispatcher) {
+ asyncDiffer.inGetItem.collect {
+ events.add("inGetItem $it")
+ }
+ }
+
+ val collectLoadState = launch(mainDispatcher) {
+ asyncDiffer.loadStateFlow.collect {
+ events.add(it.toString())
+ }
+ }
+
+ // since we cannot intercept the internal loadStateFlow, we collect from its source
+ // flow to see when the internal flow first collected the LoadState before
+ // waiting for getItem
+ val collectParallelLoadState = launch(mainDispatcher) {
+ asyncDiffer.presenter.loadStateFlow.filterNotNull().collect {
+ events.add("internal flow collected")
+ }
+ }
+
+ val collectPager = launch(mainDispatcher) {
+ pager.flow.collectLatest { asyncDiffer.submitData(it) }
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ assertThat(events).containsExactly(
+ "inGetItem false",
+ "internal flow collected",
+ localLoadStatesOf(refreshLocal = Loading).toString(),
+ "internal flow collected",
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ // reset events count
+ events.clear()
+
+ // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+ // from upstream should yield until dispatch layout completes or else
+ // LoadState-based RV updates will crash. See original bug b/150162465.
+ withContext(mainDispatcher) {
+ events.add("start dispatchLayout")
+ asyncDiffer.getItem(6)
+ asyncDiffer.getItem(7) // this triggers load
+ asyncDiffer.getItem(8)
+ events.add("end dispatchLayout")
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ // assert two things: loadStates were received after dispatchLayout and that
+ // the internal flow did collect a LoadState while inGetItem is true but had waited
+ assertThat(events).containsExactly(
+ "start dispatchLayout",
+ // getItem(6)
+ "inGetItem true",
+ "inGetItem false",
+ // getItem(7) triggers append
+ "inGetItem true",
+ "internal flow collected",
+ "inGetItem false",
+ // getItem(8)
+ "inGetItem true",
+ "inGetItem false",
+ "end dispatchLayout",
+ localLoadStatesOf(
+ appendLocal = Loading,
+ prependLocal = NotLoading(true)
+ ).toString(),
+ "internal flow collected",
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ collectInGetItem.cancel()
+ collectLoadState.cancel()
+ collectParallelLoadState.cancel()
+ collectPager.cancel()
+ }
+ }
+
+ @Test
+ fun loadStateListenerYieldsToGetItem() {
+ Dispatchers.resetMain() // reset MainDispatcherRule
+ // collection on immediate dispatcher to simulate real lifecycle dispatcher
+ val mainDispatcher = Dispatchers.Main.immediate
+ runTest {
+ val events = mutableListOf<String>()
+ val asyncDiffer = AsyncPagingDataDiffer(
+ diffCallback = object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ // override default Dispatcher.Main with Dispatchers.main.immediate so that
+ // main tasks run without queueing, we need this to simulate real life order of
+ // events
+ mainDispatcher = mainDispatcher,
+ updateCallback = listUpdateCapture,
+ workerDispatcher = backgroundScope.coroutineContext
+ )
+
+ val pager = Pager(
+ config = PagingConfig(
+ pageSize = 10,
+ enablePlaceholders = false,
+ prefetchDistance = 3,
+ initialLoadSize = 10,
+ )
+ ) { TestPagingSource() }
+
+ val collectInGetItem = launch(mainDispatcher) {
+ asyncDiffer.inGetItem.collect {
+ events.add("inGetItem $it")
+ }
+ }
+
+ // override internal loadStateListener that is registered with PagingDataPresenter
+ asyncDiffer.addLoadStateListenerInternal {
+ events.add("internal listener invoked")
+ asyncDiffer.internalLoadStateListener.invoke(it)
+ }
+
+ // add actual UI listener
+ asyncDiffer.addLoadStateListener {
+ events.add(it.toString())
+ }
+
+ val collectPager = launch(mainDispatcher) {
+ pager.flow.collectLatest { asyncDiffer.submitData(it) }
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ assertThat(events).containsExactly(
+ "inGetItem false",
+ "internal listener invoked",
+ localLoadStatesOf(refreshLocal = Loading).toString(),
+ "internal listener invoked",
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ // reset events count
+ events.clear()
+
+ // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+ // from upstream should yield until dispatch layout completes or else
+ // LoadState-based RV updates will crash. See original bug b/150162465.
+ withContext(mainDispatcher) {
+ events.add("start dispatchLayout")
+ asyncDiffer.getItem(6)
+ asyncDiffer.getItem(7) // this triggers load
+ asyncDiffer.getItem(8)
+ events.add("end dispatchLayout")
+ }
+
+ // wait till we get all expected events
+ asyncDiffer.loadStateFlow.awaitNotLoading()
+
+ // assert two things: loadStates were received after dispatchLayout and that
+ // the internal listener was invoked while inGetItem is true but had waited
+ assertThat(events).containsExactly(
+ "start dispatchLayout",
+ // getItem(6)
+ "inGetItem true",
+ "inGetItem false",
+ // getItem(7) triggers append
+ "inGetItem true",
+ "internal listener invoked",
+ "inGetItem false",
+ // getItem(8)
+ "inGetItem true",
+ "inGetItem false",
+ "end dispatchLayout",
+ localLoadStatesOf(
+ appendLocal = Loading,
+ prependLocal = NotLoading(true)
+ ).toString(),
+ "internal listener invoked",
+ localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+ ).inOrder()
+
+ collectInGetItem.cancel()
+ collectPager.cancel()
+ }
+ }
+
+ @Test
fun insertPageEmpty() = verifyPrependAppendCallback(
initialItems = 2,
initialNulls = 0,
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
similarity index 99%
rename from paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt
rename to paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
index 6b27bef..9a2eb07 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
@@ -33,12 +33,12 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class NullPaddedListDiffHelperTest {
+class PlaceholderPaddedListDiffHelperTest {
class Storage(
override val placeholdersBefore: Int,
private val data: List<String>,
override val placeholdersAfter: Int
- ) : NullPaddedList<String> {
+ ) : PlaceholderPaddedList<String> {
override fun getItem(index: Int): String = data[index]
override val size: Int
get() = placeholdersBefore + data.size + placeholdersAfter
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
similarity index 86%
rename from paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt
rename to paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
index bc35df8..812da03 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
@@ -41,10 +41,10 @@
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
-class NullPaddedListDiffWithRecyclerViewTest {
+class PlaceholderPaddedListDiffWithRecyclerViewTest {
private lateinit var context: Context
private lateinit var recyclerView: RecyclerView
- private lateinit var adapter: NullPaddedListAdapter
+ private lateinit var adapter: PlaceholderPaddedListAdapter
@Before
fun init() {
@@ -55,7 +55,7 @@
it.layoutManager = LinearLayoutManager(context)
it.itemAnimator = null
}
- adapter = NullPaddedListAdapter()
+ adapter = PlaceholderPaddedListAdapter()
recyclerView.adapter = adapter
}
@@ -71,7 +71,7 @@
@Test
fun basic() {
- val storage = NullPaddedStorage(
+ val storage = PlaceholderPaddedStorage(
placeholdersBefore = 0,
data = createItems(0, 10),
placeholdersAfter = 0
@@ -90,12 +90,12 @@
@Test
fun distinctLists_fullyOverlappingRange() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 8),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 100, count = 8),
placeholdersAfter = 30
@@ -108,12 +108,12 @@
@Test
fun distinctLists_loadedBefore_or_After() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 10),
placeholdersAfter = 10
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 5,
data = createItems(startId = 5, count = 5),
placeholdersAfter = 20
@@ -126,12 +126,12 @@
@Test
fun distinctLists_partiallyOverlapping() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 0, count = 8),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 15,
data = createItems(startId = 100, count = 8),
placeholdersAfter = 30
@@ -144,12 +144,12 @@
@Test
fun distinctLists_fewerItemsLoaded_withMorePlaceholdersBefore() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 8),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 15,
data = createItems(startId = 100, count = 3),
placeholdersAfter = 30
@@ -162,12 +162,12 @@
@Test
fun distinctLists_noPlaceholdersLeft() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 8),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 0,
data = createItems(startId = 100, count = 3),
placeholdersAfter = 0
@@ -180,12 +180,12 @@
@Test
fun distinctLists_moreItemsLoaded() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 3),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 100, count = 8),
placeholdersAfter = 30
@@ -198,12 +198,12 @@
@Test
fun distinctLists_moreItemsLoaded_andAlsoMoreOffset() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(startId = 10, count = 3),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 15,
data = createItems(startId = 100, count = 8),
placeholdersAfter = 30
@@ -216,12 +216,12 @@
@Test
fun distinctLists_expandShrink() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(10, 10),
placeholdersAfter = 20
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 0,
data = createItems(100, 1),
placeholdersAfter = 0
@@ -236,8 +236,8 @@
* Runs a state restoration test with various "current scroll positions".
*/
private fun distinctListTest_withVariousInitialPositions(
- pre: NullPaddedStorage,
- post: NullPaddedStorage
+ pre: PlaceholderPaddedStorage,
+ post: PlaceholderPaddedStorage
) {
// try restoring positions in different list states
val minSize = minOf(pre.size, post.size)
@@ -260,12 +260,12 @@
@Test
fun distinctLists_visibleRangeRemoved() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(10, 10),
placeholdersAfter = 30
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 0,
data = createItems(100, 4),
placeholdersAfter = 20
@@ -289,12 +289,12 @@
@Test
fun distinctLists_validateDiff() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 10,
data = createItems(10, 10), // their positions won't be in the new list
placeholdersAfter = 20
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 0,
data = createItems(100, 1),
placeholdersAfter = 0
@@ -308,7 +308,7 @@
// this is a random test but if it fails, the exception will have enough information to
// create an isolated test
val rand = Random(System.nanoTime())
- fun randomNullPaddedStorage(startId: Int) = NullPaddedStorage(
+ fun randomPlaceholderPaddedStorage(startId: Int) = PlaceholderPaddedStorage(
placeholdersBefore = rand.nextInt(0, 20),
data = createItems(
startId = startId,
@@ -318,20 +318,20 @@
)
repeat(RANDOM_TEST_REPEAT_SIZE) {
updateDiffTest(
- pre = randomNullPaddedStorage(0),
- post = randomNullPaddedStorage(1_000)
+ pre = randomPlaceholderPaddedStorage(0),
+ post = randomPlaceholderPaddedStorage(1_000)
)
}
}
@Test
fun continuousMatch_1() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 4,
data = createItems(startId = 0, count = 16),
placeholdersAfter = 1
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 1,
data = createItems(startId = 13, count = 4),
placeholdersAfter = 19
@@ -341,12 +341,12 @@
@Test
fun continuousMatch_2() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 6,
data = createItems(startId = 0, count = 9),
placeholdersAfter = 19
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 14,
data = createItems(startId = 4, count = 3),
placeholdersAfter = 11
@@ -356,12 +356,12 @@
@Test
fun continuousMatch_3() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 11,
data = createItems(startId = 0, count = 4),
placeholdersAfter = 6
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 7,
data = createItems(startId = 0, count = 1),
placeholdersAfter = 11
@@ -371,12 +371,12 @@
@Test
fun continuousMatch_4() {
- val pre = NullPaddedStorage(
+ val pre = PlaceholderPaddedStorage(
placeholdersBefore = 4,
data = createItems(startId = 0, count = 15),
placeholdersAfter = 18
)
- val post = NullPaddedStorage(
+ val post = PlaceholderPaddedStorage(
placeholdersBefore = 11,
data = createItems(startId = 5, count = 17),
placeholdersAfter = 9
@@ -404,7 +404,7 @@
// this is a random test but if it fails, the exception will have enough information to
// create an isolated test
val rand = Random(System.nanoTime())
- fun randomNullPaddedStorage(startId: Int) = NullPaddedStorage(
+ fun randomPlaceholderPaddedStorage(startId: Int) = PlaceholderPaddedStorage(
placeholdersBefore = rand.nextInt(0, 20),
data = createItems(
startId = startId,
@@ -416,8 +416,8 @@
placeholdersAfter = rand.nextInt(0, 20)
)
repeat(RANDOM_TEST_REPEAT_SIZE) {
- val pre = randomNullPaddedStorage(0)
- val post = randomNullPaddedStorage(
+ val pre = randomPlaceholderPaddedStorage(0)
+ val post = randomPlaceholderPaddedStorage(
startId = if (pre.dataCount > 0) {
pre.getItem(rand.nextInt(pre.dataCount)).id
} else {
@@ -435,18 +435,18 @@
* Validates that the update events between [pre] and [post] are correct.
*/
private fun updateDiffTest(
- pre: NullPaddedStorage,
- post: NullPaddedStorage
+ pre: PlaceholderPaddedStorage,
+ post: PlaceholderPaddedStorage
) {
val callback = ValidatingListUpdateCallback(pre, post)
- val diffResult = pre.computeDiff(post, NullPaddedListItem.CALLBACK)
+ val diffResult = pre.computeDiff(post, PlaceholderPaddedListItem.CALLBACK)
pre.dispatchDiff(callback, post, diffResult)
callback.validateRunningListAgainst()
}
private fun distinctListTest(
- pre: NullPaddedStorage,
- post: NullPaddedStorage,
+ pre: PlaceholderPaddedStorage,
+ post: PlaceholderPaddedStorage,
initialListPos: Int,
finalListPos: Int = initialListPos
) {
@@ -485,8 +485,8 @@
* with UI snapshots.
*/
private fun swapListTest(
- pre: NullPaddedStorage,
- post: NullPaddedStorage,
+ pre: PlaceholderPaddedStorage,
+ post: PlaceholderPaddedStorage,
preSwapAction: () -> Unit = {},
postSwapAction: () -> Unit = {},
validate: (preCapture: List<UIItemSnapshot>, postCapture: List<UIItemSnapshot>) -> Unit
@@ -509,7 +509,8 @@
return (0 until recyclerView.childCount).mapNotNull { childPos ->
val view = recyclerView.getChildAt(childPos)!!
if (view.top < RV_HEIGHT && view.bottom > 0) {
- val viewHolder = recyclerView.getChildViewHolder(view) as NullPaddedListViewHolder
+ val viewHolder =
+ recyclerView.getChildViewHolder(view) as PlaceholderPaddedListViewHolder
UIItemSnapshot(
top = view.top,
boundItem = viewHolder.boundItem,
@@ -524,16 +525,17 @@
/**
* Custom adapter class that also validates its update events to ensure they are correct.
*/
- private class NullPaddedListAdapter : RecyclerView.Adapter<NullPaddedListViewHolder>() {
- private var items: NullPaddedList<NullPaddedListItem>? = null
+ private class PlaceholderPaddedListAdapter :
+ RecyclerView.Adapter<PlaceholderPaddedListViewHolder>() {
+ private var items: PlaceholderPaddedList<PlaceholderPaddedListItem>? = null
- fun setItems(items: NullPaddedList<NullPaddedListItem>) {
+ fun setItems(items: PlaceholderPaddedList<PlaceholderPaddedListItem>) {
val previousItems = this.items
val myItems = this.items
if (myItems == null) {
notifyItemRangeInserted(0, items.size)
} else {
- val diff = myItems.computeDiff(items, NullPaddedListItem.CALLBACK)
+ val diff = myItems.computeDiff(items, PlaceholderPaddedListItem.CALLBACK)
val diffObserver = TrackingAdapterObserver(previousItems, items)
registerAdapterDataObserver(diffObserver)
val callback = AdapterListUpdateCallback(this)
@@ -547,8 +549,8 @@
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
- ): NullPaddedListViewHolder {
- return NullPaddedListViewHolder(parent.context).also {
+ ): PlaceholderPaddedListViewHolder {
+ return PlaceholderPaddedListViewHolder(parent.context).also {
it.itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
ITEM_HEIGHT
@@ -556,7 +558,7 @@
}
}
- override fun onBindViewHolder(holder: NullPaddedListViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: PlaceholderPaddedListViewHolder, position: Int) {
val item = items?.get(position)
holder.boundItem = item
holder.boundPos = position
@@ -567,22 +569,22 @@
}
}
- private data class NullPaddedListItem(
+ private data class PlaceholderPaddedListItem(
val id: Int,
val value: String
) {
companion object {
- val CALLBACK = object : DiffUtil.ItemCallback<NullPaddedListItem>() {
+ val CALLBACK = object : DiffUtil.ItemCallback<PlaceholderPaddedListItem>() {
override fun areItemsTheSame(
- oldItem: NullPaddedListItem,
- newItem: NullPaddedListItem
+ oldItem: PlaceholderPaddedListItem,
+ newItem: PlaceholderPaddedListItem
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
- oldItem: NullPaddedListItem,
- newItem: NullPaddedListItem
+ oldItem: PlaceholderPaddedListItem,
+ newItem: PlaceholderPaddedListItem
): Boolean {
return oldItem == newItem
}
@@ -590,10 +592,10 @@
}
}
- private class NullPaddedListViewHolder(
+ private class PlaceholderPaddedListViewHolder(
context: Context
) : RecyclerView.ViewHolder(View(context)) {
- var boundItem: NullPaddedListItem? = null
+ var boundItem: PlaceholderPaddedListItem? = null
var boundPos: Int = -1
override fun toString(): String {
return "VH[$boundPos , $boundItem]"
@@ -604,16 +606,16 @@
// top coordinate of the item
val top: Int,
// the item it is bound to, unless it was a placeholder
- val boundItem: NullPaddedListItem?,
+ val boundItem: PlaceholderPaddedListItem?,
// the position it was bound to
val boundPos: Int
)
- private class NullPaddedStorage(
+ private class PlaceholderPaddedStorage(
override val placeholdersBefore: Int,
- private val data: List<NullPaddedListItem>,
+ private val data: List<PlaceholderPaddedListItem>,
override val placeholdersAfter: Int
- ) : NullPaddedList<NullPaddedListItem> {
+ ) : PlaceholderPaddedList<PlaceholderPaddedListItem> {
private val stringRepresentation by lazy {
"""
$placeholdersBefore:${data.size}:$placeholdersAfter
@@ -621,7 +623,7 @@
""".trimIndent()
}
- override fun getItem(index: Int): NullPaddedListItem = data[index]
+ override fun getItem(index: Int): PlaceholderPaddedListItem = data[index]
override val size: Int
get() = placeholdersBefore + data.size + placeholdersAfter
@@ -635,9 +637,9 @@
private fun createItems(
startId: Int,
count: Int
- ): List<NullPaddedListItem> {
+ ): List<PlaceholderPaddedListItem> {
return (startId until startId + count).map {
- NullPaddedListItem(
+ PlaceholderPaddedListItem(
id = it,
value = "$it"
)
@@ -650,7 +652,7 @@
private fun createExpectedSnapshot(
firstItemTopOffset: Int = 0,
startItemIndex: Int,
- backingList: NullPaddedList<NullPaddedListItem>
+ backingList: PlaceholderPaddedList<PlaceholderPaddedListItem>
): List<UIItemSnapshot> {
check(firstItemTopOffset <= 0) {
"first item offset should not be negative"
@@ -682,8 +684,8 @@
* it)
*/
private class ValidatingListUpdateCallback<T>(
- previousList: NullPaddedList<T>?,
- private val newList: NullPaddedList<T>
+ previousList: PlaceholderPaddedList<T>?,
+ private val newList: PlaceholderPaddedList<T>
) : ListUpdateCallback {
// used in assertion messages
val msg = """
@@ -785,8 +787,8 @@
}
private class TrackingAdapterObserver<T>(
- previousList: NullPaddedList<T>?,
- postList: NullPaddedList<T>
+ previousList: PlaceholderPaddedList<T>?,
+ postList: PlaceholderPaddedList<T>
) : RecyclerView.AdapterDataObserver() {
private val callback = ValidatingListUpdateCallback(previousList, postList)
@@ -818,7 +820,7 @@
}
}
-private fun <T> NullPaddedList<T>.get(index: Int): T? {
+private fun <T> PlaceholderPaddedList<T>.get(index: Int): T? {
if (index < placeholdersBefore) return null
val storageIndex = index - placeholdersBefore
if (storageIndex >= dataCount) return null
@@ -828,7 +830,8 @@
/**
* Create a snapshot of this current that can be used to verify diffs.
*/
-private fun <T> NullPaddedList<T>.createSnapshot(): MutableList<ListSnapshotItem> = (0 until size)
+private fun <T> PlaceholderPaddedList<T>.createSnapshot():
+ MutableList<ListSnapshotItem> = (0 until size)
.mapTo(mutableListOf()) { pos ->
get(pos)?.let {
ListSnapshotItem.Item(it)
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
index 5ed8435..27f5d14 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
@@ -422,8 +422,8 @@
val recordingCallback = RecordingCallback()
pagedList.addWeakCallback(recordingCallback)
config.backgroundThreadExecutor.execute {
- val result = oldSnapshot.getNullPaddedList().computeDiff(
- newSnapshot.getNullPaddedList(),
+ val result = oldSnapshot.getPlaceholderPaddedList().computeDiff(
+ newSnapshot.getPlaceholderPaddedList(),
config.diffCallback
)
@@ -446,7 +446,7 @@
internal fun latchPagedList(
@Suppress("DEPRECATION") newList: PagedList<T>,
@Suppress("DEPRECATION") diffSnapshot: PagedList<T>,
- diffResult: NullPaddedDiffResult,
+ diffResult: PlaceholderPaddedDiffResult,
recordingCallback: RecordingCallback,
lastAccessIndex: Int,
commitCallback: Runnable?
@@ -461,9 +461,9 @@
snapshot = null
// dispatch updates to UI from previousSnapshot -> newSnapshot
- previousSnapshot.getNullPaddedList().dispatchDiff(
+ previousSnapshot.getPlaceholderPaddedList().dispatchDiff(
callback = updateCallback,
- newList = diffSnapshot.getNullPaddedList(),
+ newList = diffSnapshot.getPlaceholderPaddedList(),
diffResult = diffResult
)
@@ -481,9 +481,9 @@
// Note: we don't take into account loads between new list snapshot and new list, but
// this is only a problem in rare cases when placeholders are disabled, and a load
// starts (for some reason) and finishes before diff completes.
- val newPosition = previousSnapshot.getNullPaddedList().transformAnchorIndex(
+ val newPosition = previousSnapshot.getPlaceholderPaddedList().transformAnchorIndex(
diffResult,
- diffSnapshot.getNullPaddedList(),
+ diffSnapshot.getPlaceholderPaddedList(),
lastAccessIndex
)
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index c0b3e06..b046267 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -16,6 +16,8 @@
package androidx.paging
+import android.os.Handler
+import android.os.Looper
import androidx.annotation.IntRange
import androidx.annotation.MainThread
import androidx.lifecycle.Lifecycle
@@ -23,14 +25,24 @@
import androidx.paging.LoadType.REFRESH
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
+import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
/**
* Helper class for mapping a [PagingData] into a
@@ -133,8 +145,7 @@
)
/** True if we're currently executing [getItem] */
- @Suppress("MemberVisibilityCanBePrivate") // synthetic access
- internal var inGetItem: Boolean = false
+ internal val inGetItem = MutableStateFlow(false)
internal val presenter = object : PagingDataPresenter<T>(mainDispatcher) {
override suspend fun presentPagingDataEvent(event: PagingDataEvent<T>) {
@@ -307,16 +318,6 @@
}
}
}
-
- /**
- * Return if [getItem] is running to post any data modifications.
- *
- * This must be done because RecyclerView can't be modified during an onBind, when
- * [getItem] is generally called.
- */
- override fun postEvents(): Boolean {
- return inGetItem
- }
}
private val submitDataId = AtomicInteger(0)
@@ -411,10 +412,10 @@
@MainThread
fun getItem(@IntRange(from = 0) index: Int): T? {
try {
- inGetItem = true
+ inGetItem.update { true }
return presenter[index]
} finally {
- inGetItem = false
+ inGetItem.update { false }
}
}
@@ -450,11 +451,22 @@
* current [PagingData] changes.
*
* This flow is conflated, so it buffers the last update to [CombinedLoadStates] and
- * immediately delivers the current load states on collection.
+ * delivers the current load states on collection when RecyclerView is not dispatching layout.
*
* @sample androidx.paging.samples.loadStateFlowSample
*/
- val loadStateFlow: Flow<CombinedLoadStates> = presenter.loadStateFlow.filterNotNull()
+ val loadStateFlow: Flow<CombinedLoadStates> = presenter.loadStateFlow
+ .filterNotNull()
+ .buffer(CONFLATED)
+ .transform { it ->
+ if (inGetItem.value) {
+ yield()
+ inGetItem.firstOrNull { isGettingItem ->
+ !isGettingItem
+ }
+ }
+ emit(it)
+ }.flowOn(Dispatchers.Main)
/**
* A hot [Flow] that emits after the pages presented to the UI are updated, even if the
@@ -507,6 +519,22 @@
}
/**
+ * The loadStateListener registered internally with [PagingDataPresenter.addLoadStateListener]
+ * when there are [childLoadStateListeners].
+ *
+ * LoadStateUpdates are dispatched to this single internal listener, which will further
+ * dispatch the loadState to [childLoadStateListeners] when [inGetItem] is false.
+ */
+ private val parentLoadStateListener: AtomicReference<((CombinedLoadStates) -> Unit)?> =
+ AtomicReference(null)
+
+ /**
+ * Stores the list of listeners added through [addLoadStateListener]. Invoked
+ * when inGetItem is false.
+ */
+ private val childLoadStateListeners = CopyOnWriteArrayList<(CombinedLoadStates) -> Unit>()
+
+ /**
* Add a [CombinedLoadStates] listener to observe the loading state of the current [PagingData].
*
* As new [PagingData] generations are submitted and displayed, the listener will be notified to
@@ -519,7 +547,10 @@
* @sample androidx.paging.samples.addLoadStateListenerSample
*/
fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
- presenter.addLoadStateListener(listener)
+ if (parentLoadStateListener.get() == null) {
+ addLoadStateListenerInternal(internalLoadStateListener)
+ }
+ childLoadStateListeners.add(listener)
}
/**
@@ -529,6 +560,42 @@
* @see addLoadStateListener
*/
fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
- presenter.removeLoadStateListener(listener)
+ childLoadStateListeners.remove(listener)
+ if (childLoadStateListeners.isEmpty()) {
+ val parent = parentLoadStateListener.get()
+ parent?.let { presenter.removeLoadStateListener(it) }
+ }
+ }
+
+ internal fun addLoadStateListenerInternal(listener: (CombinedLoadStates) -> Unit) {
+ parentLoadStateListener.set(listener)
+ presenter.addLoadStateListener(listener)
+ }
+
+ internal val internalLoadStateListener: (CombinedLoadStates) -> Unit = { loadState ->
+ if (!inGetItem.value) {
+ childLoadStateListeners.forEach { it(loadState) }
+ } else {
+ LoadStateListenerHandler.apply {
+ // we only want to send the latest LoadState
+ removeCallbacks(LoadStateListenerRunnable)
+ // enqueue child listeners
+ LoadStateListenerRunnable.loadState.set(loadState)
+ post(LoadStateListenerRunnable)
+ }
+ }
+ }
+
+ private val LoadStateListenerHandler by lazy { Handler(Looper.getMainLooper()) }
+
+ private val LoadStateListenerRunnable = object : Runnable {
+ var loadState = AtomicReference<CombinedLoadStates>(null)
+
+ override fun run() {
+ loadState.get()
+ ?.let { state ->
+ childLoadStateListeners.forEach { it(state) }
+ }
+ }
}
}
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedDiffing.md b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedDiffing.md
similarity index 100%
rename from paging/paging-runtime/src/main/java/androidx/paging/NullPaddedDiffing.md
rename to paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedDiffing.md
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
similarity index 95%
rename from paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
rename to paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
index bc332f1..7f0ce24 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
@@ -35,10 +35,10 @@
* To only inform DiffUtil about single loaded page in this case, by pruning all other nulls from
* consideration.
*/
-internal fun <T : Any> NullPaddedList<T>.computeDiff(
- newList: NullPaddedList<T>,
+internal fun <T : Any> PlaceholderPaddedList<T>.computeDiff(
+ newList: PlaceholderPaddedList<T>,
diffCallback: DiffUtil.ItemCallback<T>
-): NullPaddedDiffResult {
+): PlaceholderPaddedDiffResult {
val oldSize = dataCount
val newSize = newList.dataCount
@@ -84,23 +84,23 @@
val hasOverlap = (0 until dataCount).any {
diffResult.convertOldPositionToNew(it) != RecyclerView.NO_POSITION
}
- return NullPaddedDiffResult(
+ return PlaceholderPaddedDiffResult(
diff = diffResult,
hasOverlap = hasOverlap
)
}
/**
- * See NullPaddedDiffing.md for how this works and why it works that way :).
+ * See PlaceholderPaddedDiffing.md for how this works and why it works that way :).
*
* Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
* handle this by passing the snapshot to the callback, and dispatching those changes
* immediately after dispatching this diff.
*/
-internal fun <T : Any> NullPaddedList<T>.dispatchDiff(
+internal fun <T : Any> PlaceholderPaddedList<T>.dispatchDiff(
callback: ListUpdateCallback,
- newList: NullPaddedList<T>,
- diffResult: NullPaddedDiffResult
+ newList: PlaceholderPaddedList<T>,
+ diffResult: PlaceholderPaddedDiffResult
) {
if (diffResult.hasOverlap) {
OverlappingListsDiffDispatcher.dispatchDiff(
@@ -125,9 +125,9 @@
* Given an oldPosition representing an anchor in the old data set, computes its new position
* after the diff, or a guess if it no longer exists.
*/
-internal fun NullPaddedList<*>.transformAnchorIndex(
- diffResult: NullPaddedDiffResult,
- newList: NullPaddedList<*>,
+internal fun PlaceholderPaddedList<*>.transformAnchorIndex(
+ diffResult: PlaceholderPaddedDiffResult,
+ newList: PlaceholderPaddedList<*>,
oldPosition: Int
): Int {
if (!diffResult.hasOverlap) {
@@ -163,21 +163,21 @@
return oldPosition.coerceIn(0 until newList.size)
}
-internal class NullPaddedDiffResult(
+internal class PlaceholderPaddedDiffResult(
val diff: DiffUtil.DiffResult,
// true if two lists have at least 1 item the same
val hasOverlap: Boolean
)
/**
- * Helper class to implement the heuristic documented in NullPaddedDiffing.md.
+ * Helper class to implement the heuristic documented in PlaceholderPaddedDiffing.md.
*/
internal object OverlappingListsDiffDispatcher {
fun <T> dispatchDiff(
- oldList: NullPaddedList<T>,
- newList: NullPaddedList<T>,
+ oldList: PlaceholderPaddedList<T>,
+ newList: PlaceholderPaddedList<T>,
callback: ListUpdateCallback,
- diffResult: NullPaddedDiffResult
+ diffResult: PlaceholderPaddedDiffResult
) {
val callbackWrapper = PlaceholderUsingUpdateCallback(
oldList = oldList,
@@ -190,8 +190,8 @@
@Suppress("NOTHING_TO_INLINE")
private class PlaceholderUsingUpdateCallback<T>(
- private val oldList: NullPaddedList<T>,
- private val newList: NullPaddedList<T>,
+ private val oldList: PlaceholderPaddedList<T>,
+ private val newList: PlaceholderPaddedList<T>,
private val callback: ListUpdateCallback
) : ListUpdateCallback {
// These variables hold the "current" value for placeholders and storage count and are
@@ -464,8 +464,8 @@
internal object DistinctListsDiffDispatcher {
fun <T : Any> dispatchDiff(
callback: ListUpdateCallback,
- oldList: NullPaddedList<T>,
- newList: NullPaddedList<T>,
+ oldList: PlaceholderPaddedList<T>,
+ newList: PlaceholderPaddedList<T>,
) {
val storageOverlapStart = maxOf(
oldList.placeholdersBefore, newList.placeholdersBefore
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
index 0a38d7d..b1af845 100644
--- a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -25,6 +25,7 @@
import androidx.paging.PagingData
import androidx.paging.PagingDataEvent
import androidx.paging.PagingDataPresenter
+import androidx.paging.awaitNotLoading
import androidx.paging.testing.ErrorRecovery.RETRY
import androidx.paging.testing.ErrorRecovery.RETURN_CURRENT_SNAPSHOT
import androidx.paging.testing.ErrorRecovery.THROW
@@ -36,10 +37,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
/**
@@ -188,11 +186,9 @@
internal suspend fun <Value : Any> CompletablePagingDataPresenter<Value>.awaitNotLoading(
errorHandler: LoadErrorHandler
) {
- val state = completableLoadStateFlow.filterNotNull().debounce(1).filter {
- it.isIdle() || it.hasError()
- }.firstOrNull()
+ val state = completableLoadStateFlow.filterNotNull().awaitNotLoading()
- if (state != null && state.hasError()) {
+ if (state != null && state.hasError) {
handleLoadError(state, errorHandler)
}
}
@@ -210,26 +206,6 @@
}
private class ReturnSnapshotStub : Exception()
-private fun CombinedLoadStates?.isIdle(): Boolean {
- if (this == null) return false
- return source.isIdle() && mediator?.isIdle() ?: true
-}
-
-private fun LoadStates.isIdle(): Boolean {
- return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
- prepend is LoadState.NotLoading
-}
-
-private fun CombinedLoadStates?.hasError(): Boolean {
- if (this == null) return false
- return source.hasError() || mediator?.hasError() ?: false
-}
-
-private fun LoadStates.hasError(): Boolean {
- return refresh is LoadState.Error || append is LoadState.Error ||
- prepend is LoadState.Error
-}
-
private fun CombinedLoadStates.getErrorState(): LoadState.Error {
return if (refresh is LoadState.Error) {
refresh as LoadState.Error
diff --git a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
index d425c20..ecb52eb 100644
--- a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
@@ -390,7 +390,7 @@
* Computes the offset to add to the index when loading items from presenter.
*
* The purpose of this is to address shifted item positions when new items are prepended
- * with placeholders disabled. For example, loaded items(10-12) in the NullPaddedList
+ * with placeholders disabled. For example, loaded items(10-12) in the PlaceholderPaddedList
* would have item(12) at presenter[2]. If we prefetched items(7-9), item(12) would now be in
* presenter[5].
*
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index c4cb7e1..5b6eb52 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -24,7 +24,7 @@
# This separation is necessary to ensure gradle can read certain properties
# at configuration time.
-org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true
+org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true -Dlint.nullness.ignore-deprecated=true
org.gradle.configureondemand=true
org.gradle.parallel=true
org.gradle.caching=true
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
index a86db9e..d95d88a 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
@@ -110,6 +110,7 @@
/* minAdServicesVersion=*/ 10,
/* minExtServicesVersion=*/ 10))
+ mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
val managerCompat = from(mContext)
// Actually invoke the compat code.
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
index fbfde14..e55c368 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
@@ -156,6 +156,7 @@
Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+ mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
val managerCompat = obtain(mContext)
val getAdSelectionDataRequest = GetAdSelectionDataRequest(seller)
// Verify that it throws an exception
@@ -177,6 +178,7 @@
Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+ mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
val managerCompat = obtain(mContext)
val persistAdSelectionResultRequest = PersistAdSelectionResultRequest(
adSelectionId,
@@ -202,6 +204,7 @@
Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+ mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
val managerCompat = obtain(mContext)
val reportImpressionRequest = ReportImpressionRequest(adSelectionId)
@@ -225,6 +228,7 @@
Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+ mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
val managerCompat = obtain(mContext)
// Verify that it throws an exception
assertThrows(UnsupportedOperationException::class.java) {
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
index 7e2e54b..16aef34 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
@@ -50,6 +50,7 @@
}
@Test
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun goFromV1ToV2() {
createFirstVersion()
val db = helper.runMigrationsAndValidate(
@@ -83,6 +84,7 @@
}
@Test
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun testAutoMigrationWithNewEmbeddedField() {
val embeddedHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index 625d44f..96450ac 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -174,6 +174,7 @@
@Test
@Throws(IOException::class)
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun removeColumn() {
helper.createDatabase(TEST_DB, 4)
val db = helper.runMigrationsAndValidate(
@@ -186,6 +187,7 @@
@Test
@Throws(IOException::class)
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun dropTable() {
helper.createDatabase(TEST_DB, 5)
val db = helper.runMigrationsAndValidate(
@@ -204,6 +206,7 @@
@Test
@Throws(IOException::class)
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun failedToDropTableDontVerify() {
helper.createDatabase(TEST_DB, 5)
val db = helper.runMigrationsAndValidate(
@@ -244,6 +247,7 @@
@Test
@Throws(IOException::class)
+ @Suppress("DEPRECATION") // Due to TableInfo.read()
fun newTableWithForeignKey() {
helper.createDatabase(TEST_DB, 6)
val db = helper.runMigrationsAndValidate(
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index 4e2e6ab..f3b5539 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -80,6 +80,16 @@
}
}
+afterEvaluate {
+ lint {
+ lintOptions {
+ // Until fully switch to K2, existing FE1.0 usages are legitimate
+ // TODO(b/314151707)
+ disable("KotlincFE10")
+ }
+ }
+}
+
androidx {
name = "Room XProcessor Testing"
type = LibraryType.ANNOTATION_PROCESSOR_UTILS
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 0856f18..dc9a2b9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -79,6 +79,7 @@
val ROOM_OPEN_DELEGATE = XClassName.get(ROOM_PACKAGE, "RoomOpenDelegate")
val ROOM_OPEN_DELEGATE_VALIDATION_RESULT =
XClassName.get(ROOM_PACKAGE, "RoomOpenDelegate", "ValidationResult")
+ val STATEMENT_UTIL = XClassName.get("$ROOM_PACKAGE.util", "SQLiteStatementUtil")
}
object RoomAnnotationTypeNames {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 6fbea1d..be5eb7b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -22,7 +22,6 @@
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XElement
-import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.ext.RoomTypeNames
import androidx.room.migration.bundle.DatabaseBundle
@@ -50,8 +49,8 @@
class DatabaseProcessor(baseContext: Context, val element: XTypeElement) {
val context = baseContext.fork(element)
- val roomDatabaseType: XType by lazy {
- context.processingEnv.requireType(RoomTypeNames.ROOM_DB)
+ private val roomDatabaseTypeElement: XTypeElement by lazy {
+ context.processingEnv.requireTypeElement(RoomTypeNames.ROOM_DB)
}
fun process(): Database {
@@ -70,7 +69,7 @@
validateForeignKeys(element, entities)
validateExternalContentFts(element, entities)
- val extendsRoomDb = roomDatabaseType.isAssignableFrom(element.type)
+ val extendsRoomDb = roomDatabaseTypeElement.type.isAssignableFrom(element.type)
context.checker.check(extendsRoomDb, element, ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
val views = resolveDatabaseViews(viewsMap.values.toList())
@@ -128,6 +127,9 @@
val hasForeignKeys = entities.any { it.foreignKeys.isNotEmpty() }
+ val hasClearAllTables = roomDatabaseTypeElement.getDeclaredMethods()
+ .any { it.name == "clearAllTables" }
+
context.checker.check(
predicate = dbAnnotation.value.version > 0,
element = element,
@@ -142,7 +144,8 @@
views = views,
daoMethods = daoMethods,
exportSchema = dbAnnotation.value.exportSchema,
- enableForeignKeys = hasForeignKeys
+ enableForeignKeys = hasForeignKeys,
+ overrideClearAllTables = hasClearAllTables,
)
database.autoMigrations = processAutoMigrations(element, database.bundle)
return database
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
index a4e230b..e552cd7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
@@ -24,7 +24,9 @@
* Defines a code generation scope where we can provide temporary variables, global variables etc
*/
class CodeGenScope(
- val writer: TypeWriter
+ val writer: TypeWriter,
+ // TODO(b/319660042): Remove once migration to driver API is done.
+ val useDriverApi: Boolean = false
) {
val language = writer.codeLanguage
val builder by lazy { XCodeBlock.builder(language) }
@@ -63,7 +65,7 @@
* Copies all variable indices but excludes generated code.
*/
fun fork(): CodeGenScope {
- val forked = CodeGenScope(writer)
+ val forked = CodeGenScope(writer, useDriverApi)
forked.tmpVarIndices.putAll(tmpVarIndices)
return forked
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
index 095a6dd..67e2337 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
@@ -15,11 +15,17 @@
*/
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.box
import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.Function1TypeSpec
import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SQLiteDriverTypeNames
import androidx.room.solver.CodeGenScope
/**
@@ -74,4 +80,88 @@
}
transactionWrapper?.endTransactionWithControlFlow()
}
+
+ override fun isMigratedToDriver() = adapter?.isMigratedToDriver() == true
+
+ override fun convertAndReturn(
+ sqlQueryVar: String,
+ dbProperty: XPropertySpec,
+ bindStatement: CodeGenScope.(String) -> Unit,
+ returnTypeName: XTypeName,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ when (scope.language) {
+ CodeLanguage.JAVA -> convertAndReturnJava(
+ sqlQueryVar, dbProperty, bindStatement, returnTypeName, inTransaction, scope
+ )
+ CodeLanguage.KOTLIN -> convertAndReturnKotlin(
+ sqlQueryVar, dbProperty, bindStatement, inTransaction, scope
+ )
+ }
+ }
+
+ private fun convertAndReturnJava(
+ sqlQueryVar: String,
+ dbProperty: XPropertySpec,
+ bindStatement: CodeGenScope.(String) -> Unit,
+ returnTypeName: XTypeName,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ val performFunctionName = getPerformFunctionName(inTransaction)
+ val statementVar = scope.getTmpVar("_stmt")
+ scope.builder.addStatement(
+ "return %M(%N, %L, %L)",
+ RoomTypeNames.DB_UTIL.packageMember(performFunctionName),
+ dbProperty,
+ sqlQueryVar,
+ // TODO(b/322387497): Generate lambda syntax if possible
+ Function1TypeSpec(
+ language = scope.language,
+ parameterTypeName = SQLiteDriverTypeNames.STATEMENT,
+ parameterName = statementVar,
+ returnTypeName = returnTypeName.box()
+ ) {
+ val functionScope = scope.fork()
+ bindStatement(functionScope, statementVar)
+ val outVar = functionScope.getTmpVar("_result")
+ adapter?.convert(outVar, statementVar, functionScope)
+ this.addCode(functionScope.generate())
+ this.addStatement("return %L", outVar)
+ }
+ )
+ }
+
+ private fun convertAndReturnKotlin(
+ sqlQueryVar: String,
+ dbProperty: XPropertySpec,
+ bindStatement: CodeGenScope.(String) -> Unit,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ val statementVar = scope.getTmpVar("_stmt")
+ val performFunctionName = getPerformFunctionName(inTransaction)
+ scope.builder.apply {
+ beginControlFlow(
+ "return %M(%N, %L) { %L ->",
+ RoomTypeNames.DB_UTIL.packageMember(performFunctionName),
+ dbProperty,
+ sqlQueryVar,
+ statementVar
+ )
+ bindStatement(scope, statementVar)
+ val outVar = scope.getTmpVar("_result")
+ adapter?.convert(outVar, statementVar, scope)
+ addStatement("%L", outVar)
+ endControlFlow()
+ }
+ }
+
+ private fun getPerformFunctionName(inTransaction: Boolean) =
+ if (inTransaction) {
+ "performReadTransactionBlocking"
+ } else {
+ "performReadBlocking"
+ }
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
index e868677..dde8f15 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
@@ -21,6 +21,7 @@
import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
import androidx.room.compiler.codegen.XTypeName
import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
+import androidx.room.ext.RoomTypeNames.STATEMENT_UTIL
import androidx.room.ext.capitalize
import androidx.room.ext.stripNonJava
import androidx.room.parser.ParsedQuery
@@ -70,13 +71,18 @@
} else {
"getColumnIndexOrThrow"
}
+ val packageMember = if (scope.useDriverApi) {
+ STATEMENT_UTIL.packageMember(indexMethod)
+ } else {
+ CURSOR_UTIL.packageMember(indexMethod)
+ }
scope.builder.addLocalVariable(
name = indexVar,
typeName = XTypeName.PRIMITIVE_INT,
assignExpr = XCodeBlock.of(
scope.language,
"%M(%L, %S)",
- CURSOR_UTIL.packageMember(indexMethod),
+ packageMember,
cursorVarName,
it.columnName
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 570c1f9..28f9d56 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -149,6 +149,10 @@
override fun getDefaultIndexAdapter() = indexAdapter
+ override fun isMigratedToDriver(): Boolean {
+ return relationCollectors.isEmpty()
+ }
+
data class PojoMapping(
val pojo: Pojo,
val matchedFields: List<Field>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
index f6cb819..c11dc17 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
@@ -37,4 +37,7 @@
// (e.g. does done to satisfy @Relation fields).
fun accessedTableNames(): List<String> =
rowAdapters.filterIsInstance<PojoRowAdapter>().flatMap { it.relationTableNames() }
+
+ // TODO(b/319660042): Remove once migration to driver API is done.
+ open fun isMigratedToDriver(): Boolean = false
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
index f2fda6b..83c3c83 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
@@ -17,6 +17,7 @@
package androidx.room.solver.query.result
import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.solver.CodeGenScope
/**
@@ -38,4 +39,22 @@
inTransaction: Boolean,
scope: CodeGenScope
)
+
+ // TODO(b/319660042): Remove once migration to driver API is done.
+ open fun isMigratedToDriver(): Boolean = false
+
+ /**
+ * Receives the SQL and a function to bind args into a statement, it must then generate the
+ * code that steps on the query, reads its columns and returns the result.
+ */
+ open fun convertAndReturn(
+ sqlQueryVar: String,
+ dbProperty: XPropertySpec,
+ bindStatement: CodeGenScope.(String) -> Unit,
+ returnTypeName: XTypeName,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ error("Result binder has not been migrated to use driver API.")
+ }
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
index be467f2..4d3de1b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
@@ -51,4 +51,7 @@
* Gets the default index adapter for the implementation
*/
abstract fun getDefaultIndexAdapter(): IndexAdapter
+
+ // TODO(b/319660042): Remove once migration to driver API is done.
+ open fun isMigratedToDriver(): Boolean = false
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
index 519ff4a..c102404 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
@@ -31,7 +31,8 @@
scope.builder.apply {
rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
addLocalVariable(outVarName, type.asTypeName())
- beginControlFlow("if (%L.moveToFirst())", cursorVarName).apply {
+ val stepName = if (scope.useDriverApi) "step" else "moveToFirst"
+ beginControlFlow("if (%L.$stepName())", cursorVarName).apply {
rowAdapter.convert(outVarName, cursorVarName, scope)
}
nextControlFlow("else").apply {
@@ -53,4 +54,6 @@
endControlFlow()
}
}
+
+ override fun isMigratedToDriver(): Boolean = rowAdapter.isMigratedToDriver()
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 7255e20..7ed2495 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -44,9 +44,10 @@
) {
val stringToEnumMethod = stringToEnumMethod(scope)
scope.builder.apply {
+ val getter = if (scope.useDriverApi) "getText" else "getString"
fun XCodeBlock.Builder.addGetStringStatement() {
addStatement(
- "%L = %N(%L.getString(%L))",
+ "%L = %N(%L.$getter(%L))",
outVarName,
stringToEnumMethod,
cursorVarName,
@@ -72,10 +73,11 @@
scope: CodeGenScope
) {
val enumToStringMethod = enumToStringMethod(scope)
+ val setter = if (scope.useDriverApi) "bindText" else "bindString"
scope.builder.apply {
fun XCodeBlock.Builder.addBindStringStatement() {
addStatement(
- "%L.bindString(%L, %N(%L))",
+ "%L.$setter(%L, %N(%L))",
stmtName, indexVarName, enumToStringMethod, valueVarName,
)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
index d1fe631..25ceb7b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
@@ -45,15 +45,16 @@
enum class Primitive(
val typeName: XTypeName,
val cursorGetter: String,
+ val stmtGetter: String,
val stmtSetter: String,
) {
- INT(PRIMITIVE_INT, "getInt", "bindLong"),
- SHORT(PRIMITIVE_SHORT, "getShort", "bindLong"),
- BYTE(PRIMITIVE_BYTE, "getShort", "bindLong"),
- LONG(PRIMITIVE_LONG, "getLong", "bindLong"),
- CHAR(PRIMITIVE_CHAR, "getInt", "bindLong"),
- FLOAT(PRIMITIVE_FLOAT, "getFloat", "bindDouble"),
- DOUBLE(PRIMITIVE_DOUBLE, "getDouble", "bindDouble"),
+ INT(PRIMITIVE_INT, "getInt", "getLong", "bindLong"),
+ SHORT(PRIMITIVE_SHORT, "getShort", "getLong", "bindLong"),
+ BYTE(PRIMITIVE_BYTE, "getShort", "getLong", "bindLong"),
+ LONG(PRIMITIVE_LONG, "getLong", "getLong", "bindLong"),
+ CHAR(PRIMITIVE_CHAR, "getInt", "getLong", "bindLong"),
+ FLOAT(PRIMITIVE_FLOAT, "getFloat", "getDouble", "bindDouble"),
+ DOUBLE(PRIMITIVE_DOUBLE, "getDouble", "getDouble", "bindDouble"),
}
private fun getAffinity(primitive: Primitive) = when (primitive) {
@@ -77,6 +78,7 @@
}
private val cursorGetter = primitive.cursorGetter
+ private val stmtGetter = primitive.stmtGetter
private val stmtSetter = primitive.stmtSetter
override fun bindToStmt(
@@ -123,14 +125,25 @@
scope.language,
"%L.%L(%L)",
cursorVarName,
- cursorGetter,
+ if (scope.useDriverApi) stmtGetter else cursorGetter,
indexVarName
).let {
- // These primitives don't have an exact cursor getter.
- val castFunction = when (primitive) {
- Primitive.BYTE -> "toByte"
- Primitive.CHAR -> "toChar"
- else -> null
+ // These primitives don't have an exact cursor / statement getter.
+ val castFunction = if (scope.useDriverApi) {
+ when (primitive) {
+ Primitive.INT -> "toInt"
+ Primitive.SHORT -> "toShort"
+ Primitive.BYTE -> "toByte"
+ Primitive.CHAR -> "toChar"
+ Primitive.FLOAT -> "toFloat"
+ else -> null
+ }
+ } else {
+ when (primitive) {
+ Primitive.BYTE -> "toByte"
+ Primitive.CHAR -> "toChar"
+ else -> null
+ }
} ?: return@let it
when (it.language) {
// For Java a cast will suffice
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
index c68a9a9..3ce4028 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
@@ -32,15 +32,16 @@
indexVarName: String,
scope: CodeGenScope
) {
+ val getter = if (scope.useDriverApi) "getText" else "getString"
scope.builder.apply {
if (out.nullability == XNullability.NONNULL) {
- addStatement("%L = %L.getString(%L)", outVarName, cursorVarName, indexVarName)
+ addStatement("%L = %L.$getter(%L)", outVarName, cursorVarName, indexVarName)
} else {
beginControlFlow("if (%L.isNull(%L))", cursorVarName, indexVarName).apply {
addStatement("%L = null", outVarName)
}
nextControlFlow("else").apply {
- addStatement("%L = %L.getString(%L)", outVarName, cursorVarName, indexVarName)
+ addStatement("%L = %L.$getter(%L)", outVarName, cursorVarName, indexVarName)
}
endControlFlow()
}
@@ -53,14 +54,15 @@
valueVarName: String,
scope: CodeGenScope
) {
+ val setter = if (scope.useDriverApi) "bindText" else "bindString"
scope.builder.apply {
if (out.nullability == XNullability.NONNULL) {
- addStatement("%L.bindString(%L, %L)", stmtName, indexVarName, valueVarName)
+ addStatement("%L.$setter(%L, %L)", stmtName, indexVarName, valueVarName)
} else {
beginControlFlow("if (%L == null)", valueVarName)
.addStatement("%L.bindNull(%L)", stmtName, indexVarName)
nextControlFlow("else")
- .addStatement("%L.bindString(%L, %L)", stmtName, indexVarName, valueVarName)
+ .addStatement("%L.$setter(%L, %L)", stmtName, indexVarName, valueVarName)
endControlFlow()
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
index ed1476c..04dec4a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -39,7 +39,8 @@
val daoMethods: List<DaoMethod>,
val version: Int,
val exportSchema: Boolean,
- val enableForeignKeys: Boolean
+ val enableForeignKeys: Boolean,
+ val overrideClearAllTables: Boolean
) {
// This variable will be set once auto-migrations are processed given the DatabaseBundle from
// this object. This is necessary for tracking the versions involved in the auto-migration.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index e4df16a..6b53dfe 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -587,6 +587,28 @@
}
private fun createQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
+ if (!method.queryResultBinder.isMigratedToDriver()) {
+ return compatCreateQueryMethodBody(method)
+ }
+
+ val scope = CodeGenScope(this, useDriverApi = true)
+ val queryWriter = QueryWriter(method)
+ val sqlVar = scope.getTmpVar("_sql")
+ val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
+ method.queryResultBinder.convertAndReturn(
+ sqlQueryVar = sqlVar,
+ dbProperty = dbProperty,
+ bindStatement = { stmtVar ->
+ queryWriter.bindArgs(stmtVar, listSizeArgs, this)
+ },
+ returnTypeName = method.returnType.asTypeName(),
+ inTransaction = method.inTransaction,
+ scope = scope
+ )
+ return scope.generate()
+ }
+
+ private fun compatCreateQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
val queryWriter = QueryWriter(method)
val scope = CodeGenScope(this)
val sqlVar = scope.getTmpVar("_sql")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
index cd4ceca..82bd6f6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
@@ -21,17 +21,16 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.codegen.XPropertySpec.Companion.apply
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
-import androidx.room.ext.AndroidTypeNames
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.KotlinCollectionMemberNames
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.SupportDbTypeNames
import androidx.room.ext.decapitalize
import androidx.room.ext.stripNonJava
import androidx.room.solver.CodeGenScope
@@ -60,7 +59,9 @@
)
addFunction(createOpenDelegate())
addFunction(createCreateInvalidationTracker())
- addFunction(createClearAllTables())
+ if (database.overrideClearAllTables) {
+ addFunction(createClearAllTables())
+ }
addFunction(createCreateTypeConvertersMap())
addFunction(createCreateAutoMigrationSpecsSet())
addFunction(createGetAutoMigrations())
@@ -196,70 +197,19 @@
}
private fun createClearAllTables(): XFunSpec {
- val scope = CodeGenScope(this)
- val body = XCodeBlock.builder(codeLanguage).apply {
- addStatement("super.assertNotMainThread()")
- val dbVar = scope.getTmpVar("_db")
- addLocalVal(
- dbVar,
- SupportDbTypeNames.DB,
- when (language) {
- CodeLanguage.JAVA -> "super.getOpenHelper().getWritableDatabase()"
- CodeLanguage.KOTLIN -> "super.openHelper.writableDatabase"
- }
- )
- val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
- if (database.enableForeignKeys) {
- addLocalVal(
- deferVar,
- XTypeName.PRIMITIVE_BOOLEAN,
- "%L.VERSION.SDK_INT >= %L.VERSION_CODES.LOLLIPOP",
- AndroidTypeNames.BUILD,
- AndroidTypeNames.BUILD
- )
- }
- beginControlFlow("try").apply {
- if (database.enableForeignKeys) {
- beginControlFlow("if (!%L)", deferVar).apply {
- addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = FALSE")
- }
- endControlFlow()
- }
- addStatement("super.beginTransaction()")
- if (database.enableForeignKeys) {
- beginControlFlow("if (%L)", deferVar).apply {
- addStatement("%L.execSQL(%S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
- }
- endControlFlow()
- }
- database.entities.sortedWith(EntityDeleteComparator()).forEach {
- addStatement("%L.execSQL(%S)", dbVar, "DELETE FROM `${it.tableName}`")
- }
- addStatement("super.setTransactionSuccessful()")
- }
- nextControlFlow("finally").apply {
- addStatement("super.endTransaction()")
- if (database.enableForeignKeys) {
- beginControlFlow("if (!%L)", deferVar).apply {
- addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = TRUE")
- }
- endControlFlow()
- }
- addStatement("%L.query(%S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
- beginControlFlow("if (!%L.inTransaction())", dbVar).apply {
- addStatement("%L.execSQL(%S)", dbVar, "VACUUM")
- }
- endControlFlow()
- }
- endControlFlow()
- }.build()
return XFunSpec.builder(
language = codeLanguage,
name = "clearAllTables",
visibility = VisibilityModifier.PUBLIC,
isOverride = true
).apply {
- addCode(body)
+ val tableNames = database.entities.sortedWith(EntityDeleteComparator())
+ .joinToString(", ") { "\"${it.tableName}\"" }
+ addStatement(
+ "super.performClear(%L, %L)",
+ database.enableForeignKeys,
+ tableNames
+ )
}.build()
}
@@ -267,12 +217,11 @@
val scope = CodeGenScope(this)
val body = XCodeBlock.builder(codeLanguage).apply {
val shadowTablesVar = "_shadowTablesMap"
- val shadowTablesTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
+ val shadowTablesTypeParam = arrayOf(
CommonTypeNames.STRING, CommonTypeNames.STRING
)
- val tableNames = database.entities.joinToString(",") {
- "\"${it.tableName}\""
- }
+ val shadowTablesTypeName =
+ CommonTypeNames.MUTABLE_MAP.parametrizedBy(*shadowTablesTypeParam)
val shadowTableNames = database.entities.filter {
it.shadowTableName != null
}.map {
@@ -281,41 +230,68 @@
addLocalVariable(
name = shadowTablesVar,
typeName = shadowTablesTypeName,
- assignExpr = XCodeBlock.ofNewInstance(
- codeLanguage,
- shadowTablesTypeName,
- "%L",
- shadowTableNames.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ codeLanguage,
+ CommonTypeNames.HASH_MAP.parametrizedBy(*shadowTablesTypeParam),
+ "%L",
+ shadowTableNames.size
+ )
+
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_MAP_OF
+ )
+ }
)
shadowTableNames.forEach { (tableName, shadowTableName) ->
addStatement("%L.put(%S, %S)", shadowTablesVar, tableName, shadowTableName)
}
val viewTablesVar = scope.getTmpVar("_viewTables")
- val tablesType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
- val viewTablesType = CommonTypeNames.HASH_MAP.parametrizedBy(
+ val viewTableTypeParam = arrayOf(
CommonTypeNames.STRING,
CommonTypeNames.SET.parametrizedBy(CommonTypeNames.STRING)
)
+ val viewTablesTypeName = CommonTypeNames.MUTABLE_MAP.parametrizedBy(*viewTableTypeParam)
addLocalVariable(
name = viewTablesVar,
- typeName = viewTablesType,
- assignExpr = XCodeBlock.ofNewInstance(
- codeLanguage,
- viewTablesType,
- "%L", database.views.size
- )
+ typeName = viewTablesTypeName,
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ codeLanguage,
+ CommonTypeNames.HASH_MAP.parametrizedBy(*viewTableTypeParam),
+ "%L",
+ database.views.size
+ )
+
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_MAP_OF
+ )
+ }
)
+ val tablesType = CommonTypeNames.MUTABLE_SET.parametrizedBy(CommonTypeNames.STRING)
for (view in database.views) {
val tablesVar = scope.getTmpVar("_tables")
addLocalVariable(
name = tablesVar,
typeName = tablesType,
- assignExpr = XCodeBlock.ofNewInstance(
- codeLanguage,
- tablesType,
- "%L", view.tables.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ codeLanguage,
+ CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING),
+ "%L",
+ view.tables.size
+ )
+
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_SET_OF
+ )
+ }
)
for (table in view.tables) {
addStatement("%L.add(%S)", tablesVar, table)
@@ -325,6 +301,8 @@
viewTablesVar, view.viewName.lowercase(Locale.US), tablesVar
)
}
+ val tableNames =
+ database.entities.joinToString(", ") { "\"${it.tableName}\"" }
addStatement(
"return %L",
XCodeBlock.ofNewInstance(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
index d55ee3b..2e6106a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
@@ -16,9 +16,11 @@
package androidx.room.writer
+import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.KotlinCollectionMemberNames
import androidx.room.ext.RoomMemberNames
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.capitalize
@@ -32,16 +34,23 @@
val expectedInfoVar = scope.getTmpVar("_info$suffix")
scope.builder.apply {
val columnSetVar = scope.getTmpVar("_columns$suffix")
- val columnsSetType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+ val columnsSetType = CommonTypeNames.MUTABLE_SET.parametrizedBy(CommonTypeNames.STRING)
addLocalVariable(
name = columnSetVar,
typeName = columnsSetType,
- assignExpr = XCodeBlock.ofNewInstance(
- language,
- columnsSetType,
- "%L",
- entity.fields.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ language,
+ CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING),
+ "%L",
+ entity.fields.size
+ )
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_SET_OF
+ )
+ }
)
entity.nonHiddenFields.forEach {
addStatement("%L.add(%S)", columnSetVar, it.columnName)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
index 8646d11..b633e460 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
@@ -20,6 +20,7 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.KotlinCollectionMemberNames
import androidx.room.ext.RoomMemberNames
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.capitalize
@@ -40,19 +41,29 @@
val expectedInfoVar = scope.getTmpVar("_info$suffix")
scope.builder.apply {
val columnListVar = scope.getTmpVar("_columns$suffix")
- val columnListType = CommonTypeNames.HASH_MAP.parametrizedBy(
+ val columnListType = CommonTypeNames.MUTABLE_MAP.parametrizedBy(
CommonTypeNames.STRING,
RoomTypeNames.TABLE_INFO_COLUMN
)
addLocalVariable(
name = columnListVar,
typeName = columnListType,
- assignExpr = XCodeBlock.ofNewInstance(
- language,
- columnListType,
- "%L",
- entity.fields.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ language,
+ CommonTypeNames.HASH_MAP.parametrizedBy(
+ CommonTypeNames.STRING,
+ RoomTypeNames.TABLE_INFO_COLUMN
+ ),
+ "%L",
+ entity.fields.size
+ )
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_MAP_OF
+ )
+ }
)
entity.fields.forEach { field ->
addStatement(
@@ -75,16 +86,25 @@
val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
val foreignKeySetType =
- CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
+ CommonTypeNames.MUTABLE_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
addLocalVariable(
name = foreignKeySetVar,
typeName = foreignKeySetType,
- assignExpr = XCodeBlock.ofNewInstance(
- language,
- foreignKeySetType,
- "%L",
- entity.foreignKeys.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ language,
+ CommonTypeNames.HASH_SET.parametrizedBy(
+ RoomTypeNames.TABLE_INFO_FOREIGN_KEY
+ ),
+ "%L",
+ entity.foreignKeys.size
+ )
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_SET_OF
+ )
+ }
)
entity.foreignKeys.forEach {
addStatement(
@@ -105,16 +125,25 @@
val indicesSetVar = scope.getTmpVar("_indices$suffix")
val indicesType =
- CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
+ CommonTypeNames.MUTABLE_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
addLocalVariable(
name = indicesSetVar,
typeName = indicesType,
- assignExpr = XCodeBlock.ofNewInstance(
- language,
- indicesType,
- "%L",
- entity.indices.size
- )
+ assignExpr = when (language) {
+ CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+ language,
+ CommonTypeNames.HASH_SET.parametrizedBy(
+ RoomTypeNames.TABLE_INFO_INDEX
+ ),
+ "%L",
+ entity.indices.size
+ )
+ CodeLanguage.KOTLIN -> XCodeBlock.of(
+ language,
+ "%M()",
+ KotlinCollectionMemberNames.MUTABLE_SET_OF
+ )
+ }
)
entity.indices.forEach { index ->
val orders = if (index.orders.isEmpty()) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 0abf406..2c37b7d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -376,7 +376,8 @@
daoMethods = emptyList(),
version = -1,
exportSchema = false,
- enableForeignKeys = false
+ enableForeignKeys = false,
+ overrideClearAllTables = true,
)
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
index 6370004..aa1b9b7 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
@@ -66,7 +66,8 @@
daoMethods = emptyList(),
version = 1,
exportSchema = false,
- enableForeignKeys = false
+ enableForeignKeys = false,
+ overrideClearAllTables = true
)
val expectedLegacyHash = DigestUtils.md5Hex(
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
index 1d1f41f..6edd091 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
@@ -10,7 +10,9 @@
import androidx.room.guava.GuavaRoom;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
+import androidx.room.util.SQLiteStatementUtil;
import androidx.room.util.StringUtil;
+import androidx.sqlite.SQLiteStatement;
import androidx.sqlite.db.SupportSQLiteQuery;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.Class;
@@ -25,6 +27,7 @@
import java.util.List;
import java.util.concurrent.Callable;
import javax.annotation.processing.Generated;
+import kotlin.jvm.functions.Function1;
@Generated("androidx.room.RoomProcessor")
@SuppressWarnings({"unchecked", "deprecation"})
@@ -82,91 +85,87 @@
@Override
public User getById(final int id) {
final String _sql = "SELECT * FROM user where uid = ?";
- final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
- int _argIndex = 1;
- _statement.bindLong(_argIndex, id);
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
- try {
- final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = new User();
- _result.uid = _cursor.getInt(_cursorIndexOfUid);
- if (_cursor.isNull(_cursorIndexOfName)) {
- _result.name = null;
+ return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+ @Override
+ @NonNull
+ public User invoke(@NonNull final SQLiteStatement _stmt) {
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, id);
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+ final User _result;
+ if (_stmt.step()) {
+ _result = new User();
+ _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+ if (_stmt.isNull(_cursorIndexOfName)) {
+ _result.name = null;
+ } else {
+ _result.name = _stmt.getText(_cursorIndexOfName);
+ }
+ final String _tmpLastName;
+ if (_stmt.isNull(_cursorIndexOfLastName)) {
+ _tmpLastName = null;
+ } else {
+ _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+ }
+ _result.setLastName(_tmpLastName);
+ _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
} else {
- _result.name = _cursor.getString(_cursorIndexOfName);
+ _result = null;
}
- final String _tmpLastName;
- if (_cursor.isNull(_cursorIndexOfLastName)) {
- _tmpLastName = null;
- } else {
- _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
- }
- _result.setLastName(_tmpLastName);
- _result.age = _cursor.getInt(_cursorIndexOfAge);
- } else {
- _result = null;
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- _statement.release();
- }
+ });
}
@Override
public User findByName(final String name, final String lastName) {
final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
- final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
- int _argIndex = 1;
- if (name == null) {
- _statement.bindNull(_argIndex);
- } else {
- _statement.bindString(_argIndex, name);
- }
- _argIndex = 2;
- if (lastName == null) {
- _statement.bindNull(_argIndex);
- } else {
- _statement.bindString(_argIndex, lastName);
- }
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
- try {
- final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = new User();
- _result.uid = _cursor.getInt(_cursorIndexOfUid);
- if (_cursor.isNull(_cursorIndexOfName)) {
- _result.name = null;
+ return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+ @Override
+ @NonNull
+ public User invoke(@NonNull final SQLiteStatement _stmt) {
+ int _argIndex = 1;
+ if (name == null) {
+ _stmt.bindNull(_argIndex);
} else {
- _result.name = _cursor.getString(_cursorIndexOfName);
+ _stmt.bindText(_argIndex, name);
}
- final String _tmpLastName;
- if (_cursor.isNull(_cursorIndexOfLastName)) {
- _tmpLastName = null;
+ _argIndex = 2;
+ if (lastName == null) {
+ _stmt.bindNull(_argIndex);
} else {
- _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _stmt.bindText(_argIndex, lastName);
}
- _result.setLastName(_tmpLastName);
- _result.age = _cursor.getInt(_cursorIndexOfAge);
- } else {
- _result = null;
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+ final User _result;
+ if (_stmt.step()) {
+ _result = new User();
+ _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+ if (_stmt.isNull(_cursorIndexOfName)) {
+ _result.name = null;
+ } else {
+ _result.name = _stmt.getText(_cursorIndexOfName);
+ }
+ final String _tmpLastName;
+ if (_stmt.isNull(_cursorIndexOfLastName)) {
+ _tmpLastName = null;
+ } else {
+ _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+ }
+ _result.setLastName(_tmpLastName);
+ _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+ } else {
+ _result = null;
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- _statement.release();
- }
+ });
}
@Override
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 3688bea..5241793 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -10,7 +10,9 @@
import androidx.room.guava.GuavaRoom;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
+import androidx.room.util.SQLiteStatementUtil;
import androidx.room.util.StringUtil;
+import androidx.sqlite.SQLiteStatement;
import androidx.sqlite.db.SupportSQLiteQuery;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.Class;
@@ -25,6 +27,7 @@
import java.util.List;
import java.util.concurrent.Callable;
import javax.annotation.processing.Generated;
+import kotlin.jvm.functions.Function1;
@Generated("androidx.room.RoomProcessor")
@SuppressWarnings({"unchecked", "deprecation"})
@@ -78,67 +81,63 @@
@Override
public User getById(final int id) {
final String _sql = "SELECT * FROM user where uid = ?";
- final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
- int _argIndex = 1;
- _statement.bindLong(_argIndex, id);
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
- try {
- final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = new User();
- _result.uid = _cursor.getInt(_cursorIndexOfUid);
- _result.name = _cursor.getString(_cursorIndexOfName);
- final String _tmpLastName;
- _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
- _result.setLastName(_tmpLastName);
- _result.age = _cursor.getInt(_cursorIndexOfAge);
- } else {
- _result = null;
+ return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+ @Override
+ @NonNull
+ public User invoke(@NonNull final SQLiteStatement _stmt) {
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, id);
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+ final User _result;
+ if (_stmt.step()) {
+ _result = new User();
+ _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+ _result.name = _stmt.getText(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+ } else {
+ _result = null;
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- _statement.release();
- }
+ });
}
@Override
public User findByName(final String name, final String lastName) {
final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
- final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
- int _argIndex = 1;
- _statement.bindString(_argIndex, name);
- _argIndex = 2;
- _statement.bindString(_argIndex, lastName);
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
- try {
- final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = new User();
- _result.uid = _cursor.getInt(_cursorIndexOfUid);
- _result.name = _cursor.getString(_cursorIndexOfName);
- final String _tmpLastName;
- _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
- _result.setLastName(_tmpLastName);
- _result.age = _cursor.getInt(_cursorIndexOfAge);
- } else {
- _result = null;
+ return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+ @Override
+ @NonNull
+ public User invoke(@NonNull final SQLiteStatement _stmt) {
+ int _argIndex = 1;
+ _stmt.bindText(_argIndex, name);
+ _argIndex = 2;
+ _stmt.bindText(_argIndex, lastName);
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+ final User _result;
+ if (_stmt.step()) {
+ _result = new User();
+ _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+ _result.name = _stmt.getText(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+ } else {
+ _result = null;
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- _statement.release();
- }
+ });
}
@Override
diff --git a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
index 36110cc..f4a1802 100644
--- a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
+++ b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
@@ -10,7 +10,6 @@
import androidx.room.util.ViewInfo;
import androidx.sqlite.SQLiteConnection;
import androidx.sqlite.SQLiteKt;
-import androidx.sqlite.db.SupportSQLiteDatabase;
import java.lang.Class;
import java.lang.Override;
import java.lang.String;
@@ -72,13 +71,13 @@
@NonNull
public RoomOpenDelegate.ValidationResult onValidateSchema(
@NonNull final SQLiteConnection connection) {
- final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
+ final Map<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
_columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
_columnsUser.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsUser.put("lastName", new TableInfo.Column("lastName", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsUser.put("ageColumn", new TableInfo.Column("ageColumn", "INTEGER", true, 0, null, TableInfo.CREATED_FROM_ENTITY));
- final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
- final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
+ final Set<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
+ final Set<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
final TableInfo _existingUser = TableInfo.read(connection, "User");
if (!_infoUser.equals(_existingUser)) {
@@ -86,13 +85,13 @@
+ " Expected:\n" + _infoUser + "\n"
+ " Found:\n" + _existingUser);
}
- final HashMap<String, TableInfo.Column> _columnsChild1 = new HashMap<String, TableInfo.Column>(4);
+ final Map<String, TableInfo.Column> _columnsChild1 = new HashMap<String, TableInfo.Column>(4);
_columnsChild1.put("id", new TableInfo.Column("id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild1.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild1.put("serial", new TableInfo.Column("serial", "INTEGER", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild1.put("code", new TableInfo.Column("code", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
- final HashSet<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
- final HashSet<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
+ final Set<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
+ final Set<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
final TableInfo _infoChild1 = new TableInfo("Child1", _columnsChild1, _foreignKeysChild1, _indicesChild1);
final TableInfo _existingChild1 = TableInfo.read(connection, "Child1");
if (!_infoChild1.equals(_existingChild1)) {
@@ -100,13 +99,13 @@
+ " Expected:\n" + _infoChild1 + "\n"
+ " Found:\n" + _existingChild1);
}
- final HashMap<String, TableInfo.Column> _columnsChild2 = new HashMap<String, TableInfo.Column>(4);
+ final Map<String, TableInfo.Column> _columnsChild2 = new HashMap<String, TableInfo.Column>(4);
_columnsChild2.put("id", new TableInfo.Column("id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild2.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild2.put("serial", new TableInfo.Column("serial", "INTEGER", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsChild2.put("code", new TableInfo.Column("code", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
- final HashSet<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
- final HashSet<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
+ final Set<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
+ final Set<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
final TableInfo _infoChild2 = new TableInfo("Child2", _columnsChild2, _foreignKeysChild2, _indicesChild2);
final TableInfo _existingChild2 = TableInfo.read(connection, "Child2");
if (!_infoChild2.equals(_existingChild2)) {
@@ -130,31 +129,17 @@
@Override
@NonNull
protected InvalidationTracker createInvalidationTracker() {
- final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
- final HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
- final HashSet<String> _tables = new HashSet<String>(1);
+ final Map<String, String> _shadowTablesMap = new HashMap<String, String>(0);
+ final Map<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
+ final Set<String> _tables = new HashSet<String>(1);
_tables.add("User");
_viewTables.put("usersummary", _tables);
- return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User","Child1","Child2");
+ return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User", "Child1", "Child2");
}
@Override
public void clearAllTables() {
- super.assertNotMainThread();
- final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
- try {
- super.beginTransaction();
- _db.execSQL("DELETE FROM `User`");
- _db.execSQL("DELETE FROM `Child1`");
- _db.execSQL("DELETE FROM `Child2`");
- super.setTransactionSuccessful();
- } finally {
- super.endTransaction();
- _db.query("PRAGMA wal_checkpoint(FULL)").close();
- if (!_db.inTransaction()) {
- _db.execSQL("VACUUM");
- }
- }
+ super.performClear(false, "User", "Child1", "Child2");
}
@Override
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
index 2364ea1..dacc6d7 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import javax.`annotation`.processing.Generated
import kotlin.Int
import kotlin.String
@@ -24,23 +21,17 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
_result = MyEntity(_tmpPk)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
index 4b29902..40d8ff1 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
@@ -1,11 +1,8 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.appendPlaceholders
import androidx.room.util.getColumnIndexOrThrow
import androidx.room.util.newStringBuilder
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import java.lang.StringBuilder
import javax.`annotation`.processing.Generated
import kotlin.Array
@@ -34,29 +31,22 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -67,33 +57,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- if (arg == null) {
- _statement.bindNull(_argIndex)
- } else {
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ if (arg == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
}
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -104,33 +87,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String? in arg) {
- if (_item == null) {
- _statement.bindNull(_argIndex)
- } else {
- _statement.bindString(_argIndex, _item)
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String? in arg) {
+ if (_item == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ _stmt.bindText(_argIndex, _item)
+ }
+ _argIndex++
}
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -141,29 +117,22 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -174,33 +143,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String? in arg) {
- if (_item == null) {
- _statement.bindNull(_argIndex)
- } else {
- _statement.bindString(_argIndex, _item)
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String? in arg) {
+ if (_item == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ _stmt.bindText(_argIndex, _item)
+ }
+ _argIndex++
}
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -211,29 +173,22 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: Int in arg) {
- _statement.bindLong(_argIndex, _item.toLong())
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: Int in arg) {
+ _stmt.bindLong(_argIndex, _item.toLong())
+ _argIndex++
+ }
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -244,33 +199,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- if (arg == null) {
- _statement.bindNull(_argIndex)
- } else {
- for (_item: Int in arg) {
- _statement.bindLong(_argIndex, _item.toLong())
- _argIndex++
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ if (arg == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ for (_item: Int in arg) {
+ _stmt.bindLong(_argIndex, _item.toLong())
+ _argIndex++
+ }
}
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+ val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpId: String
- _tmpId = _cursor.getString(_cursorIndexOfId)
+ _tmpId = _stmt.getText(_cursorIndexOfId)
_result = MyEntity(_tmpId)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
index 585fef6..c41869c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import javax.`annotation`.processing.Generated
import kotlin.Int
import kotlin.String
@@ -24,53 +21,41 @@
public override fun stringParam(arg: String): MyEntity {
val _sql: String = "SELECT * FROM MyEntity WHERE string = ?"
- val _statement: RoomSQLiteQuery = acquire(_sql, 1)
- var _argIndex: Int = 1
- _statement.bindString(_argIndex, arg)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ _stmt.bindText(_argIndex, arg)
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
public override fun nullableStringParam(arg: String?): MyEntity {
val _sql: String = "SELECT * FROM MyEntity WHERE string = ?"
- val _statement: RoomSQLiteQuery = acquire(_sql, 1)
- var _argIndex: Int = 1
- if (arg == null) {
- _statement.bindNull(_argIndex)
- } else {
- _statement.bindString(_argIndex, arg)
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ if (arg == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ _stmt.bindText(_argIndex, arg)
+ }
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
index 50fe3b7..73f170b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
@@ -1,11 +1,8 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.appendPlaceholders
import androidx.room.util.getColumnIndexOrThrow
import androidx.room.util.newStringBuilder
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import java.lang.StringBuilder
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -33,29 +30,22 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -66,33 +56,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- if (arg == null) {
- _statement.bindNull(_argIndex)
- } else {
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ if (arg == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
}
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -103,33 +86,26 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String? in arg) {
- if (_item == null) {
- _statement.bindNull(_argIndex)
- } else {
- _statement.bindString(_argIndex, _item)
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String? in arg) {
+ if (_item == null) {
+ _stmt.bindNull(_argIndex)
+ } else {
+ _stmt.bindText(_argIndex, _item)
+ }
+ _argIndex++
}
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
@@ -140,29 +116,22 @@
appendPlaceholders(_stringBuilder, _inputSize)
_stringBuilder.append(")")
val _sql: String = _stringBuilder.toString()
- val _argCount: Int = 0 + _inputSize
- val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
- var _argIndex: Int = 1
- for (_item: String in arg) {
- _statement.bindString(_argIndex, _item)
- _argIndex++
- }
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ for (_item: String in arg) {
+ _stmt.bindText(_argIndex, _item)
+ _argIndex++
+ }
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
_result = MyEntity(_tmpString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
index 18b355f..f0b1c65 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
@@ -6,10 +6,7 @@
import androidx.room.util.TableInfo.Companion.read
import androidx.room.util.dropFtsSyncTriggers
import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
import javax.`annotation`.processing.Generated
import kotlin.Any
import kotlin.Lazy
@@ -63,12 +60,11 @@
public override fun onValidateSchema(connection: SQLiteConnection):
RoomOpenDelegate.ValidationResult {
- val _columnsMyEntity: HashMap<String, TableInfo.Column> =
- HashMap<String, TableInfo.Column>(1)
+ val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
_columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
TableInfo.CREATED_FROM_ENTITY))
- val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
- val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+ val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+ val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
_indicesMyEntity)
val _existingMyEntity: TableInfo = read(connection, "MyEntity")
@@ -88,25 +84,13 @@
}
protected override fun createInvalidationTracker(): InvalidationTracker {
- val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
- val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+ val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
+ val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
}
public override fun clearAllTables() {
- super.assertNotMainThread()
- val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
- try {
- super.beginTransaction()
- _db.execSQL("DELETE FROM `MyEntity`")
- super.setTransactionSuccessful()
- } finally {
- super.endTransaction()
- _db.query("PRAGMA wal_checkpoint(FULL)").close()
- if (!_db.inTransaction()) {
- _db.execSQL("VACUUM")
- }
- }
+ super.performClear(false, "MyEntity")
}
protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
index df1e1ac..2b52aea 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
@@ -6,10 +6,7 @@
import androidx.room.util.TableInfo.Companion.read
import androidx.room.util.dropFtsSyncTriggers
import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
import javax.`annotation`.processing.Generated
import kotlin.Any
import kotlin.Lazy
@@ -63,12 +60,11 @@
public override fun onValidateSchema(connection: SQLiteConnection):
RoomOpenDelegate.ValidationResult {
- val _columnsMyEntity: HashMap<String, TableInfo.Column> =
- HashMap<String, TableInfo.Column>(1)
+ val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
_columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
TableInfo.CREATED_FROM_ENTITY))
- val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
- val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+ val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+ val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
_indicesMyEntity)
val _existingMyEntity: TableInfo = read(connection, "MyEntity")
@@ -88,25 +84,13 @@
}
protected override fun createInvalidationTracker(): InvalidationTracker {
- val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
- val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+ val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
+ val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
}
public override fun clearAllTables() {
- super.assertNotMainThread()
- val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
- try {
- super.beginTransaction()
- _db.execSQL("DELETE FROM `MyEntity`")
- super.setTransactionSuccessful()
- } finally {
- super.endTransaction()
- _db.query("PRAGMA wal_checkpoint(FULL)").close()
- if (!_db.inTransaction()) {
- _db.execSQL("VACUUM")
- }
- }
+ super.performClear(false, "MyEntity")
}
protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
index 2af2047..558d443 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
@@ -7,13 +7,9 @@
import androidx.room.util.ViewInfo
import androidx.room.util.dropFtsSyncTriggers
import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
import javax.`annotation`.processing.Generated
import kotlin.Any
-import kotlin.Boolean
import kotlin.Lazy
import kotlin.String
import kotlin.Suppress
@@ -76,13 +72,11 @@
public override fun onValidateSchema(connection: SQLiteConnection):
RoomOpenDelegate.ValidationResult {
- val _columnsMyParentEntity: HashMap<String, TableInfo.Column> =
- HashMap<String, TableInfo.Column>(1)
+ val _columnsMyParentEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
_columnsMyParentEntity.put("parentKey", TableInfo.Column("parentKey", "INTEGER", true, 1,
null, TableInfo.CREATED_FROM_ENTITY))
- val _foreignKeysMyParentEntity: HashSet<TableInfo.ForeignKey> =
- HashSet<TableInfo.ForeignKey>(0)
- val _indicesMyParentEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+ val _foreignKeysMyParentEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+ val _indicesMyParentEntity: MutableSet<TableInfo.Index> = mutableSetOf()
val _infoMyParentEntity: TableInfo = TableInfo("MyParentEntity", _columnsMyParentEntity,
_foreignKeysMyParentEntity, _indicesMyParentEntity)
val _existingMyParentEntity: TableInfo = tableInfoRead(connection, "MyParentEntity")
@@ -95,16 +89,15 @@
| Found:
|""".trimMargin() + _existingMyParentEntity)
}
- val _columnsMyEntity: HashMap<String, TableInfo.Column> =
- HashMap<String, TableInfo.Column>(2)
+ val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
_columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
TableInfo.CREATED_FROM_ENTITY))
_columnsMyEntity.put("indexedCol", TableInfo.Column("indexedCol", "TEXT", true, 0, null,
TableInfo.CREATED_FROM_ENTITY))
- val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(1)
+ val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
_foreignKeysMyEntity.add(TableInfo.ForeignKey("MyParentEntity", "CASCADE", "NO ACTION",
listOf("indexedCol"), listOf("parentKey")))
- val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(1)
+ val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
_indicesMyEntity.add(TableInfo.Index("index_MyEntity_indexedCol", false,
listOf("indexedCol"), listOf("ASC")))
val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
@@ -119,7 +112,7 @@
| Found:
|""".trimMargin() + _existingMyEntity)
}
- val _columnsMyFtsEntity: HashSet<String> = HashSet<String>(2)
+ val _columnsMyFtsEntity: MutableSet<String> = mutableSetOf()
_columnsMyFtsEntity.add("text")
val _infoMyFtsEntity: FtsTableInfo = FtsTableInfo("MyFtsEntity", _columnsMyFtsEntity,
"CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
@@ -152,43 +145,18 @@
}
protected override fun createInvalidationTracker(): InvalidationTracker {
- val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(1)
+ val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
_shadowTablesMap.put("MyFtsEntity", "MyFtsEntity_content")
- val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(1)
- val _tables: HashSet<String> = HashSet<String>(1)
+ val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
+ val _tables: MutableSet<String> = mutableSetOf()
_tables.add("MyFtsEntity")
_viewTables.put("myview", _tables)
- return InvalidationTracker(this, _shadowTablesMap, _viewTables,
- "MyParentEntity","MyEntity","MyFtsEntity")
+ return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyParentEntity", "MyEntity",
+ "MyFtsEntity")
}
public override fun clearAllTables() {
- super.assertNotMainThread()
- val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
- val _supportsDeferForeignKeys: Boolean = android.os.Build.VERSION.SDK_INT >=
- android.os.Build.VERSION_CODES.LOLLIPOP
- try {
- if (!_supportsDeferForeignKeys) {
- _db.execSQL("PRAGMA foreign_keys = FALSE")
- }
- super.beginTransaction()
- if (_supportsDeferForeignKeys) {
- _db.execSQL("PRAGMA defer_foreign_keys = TRUE")
- }
- _db.execSQL("DELETE FROM `MyParentEntity`")
- _db.execSQL("DELETE FROM `MyEntity`")
- _db.execSQL("DELETE FROM `MyFtsEntity`")
- super.setTransactionSuccessful()
- } finally {
- super.endTransaction()
- if (!_supportsDeferForeignKeys) {
- _db.execSQL("PRAGMA foreign_keys = TRUE")
- }
- _db.query("PRAGMA wal_checkpoint(FULL)").close()
- if (!_db.inTransaction()) {
- _db.execSQL("VACUUM")
- }
- }
+ super.performClear(true, "MyParentEntity", "MyEntity", "MyFtsEntity")
}
protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
index a6820c0..7932177 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.SharedSQLiteStatement
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -54,25 +51,19 @@
public override fun getEntity(id: Long): MyEntity {
val _sql: String = "SELECT * FROM MyEntity WHERE pk = ?"
- val _statement: RoomSQLiteQuery = acquire(_sql, 1)
- var _argIndex: Int = 1
- _statement.bindLong(_argIndex, id)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ var _argIndex: Int = 1
+ _stmt.bindLong(_argIndex, id)
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Long
- _tmpPk = _cursor.getLong(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk)
_result = MyEntity(_tmpPk)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
index b812ed3..7812585 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import javax.`annotation`.processing.Generated
import kotlin.Int
import kotlin.Long
@@ -25,23 +22,17 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Long
- _tmpPk = _cursor.getLong(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk)
_result = MyEntity(_tmpPk)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
index 61f845b..cc819dac 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Boolean
@@ -57,37 +54,31 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfBoolean: Int = getColumnIndexOrThrow(_cursor, "boolean")
- val _cursorIndexOfNullableBoolean: Int = getColumnIndexOrThrow(_cursor, "nullableBoolean")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfBoolean: Int = getColumnIndexOrThrow(_stmt, "boolean")
+ val _cursorIndexOfNullableBoolean: Int = getColumnIndexOrThrow(_stmt, "nullableBoolean")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpBoolean: Boolean
val _tmp: Int
- _tmp = _cursor.getInt(_cursorIndexOfBoolean)
+ _tmp = _stmt.getLong(_cursorIndexOfBoolean).toInt()
_tmpBoolean = _tmp != 0
val _tmpNullableBoolean: Boolean?
val _tmp_1: Int?
- if (_cursor.isNull(_cursorIndexOfNullableBoolean)) {
+ if (_stmt.isNull(_cursorIndexOfNullableBoolean)) {
_tmp_1 = null
} else {
- _tmp_1 = _cursor.getInt(_cursorIndexOfNullableBoolean)
+ _tmp_1 = _stmt.getLong(_cursorIndexOfNullableBoolean).toInt()
}
_tmpNullableBoolean = _tmp_1?.let { it != 0 }
_result = MyEntity(_tmpPk,_tmpBoolean,_tmpNullableBoolean)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
index e91082b..dac2cad 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.ByteArray
@@ -55,33 +52,27 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfByteArray: Int = getColumnIndexOrThrow(_cursor, "byteArray")
- val _cursorIndexOfNullableByteArray: Int = getColumnIndexOrThrow(_cursor, "nullableByteArray")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfByteArray: Int = getColumnIndexOrThrow(_stmt, "byteArray")
+ val _cursorIndexOfNullableByteArray: Int = getColumnIndexOrThrow(_stmt, "nullableByteArray")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpByteArray: ByteArray
- _tmpByteArray = _cursor.getBlob(_cursorIndexOfByteArray)
+ _tmpByteArray = _stmt.getBlob(_cursorIndexOfByteArray)
val _tmpNullableByteArray: ByteArray?
- if (_cursor.isNull(_cursorIndexOfNullableByteArray)) {
+ if (_stmt.isNull(_cursorIndexOfNullableByteArray)) {
_tmpNullableByteArray = null
} else {
- _tmpNullableByteArray = _cursor.getBlob(_cursorIndexOfNullableByteArray)
+ _tmpNullableByteArray = _stmt.getBlob(_cursorIndexOfNullableByteArray)
}
_result = MyEntity(_tmpPk,_tmpByteArray,_tmpNullableByteArray)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
index 563454a3..a5f784b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -51,28 +48,22 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpFoo: Foo
val _tmp: String
- _tmp = _cursor.getString(_cursorIndexOfFoo)
+ _tmp = _stmt.getText(_cursorIndexOfFoo)
_tmpFoo = __fooConverter.fromString(_tmp)
_result = MyEntity(_tmpPk,_tmpFoo)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
index 5859adc..1a6dc11 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -50,29 +47,23 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_stmt, "bar")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpBar: Bar
val _tmp: String
- _tmp = _cursor.getString(_cursorIndexOfBar)
+ _tmp = _stmt.getText(_cursorIndexOfBar)
val _tmp_1: Foo = FooBarConverter.fromString(_tmp)
_tmpBar = FooBarConverter.fromFoo(_tmp_1)
_result = MyEntity(_tmpPk,_tmpBar)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
index 69a1e7a..a5fdc7d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -51,28 +48,22 @@
internal override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpFoo: Foo
val _tmp: String
- _tmp = _cursor.getString(_cursorIndexOfFoo)
+ _tmp = _stmt.getText(_cursorIndexOfFoo)
_tmpFoo = __fooConverter.fromString(_tmp)
_result = MyEntity(_tmpPk,_tmpFoo)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
index 7eef3c0..c23234e 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -60,23 +57,20 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
- val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
+ val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_stmt, "bar")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpFoo: Foo
val _tmp: String?
- if (_cursor.isNull(_cursorIndexOfFoo)) {
+ if (_stmt.isNull(_cursorIndexOfFoo)) {
_tmp = null
} else {
- _tmp = _cursor.getString(_cursorIndexOfFoo)
+ _tmp = _stmt.getText(_cursorIndexOfFoo)
}
val _tmp_1: Foo? = FooBarConverter.fromString(_tmp)
if (_tmp_1 == null) {
@@ -86,10 +80,10 @@
}
val _tmpBar: Bar
val _tmp_2: String?
- if (_cursor.isNull(_cursorIndexOfBar)) {
+ if (_stmt.isNull(_cursorIndexOfBar)) {
_tmp_2 = null
} else {
- _tmp_2 = _cursor.getString(_cursorIndexOfBar)
+ _tmp_2 = _stmt.getText(_cursorIndexOfBar)
}
val _tmp_3: Foo? = FooBarConverter.fromString(_tmp_2)
val _tmp_4: Bar?
@@ -107,10 +101,7 @@
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
index e3f3b20..e10ea35 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -55,28 +52,22 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpFoo: Foo
val _tmp: String
- _tmp = _cursor.getString(_cursorIndexOfFoo)
+ _tmp = _stmt.getText(_cursorIndexOfFoo)
_tmpFoo = __fooConverter().fromString(_tmp)
_result = MyEntity(_tmpPk,_tmpFoo)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
index c76f721..98b4588 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -59,32 +56,29 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfNumberData: Int = getColumnIndexOrThrow(_cursor, "numberData")
- val _cursorIndexOfStringData: Int = getColumnIndexOrThrow(_cursor, "stringData")
- val _cursorIndexOfNumberData_1: Int = getColumnIndexOrThrow(_cursor, "nullablenumberData")
- val _cursorIndexOfStringData_1: Int = getColumnIndexOrThrow(_cursor, "nullablestringData")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfNumberData: Int = getColumnIndexOrThrow(_stmt, "numberData")
+ val _cursorIndexOfStringData: Int = getColumnIndexOrThrow(_stmt, "stringData")
+ val _cursorIndexOfNumberData_1: Int = getColumnIndexOrThrow(_stmt, "nullablenumberData")
+ val _cursorIndexOfStringData_1: Int = getColumnIndexOrThrow(_stmt, "nullablestringData")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpFoo: Foo
val _tmpNumberData: Long
- _tmpNumberData = _cursor.getLong(_cursorIndexOfNumberData)
+ _tmpNumberData = _stmt.getLong(_cursorIndexOfNumberData)
val _tmpStringData: String
- _tmpStringData = _cursor.getString(_cursorIndexOfStringData)
+ _tmpStringData = _stmt.getText(_cursorIndexOfStringData)
_tmpFoo = Foo(_tmpNumberData,_tmpStringData)
val _tmpNullableFoo: Foo?
- if (!(_cursor.isNull(_cursorIndexOfNumberData_1) &&
- _cursor.isNull(_cursorIndexOfStringData_1))) {
+ if (!(_stmt.isNull(_cursorIndexOfNumberData_1) &&
+ _stmt.isNull(_cursorIndexOfStringData_1))) {
val _tmpNumberData_1: Long
- _tmpNumberData_1 = _cursor.getLong(_cursorIndexOfNumberData_1)
+ _tmpNumberData_1 = _stmt.getLong(_cursorIndexOfNumberData_1)
val _tmpStringData_1: String
- _tmpStringData_1 = _cursor.getString(_cursorIndexOfStringData_1)
+ _tmpStringData_1 = _stmt.getText(_cursorIndexOfStringData_1)
_tmpNullableFoo = Foo(_tmpNumberData_1,_tmpStringData_1)
} else {
_tmpNullableFoo = null
@@ -93,10 +87,7 @@
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
index cbb1148..c83cdf9 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import java.lang.IllegalArgumentException
import javax.`annotation`.processing.Generated
@@ -55,33 +52,27 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfEnum: Int = getColumnIndexOrThrow(_cursor, "enum")
- val _cursorIndexOfNullableEnum: Int = getColumnIndexOrThrow(_cursor, "nullableEnum")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfEnum: Int = getColumnIndexOrThrow(_stmt, "enum")
+ val _cursorIndexOfNullableEnum: Int = getColumnIndexOrThrow(_stmt, "nullableEnum")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpEnum: Fruit
- _tmpEnum = __Fruit_stringToEnum(_cursor.getString(_cursorIndexOfEnum))
+ _tmpEnum = __Fruit_stringToEnum(_stmt.getText(_cursorIndexOfEnum))
val _tmpNullableEnum: Fruit?
- if (_cursor.isNull(_cursorIndexOfNullableEnum)) {
+ if (_stmt.isNull(_cursorIndexOfNullableEnum)) {
_tmpNullableEnum = null
} else {
- _tmpNullableEnum = __Fruit_stringToEnum(_cursor.getString(_cursorIndexOfNullableEnum))
+ _tmpNullableEnum = __Fruit_stringToEnum(_stmt.getText(_cursorIndexOfNullableEnum))
}
_result = MyEntity(_tmpPk,_tmpEnum,_tmpNullableEnum)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
index 9ada137..fa0b92ae 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -51,30 +48,24 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfInternalVal: Int = getColumnIndexOrThrow(_cursor, "internalVal")
- val _cursorIndexOfInternalVar: Int = getColumnIndexOrThrow(_cursor, "internalVar")
- val _cursorIndexOfInternalSetterVar: Int = getColumnIndexOrThrow(_cursor, "internalSetterVar")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfInternalVal: Int = getColumnIndexOrThrow(_stmt, "internalVal")
+ val _cursorIndexOfInternalVar: Int = getColumnIndexOrThrow(_stmt, "internalVar")
+ val _cursorIndexOfInternalSetterVar: Int = getColumnIndexOrThrow(_stmt, "internalSetterVar")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpInternalVal: Long
- _tmpInternalVal = _cursor.getLong(_cursorIndexOfInternalVal)
+ _tmpInternalVal = _stmt.getLong(_cursorIndexOfInternalVal)
_result = MyEntity(_tmpPk,_tmpInternalVal)
- _result.internalVar = _cursor.getLong(_cursorIndexOfInternalVar)
- _result.internalSetterVar = _cursor.getLong(_cursorIndexOfInternalSetterVar)
+ _result.internalVar = _stmt.getLong(_cursorIndexOfInternalVar)
+ _result.internalSetterVar = _stmt.getLong(_cursorIndexOfInternalSetterVar)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
index 92db9a1..41882d5 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -78,70 +75,64 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfPrimitive: Int = getColumnIndexOrThrow(_cursor, "primitive")
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
- val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_cursor, "nullableString")
- val _cursorIndexOfFieldString: Int = getColumnIndexOrThrow(_cursor, "fieldString")
- val _cursorIndexOfNullableFieldString: Int = getColumnIndexOrThrow(_cursor,
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfPrimitive: Int = getColumnIndexOrThrow(_stmt, "primitive")
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
+ val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_stmt, "nullableString")
+ val _cursorIndexOfFieldString: Int = getColumnIndexOrThrow(_stmt, "fieldString")
+ val _cursorIndexOfNullableFieldString: Int = getColumnIndexOrThrow(_stmt,
"nullableFieldString")
- val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
- val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
- val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+ val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_stmt, "variablePrimitive")
+ val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_stmt, "variableString")
+ val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_stmt,
"variableNullableString")
- val _cursorIndexOfVariableFieldString: Int = getColumnIndexOrThrow(_cursor,
+ val _cursorIndexOfVariableFieldString: Int = getColumnIndexOrThrow(_stmt,
"variableFieldString")
- val _cursorIndexOfVariableNullableFieldString: Int = getColumnIndexOrThrow(_cursor,
+ val _cursorIndexOfVariableNullableFieldString: Int = getColumnIndexOrThrow(_stmt,
"variableNullableFieldString")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpPrimitive: Long
- _tmpPrimitive = _cursor.getLong(_cursorIndexOfPrimitive)
+ _tmpPrimitive = _stmt.getLong(_cursorIndexOfPrimitive)
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
val _tmpNullableString: String?
- if (_cursor.isNull(_cursorIndexOfNullableString)) {
+ if (_stmt.isNull(_cursorIndexOfNullableString)) {
_tmpNullableString = null
} else {
- _tmpNullableString = _cursor.getString(_cursorIndexOfNullableString)
+ _tmpNullableString = _stmt.getText(_cursorIndexOfNullableString)
}
val _tmpFieldString: String
- _tmpFieldString = _cursor.getString(_cursorIndexOfFieldString)
+ _tmpFieldString = _stmt.getText(_cursorIndexOfFieldString)
val _tmpNullableFieldString: String?
- if (_cursor.isNull(_cursorIndexOfNullableFieldString)) {
+ if (_stmt.isNull(_cursorIndexOfNullableFieldString)) {
_tmpNullableFieldString = null
} else {
- _tmpNullableFieldString = _cursor.getString(_cursorIndexOfNullableFieldString)
+ _tmpNullableFieldString = _stmt.getText(_cursorIndexOfNullableFieldString)
}
_result =
MyEntity(_tmpPk,_tmpPrimitive,_tmpString,_tmpNullableString,_tmpFieldString,_tmpNullableFieldString)
- _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
- _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
- if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+ _result.variablePrimitive = _stmt.getLong(_cursorIndexOfVariablePrimitive)
+ _result.variableString = _stmt.getText(_cursorIndexOfVariableString)
+ if (_stmt.isNull(_cursorIndexOfVariableNullableString)) {
_result.variableNullableString = null
} else {
- _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+ _result.variableNullableString = _stmt.getText(_cursorIndexOfVariableNullableString)
}
- _result.variableFieldString = _cursor.getString(_cursorIndexOfVariableFieldString)
- if (_cursor.isNull(_cursorIndexOfVariableNullableFieldString)) {
+ _result.variableFieldString = _stmt.getText(_cursorIndexOfVariableFieldString)
+ if (_stmt.isNull(_cursorIndexOfVariableNullableFieldString)) {
_result.variableNullableFieldString = null
} else {
_result.variableNullableFieldString =
- _cursor.getString(_cursorIndexOfVariableNullableFieldString)
+ _stmt.getText(_cursorIndexOfVariableNullableFieldString)
}
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
index c68ee4e..d965ec0 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Byte
@@ -59,41 +56,35 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_cursor, "int")
- val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_cursor, "short")
- val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_cursor, "byte")
- val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_cursor, "long")
- val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_cursor, "char")
- val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_cursor, "float")
- val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_cursor, "double")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_stmt, "int")
+ val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_stmt, "short")
+ val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_stmt, "byte")
+ val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_stmt, "long")
+ val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_stmt, "char")
+ val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_stmt, "float")
+ val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_stmt, "double")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpInt: Int
- _tmpInt = _cursor.getInt(_cursorIndexOfInt)
+ _tmpInt = _stmt.getLong(_cursorIndexOfInt).toInt()
val _tmpShort: Short
- _tmpShort = _cursor.getShort(_cursorIndexOfShort)
+ _tmpShort = _stmt.getLong(_cursorIndexOfShort).toShort()
val _tmpByte: Byte
- _tmpByte = _cursor.getShort(_cursorIndexOfByte).toByte()
+ _tmpByte = _stmt.getLong(_cursorIndexOfByte).toByte()
val _tmpLong: Long
- _tmpLong = _cursor.getLong(_cursorIndexOfLong)
+ _tmpLong = _stmt.getLong(_cursorIndexOfLong)
val _tmpChar: Char
- _tmpChar = _cursor.getInt(_cursorIndexOfChar).toChar()
+ _tmpChar = _stmt.getLong(_cursorIndexOfChar).toChar()
val _tmpFloat: Float
- _tmpFloat = _cursor.getFloat(_cursorIndexOfFloat)
+ _tmpFloat = _stmt.getDouble(_cursorIndexOfFloat).toFloat()
val _tmpDouble: Double
- _tmpDouble = _cursor.getDouble(_cursorIndexOfDouble)
+ _tmpDouble = _stmt.getDouble(_cursorIndexOfDouble)
_result = MyEntity(_tmpInt,_tmpShort,_tmpByte,_tmpLong,_tmpChar,_tmpFloat,_tmpDouble)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
index d30d498..a870118 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Byte
@@ -94,69 +91,63 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_cursor, "int")
- val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_cursor, "short")
- val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_cursor, "byte")
- val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_cursor, "long")
- val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_cursor, "char")
- val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_cursor, "float")
- val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_cursor, "double")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_stmt, "int")
+ val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_stmt, "short")
+ val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_stmt, "byte")
+ val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_stmt, "long")
+ val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_stmt, "char")
+ val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_stmt, "float")
+ val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_stmt, "double")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpInt: Int?
- if (_cursor.isNull(_cursorIndexOfInt)) {
+ if (_stmt.isNull(_cursorIndexOfInt)) {
_tmpInt = null
} else {
- _tmpInt = _cursor.getInt(_cursorIndexOfInt)
+ _tmpInt = _stmt.getLong(_cursorIndexOfInt).toInt()
}
val _tmpShort: Short?
- if (_cursor.isNull(_cursorIndexOfShort)) {
+ if (_stmt.isNull(_cursorIndexOfShort)) {
_tmpShort = null
} else {
- _tmpShort = _cursor.getShort(_cursorIndexOfShort)
+ _tmpShort = _stmt.getLong(_cursorIndexOfShort).toShort()
}
val _tmpByte: Byte?
- if (_cursor.isNull(_cursorIndexOfByte)) {
+ if (_stmt.isNull(_cursorIndexOfByte)) {
_tmpByte = null
} else {
- _tmpByte = _cursor.getShort(_cursorIndexOfByte).toByte()
+ _tmpByte = _stmt.getLong(_cursorIndexOfByte).toByte()
}
val _tmpLong: Long?
- if (_cursor.isNull(_cursorIndexOfLong)) {
+ if (_stmt.isNull(_cursorIndexOfLong)) {
_tmpLong = null
} else {
- _tmpLong = _cursor.getLong(_cursorIndexOfLong)
+ _tmpLong = _stmt.getLong(_cursorIndexOfLong)
}
val _tmpChar: Char?
- if (_cursor.isNull(_cursorIndexOfChar)) {
+ if (_stmt.isNull(_cursorIndexOfChar)) {
_tmpChar = null
} else {
- _tmpChar = _cursor.getInt(_cursorIndexOfChar).toChar()
+ _tmpChar = _stmt.getLong(_cursorIndexOfChar).toChar()
}
val _tmpFloat: Float?
- if (_cursor.isNull(_cursorIndexOfFloat)) {
+ if (_stmt.isNull(_cursorIndexOfFloat)) {
_tmpFloat = null
} else {
- _tmpFloat = _cursor.getFloat(_cursorIndexOfFloat)
+ _tmpFloat = _stmt.getDouble(_cursorIndexOfFloat).toFloat()
}
val _tmpDouble: Double?
- if (_cursor.isNull(_cursorIndexOfDouble)) {
+ if (_stmt.isNull(_cursorIndexOfDouble)) {
_tmpDouble = null
} else {
- _tmpDouble = _cursor.getDouble(_cursorIndexOfDouble)
+ _tmpDouble = _stmt.getDouble(_cursorIndexOfDouble)
}
_result = MyEntity(_tmpInt,_tmpShort,_tmpByte,_tmpLong,_tmpChar,_tmpFloat,_tmpDouble)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
index 6a4aa8b..848194c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -53,30 +50,24 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
- val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_cursor, "nullableString")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
+ val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_stmt, "nullableString")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpString: String
- _tmpString = _cursor.getString(_cursorIndexOfString)
+ _tmpString = _stmt.getText(_cursorIndexOfString)
val _tmpNullableString: String?
- if (_cursor.isNull(_cursorIndexOfNullableString)) {
+ if (_stmt.isNull(_cursorIndexOfNullableString)) {
_tmpNullableString = null
} else {
- _tmpNullableString = _cursor.getString(_cursorIndexOfNullableString)
+ _tmpNullableString = _stmt.getText(_cursorIndexOfNullableString)
}
_result = MyEntity(_tmpString,_tmpNullableString)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
index 04e8b45..5f0966c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
@@ -1,12 +1,9 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.convertByteToUUID
import androidx.room.util.convertUUIDToByte
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.UUID
import javax.`annotation`.processing.Generated
@@ -57,33 +54,27 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfUuid: Int = getColumnIndexOrThrow(_cursor, "uuid")
- val _cursorIndexOfNullableUuid: Int = getColumnIndexOrThrow(_cursor, "nullableUuid")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfUuid: Int = getColumnIndexOrThrow(_stmt, "uuid")
+ val _cursorIndexOfNullableUuid: Int = getColumnIndexOrThrow(_stmt, "nullableUuid")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
val _tmpUuid: UUID
- _tmpUuid = convertByteToUUID(_cursor.getBlob(_cursorIndexOfUuid))
+ _tmpUuid = convertByteToUUID(_stmt.getBlob(_cursorIndexOfUuid))
val _tmpNullableUuid: UUID?
- if (_cursor.isNull(_cursorIndexOfNullableUuid)) {
+ if (_stmt.isNull(_cursorIndexOfNullableUuid)) {
_tmpNullableUuid = null
} else {
- _tmpNullableUuid = convertByteToUUID(_cursor.getBlob(_cursorIndexOfNullableUuid))
+ _tmpNullableUuid = convertByteToUUID(_stmt.getBlob(_cursorIndexOfNullableUuid))
}
_result = MyEntity(_tmpPk,_tmpUuid,_tmpNullableUuid)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
index 817dc88..12f7417 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
@@ -1,12 +1,9 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.convertByteToUUID
import androidx.room.util.convertUUIDToByte
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.UUID
import javax.`annotation`.processing.Generated
@@ -80,60 +77,54 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfUuidData: Int = getColumnIndexOrThrow(_cursor, "uuidData")
- val _cursorIndexOfNullableUuidData: Int = getColumnIndexOrThrow(_cursor, "nullableUuidData")
- val _cursorIndexOfNullableLongData: Int = getColumnIndexOrThrow(_cursor, "nullableLongData")
- val _cursorIndexOfDoubleNullableLongData: Int = getColumnIndexOrThrow(_cursor,
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfUuidData: Int = getColumnIndexOrThrow(_stmt, "uuidData")
+ val _cursorIndexOfNullableUuidData: Int = getColumnIndexOrThrow(_stmt, "nullableUuidData")
+ val _cursorIndexOfNullableLongData: Int = getColumnIndexOrThrow(_stmt, "nullableLongData")
+ val _cursorIndexOfDoubleNullableLongData: Int = getColumnIndexOrThrow(_stmt,
"doubleNullableLongData")
- val _cursorIndexOfGenericData: Int = getColumnIndexOrThrow(_cursor, "genericData")
+ val _cursorIndexOfGenericData: Int = getColumnIndexOrThrow(_stmt, "genericData")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: LongValueClass
val _data: Long
- _data = _cursor.getLong(_cursorIndexOfPk)
+ _data = _stmt.getLong(_cursorIndexOfPk)
_tmpPk = LongValueClass(_data)
val _tmpUuidData: UUIDValueClass
val _data_1: UUID
- _data_1 = convertByteToUUID(_cursor.getBlob(_cursorIndexOfUuidData))
+ _data_1 = convertByteToUUID(_stmt.getBlob(_cursorIndexOfUuidData))
_tmpUuidData = UUIDValueClass(_data_1)
val _tmpNullableUuidData: UUIDValueClass?
- if (_cursor.isNull(_cursorIndexOfNullableUuidData)) {
+ if (_stmt.isNull(_cursorIndexOfNullableUuidData)) {
_tmpNullableUuidData = null
} else {
val _data_2: UUID
- _data_2 = convertByteToUUID(_cursor.getBlob(_cursorIndexOfNullableUuidData))
+ _data_2 = convertByteToUUID(_stmt.getBlob(_cursorIndexOfNullableUuidData))
_tmpNullableUuidData = UUIDValueClass(_data_2)
}
val _tmpNullableLongData: NullableLongValueClass
val _data_3: Long
- _data_3 = _cursor.getLong(_cursorIndexOfNullableLongData)
+ _data_3 = _stmt.getLong(_cursorIndexOfNullableLongData)
_tmpNullableLongData = NullableLongValueClass(_data_3)
val _tmpDoubleNullableLongData: NullableLongValueClass?
- if (_cursor.isNull(_cursorIndexOfDoubleNullableLongData)) {
+ if (_stmt.isNull(_cursorIndexOfDoubleNullableLongData)) {
_tmpDoubleNullableLongData = null
} else {
val _data_4: Long
- _data_4 = _cursor.getLong(_cursorIndexOfDoubleNullableLongData)
+ _data_4 = _stmt.getLong(_cursorIndexOfDoubleNullableLongData)
_tmpDoubleNullableLongData = NullableLongValueClass(_data_4)
}
val _tmpGenericData: GenericValueClass<String>
val _password: String
- _password = _cursor.getString(_cursorIndexOfGenericData)
+ _password = _stmt.getText(_cursorIndexOfGenericData)
_tmpGenericData = GenericValueClass<String>(_password)
_result =
MyEntity(_tmpPk,_tmpUuidData,_tmpNullableUuidData,_tmpNullableLongData,_tmpDoubleNullableLongData,_tmpGenericData)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index b625f23..dc8ddc9 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -55,34 +52,28 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
- val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
- val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
- val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+ val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_stmt, "variablePrimitive")
+ val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_stmt, "variableString")
+ val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_stmt,
"variableNullableString")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
val _tmpPk: Int
- _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
_result = MyEntity(_tmpPk)
- _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
- _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
- if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+ _result.variablePrimitive = _stmt.getLong(_cursorIndexOfVariablePrimitive)
+ _result.variableString = _stmt.getText(_cursorIndexOfVariableString)
+ if (_stmt.isNull(_cursorIndexOfVariableNullableString)) {
_result.variableNullableString = null
} else {
- _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+ _result.variableNullableString = _stmt.getText(_cursorIndexOfVariableNullableString)
}
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
index 500ed9e..c111064 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
import androidx.room.EntityInsertionAdapter
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
import androidx.sqlite.db.SupportSQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
@@ -54,32 +51,26 @@
public override fun getEntity(): MyEntity {
val _sql: String = "SELECT * FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, _statement, false, null)
- try {
- val _cursorIndexOfMValue: Int = getColumnIndexOrThrow(_cursor, "mValue")
- val _cursorIndexOfMNullableValue: Int = getColumnIndexOrThrow(_cursor, "mNullableValue")
+ return performReadBlocking(__db, _sql) { _stmt ->
+ val _cursorIndexOfMValue: Int = getColumnIndexOrThrow(_stmt, "mValue")
+ val _cursorIndexOfMNullableValue: Int = getColumnIndexOrThrow(_stmt, "mNullableValue")
val _result: MyEntity
- if (_cursor.moveToFirst()) {
+ if (_stmt.step()) {
_result = MyEntity()
val _tmpMValue: Long
- _tmpMValue = _cursor.getLong(_cursorIndexOfMValue)
+ _tmpMValue = _stmt.getLong(_cursorIndexOfMValue)
_result.setValue(_tmpMValue)
val _tmpMNullableValue: String?
- if (_cursor.isNull(_cursorIndexOfMNullableValue)) {
+ if (_stmt.isNull(_cursorIndexOfMNullableValue)) {
_tmpMNullableValue = null
} else {
- _tmpMNullableValue = _cursor.getString(_cursorIndexOfMNullableValue)
+ _tmpMNullableValue = _stmt.getText(_cursorIndexOfMNullableValue)
}
_result.setNullableValue(_tmpMNullableValue)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
- } finally {
- _cursor.close()
- _statement.release()
+ _result
}
}
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..c105ab0
--- /dev/null
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+DefaultValueChange: androidx.room.util.TableInfo#TableInfo(String, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column>, java.util.Set<androidx.room.util.TableInfo.ForeignKey>, java.util.Set<androidx.room.util.TableInfo.Index>) parameter #3:
+ Attempted to remove default value from parameter indices in androidx.room.util.TableInfo
+
+
+RemovedMethod: androidx.room.util.FtsTableInfo#parseOptions(String):
+ Removed method androidx.room.util.FtsTableInfo.parseOptions(String)
+RemovedMethod: androidx.room.util.FtsTableInfo.Companion#parseOptions(String):
+ Removed method androidx.room.util.FtsTableInfo.Companion.parseOptions(String)
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index ae0bc5e..54202fc 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -150,6 +150,7 @@
method public boolean inTransaction();
method @CallSuper public void init(androidx.room.DatabaseConfiguration configuration);
method @Deprecated protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase db);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) protected final void internalInitInvalidationTracker(androidx.sqlite.SQLiteConnection connection);
method public boolean isOpen();
method public android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery query);
method public android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery query, optional android.os.CancellationSignal? signal);
@@ -398,6 +399,8 @@
method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void dropFtsSyncTriggers(androidx.sqlite.db.SupportSQLiteDatabase db);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void dropFtsSyncTriggers(androidx.sqlite.SQLiteConnection connection);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void foreignKeyCheck(androidx.sqlite.db.SupportSQLiteDatabase db, String tableName);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <R> R performReadBlocking(androidx.room.RoomDatabase db, String sql, kotlin.jvm.functions.Function1<? super androidx.sqlite.SQLiteStatement,? extends R> block);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <R> R performReadTransactionBlocking(androidx.room.RoomDatabase db, String sql, kotlin.jvm.functions.Function1<? super androidx.sqlite.SQLiteStatement,? extends R> block);
method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy, android.os.CancellationSignal? signal);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static int readVersion(java.io.File databaseFile) throws java.io.IOException;
@@ -410,7 +413,6 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FtsTableInfo {
ctor public FtsTableInfo(String name, java.util.Set<java.lang.String> columns, String createSql);
ctor public FtsTableInfo(String name, java.util.Set<java.lang.String> columns, java.util.Set<java.lang.String> options);
- method @VisibleForTesting public static java.util.Set<java.lang.String> parseOptions(String createStatement);
method public static androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
method public static androidx.room.util.FtsTableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
field public static final androidx.room.util.FtsTableInfo.Companion Companion;
@@ -420,7 +422,6 @@
}
public static final class FtsTableInfo.Companion {
- method @VisibleForTesting public java.util.Set<java.lang.String> parseOptions(String createStatement);
method public androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
method public androidx.room.util.FtsTableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
}
@@ -431,6 +432,10 @@
method public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
}
+ public final class SQLiteStatementUtil {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int getColumnIndexOrThrow(androidx.sqlite.SQLiteStatement stmt, String name);
+ }
+
@RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class StringUtil {
method public static void appendPlaceholders(StringBuilder builder, int count);
method public static String? joinIntoString(java.util.List<java.lang.Integer>? input);
@@ -440,9 +445,9 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TableInfo {
- ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys);
- ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys, optional java.util.Set<androidx.room.util.TableInfo.Index>? indices);
- method public static androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
+ ctor @Deprecated public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys);
+ ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys, java.util.Set<androidx.room.util.TableInfo.Index>? indices);
+ method @Deprecated public static androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
method public static androidx.room.util.TableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
field public static final int CREATED_FROM_DATABASE = 2; // 0x2
field public static final int CREATED_FROM_ENTITY = 1; // 0x1
@@ -454,10 +459,10 @@
field public final String name;
}
- public static final class TableInfo.Column {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class TableInfo.Column {
ctor @Deprecated public TableInfo.Column(String name, String type, boolean notNull, int primaryKeyPosition);
ctor public TableInfo.Column(String name, String type, boolean notNull, int primaryKeyPosition, String? defaultValue, int createdFrom);
- method @VisibleForTesting public static boolean defaultValueEquals(String current, String? other);
+ method public static boolean defaultValueEquals(String current, String? other);
method public boolean isPrimaryKey();
property public final boolean isPrimaryKey;
field public static final androidx.room.util.TableInfo.Column.Companion Companion;
@@ -471,11 +476,11 @@
}
public static final class TableInfo.Column.Companion {
- method @VisibleForTesting public boolean defaultValueEquals(String current, String? other);
+ method public boolean defaultValueEquals(String current, String? other);
}
public static final class TableInfo.Companion {
- method public androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
+ method @Deprecated public androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
method public androidx.room.util.TableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
}
@@ -509,7 +514,7 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ViewInfo {
ctor public ViewInfo(String name, String? sql);
- method public static androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
+ method @Deprecated public static androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
method public static androidx.room.util.ViewInfo read(androidx.sqlite.SQLiteConnection connection, String viewName);
field public static final androidx.room.util.ViewInfo.Companion Companion;
field public final String name;
@@ -517,7 +522,7 @@
}
public static final class ViewInfo.Companion {
- method public androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
+ method @Deprecated public androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
method public androidx.room.util.ViewInfo read(androidx.sqlite.SQLiteConnection connection, String viewName);
}
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
index 439997c..1793dc7 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
@@ -16,6 +16,7 @@
package androidx.room.migration;
+import static androidx.room.util.TableInfo.Column.defaultValueEquals;
import static com.google.common.truth.Truth.assertThat;
@@ -458,7 +459,7 @@
assertThat("((0) + (1 + 2))")
.isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
- assertThat(TableInfo.Column.defaultValueEquals(
+ assertThat(defaultValueEquals(
"((0) + (1 + 2))",
Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
}
@@ -472,7 +473,7 @@
assertThat("(((0) + (1 + 2)))")
.isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
- assertThat(TableInfo.Column.defaultValueEquals(
+ assertThat(defaultValueEquals(
"(((0) + (1 + 2)))",
Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
}
@@ -486,7 +487,7 @@
assertThat("(((3 + 5) + (2 + 1)) + (1 + 2))")
.isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
- assertThat(TableInfo.Column.defaultValueEquals(
+ assertThat(defaultValueEquals(
"(((3 + 5) + (2 + 1)) + (1 + 2))",
Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
}
@@ -500,7 +501,7 @@
assertThat("( (0) + (1 + 2))")
.isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
- assertThat(TableInfo.Column.defaultValueEquals(
+ assertThat(defaultValueEquals(
"( (0) + (1 + 2))",
Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
}
@@ -514,7 +515,7 @@
assertThat("((0) + (1 + 2) )")
.isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
- assertThat(TableInfo.Column.defaultValueEquals(
+ assertThat(defaultValueEquals(
"((0) + (1 + 2) )",
Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index 6b18661..c1192db 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -26,7 +26,9 @@
import androidx.arch.core.internal.SafeIterableMap
import androidx.lifecycle.LiveData
import androidx.room.Room.LOG_TAG
+import androidx.room.driver.SupportSQLiteConnection
import androidx.room.util.useCursor
+import androidx.sqlite.SQLiteConnection
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteStatement
@@ -35,8 +37,8 @@
import java.util.concurrent.atomic.AtomicBoolean
/**
- * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
- * these tables.
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
*/
// Some details on how the InvalidationTracker works:
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
@@ -49,7 +51,9 @@
// memory table table, flipping the invalidated flag ON.
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
// It works as an Observer, and notifies other instances of table invalidation.
-open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
+actual open class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
internal val database: RoomDatabase,
private val shadowTablesMap: Map<String, String>,
private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
@@ -136,9 +140,20 @@
/**
* Internal method to initialize table tracking.
- *
- * You should never call this method, it is called by the generated code.
*/
+ internal actual fun internalInit(connection: SQLiteConnection) {
+ if (connection is SupportSQLiteConnection) {
+ @Suppress("DEPRECATION")
+ internalInit(connection.db)
+ } else {
+ TODO("Not yet migrated to use SQLiteDriver - b/309990302")
+ }
+ }
+
+ /**
+ * Internal method to initialize table tracking.
+ */
+ @Deprecated("No longer called by generated code")
internal fun internalInit(database: SupportSQLiteDatabase) {
synchronized(trackerLock) {
if (initialized) {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
index 3261f20..da3cdc4 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
@@ -25,6 +25,7 @@
import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.use
/**
* An Android platform specific [RoomConnectionManager] with backwards compatibility with
@@ -243,7 +244,7 @@
private var currentTransactionType: Transactor.SQLiteTransactionType? = null
override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
- return block.invoke(delegate.prepare(sql))
+ return delegate.prepare(sql).use { block.invoke(it) }
}
// TODO(b/318767291): Add coroutine confinement like RoomDatabase.withTransaction
@@ -268,7 +269,9 @@
Transactor.SQLiteTransactionType.EXCLUSIVE -> db.beginTransaction()
}
try {
- return SupportTransactor<R>().block()
+ val result = SupportTransactor<R>().block()
+ db.setTransactionSuccessful()
+ return result
} catch (rollback: RollbackException) {
@Suppress("UNCHECKED_CAST")
return rollback.result as R
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 6ad9a08..df6336b 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -22,6 +22,7 @@
import android.content.Context
import android.content.Intent
import android.database.Cursor
+import android.os.Build
import android.os.CancellationSignal
import android.os.Looper
import android.util.Log
@@ -38,6 +39,7 @@
import androidx.room.util.findMigrationPath as findMigrationPathExt
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
@@ -129,7 +131,8 @@
*
* @return The invalidation tracker for the database.
*/
- open val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+ actual open val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
private var allowMainThreadQueries = false
@JvmField
@@ -362,13 +365,14 @@
}
/**
- * Called when the RoomDatabase is created.
+ * Creates the invalidation tracker
*
- * This is already implemented by the generated code.
+ * An implementation of this function is generated by the Room processor. Note that this method
+ * is called when the [RoomDatabase] is initialized.
*
- * @return Creates a new InvalidationTracker.
+ * @return A new invalidation tracker.
*/
- protected abstract fun createInvalidationTracker(): InvalidationTracker
+ protected actual abstract fun createInvalidationTracker(): InvalidationTracker
/**
* Returns a Map of String -> List<Class> where each entry has the `key` as the DAO name
@@ -446,6 +450,46 @@
abstract fun clearAllTables()
/**
+ * Performs a 'clear all tables' operation.
+ *
+ * This should only be invoked from generated code.
+ *
+ * @see [RoomDatabase.clearAllTables]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ protected fun performClear(hasForeignKeys: Boolean, vararg tableNames: String) {
+ assertNotMainThread()
+ assertNotSuspendingTransaction()
+ runBlocking {
+ connectionManager.useConnection(isReadOnly = false) { connection ->
+ val supportsDeferForeignKeys =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ if (hasForeignKeys && !supportsDeferForeignKeys) {
+ connection.execSQL("PRAGMA foreign_keys = FALSE")
+ }
+ // TODO(b/309990302): Commonize Invalidation Tracker
+ invalidationTracker.syncTriggers(openHelper.writableDatabase)
+ connection.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
+ if (hasForeignKeys && supportsDeferForeignKeys) {
+ execSQL("PRAGMA defer_foreign_keys = TRUE")
+ }
+ tableNames.forEach { tableName ->
+ execSQL("DELETE FROM `$tableName`")
+ }
+ }
+ if (hasForeignKeys && !supportsDeferForeignKeys) {
+ connection.execSQL("PRAGMA foreign_keys = TRUE")
+ }
+ if (!connection.inTransaction()) {
+ connection.execSQL("PRAGMA wal_checkpoint(FULL)")
+ connection.execSQL("VACUUM")
+ invalidationTracker.refreshVersionsAsync()
+ }
+ }
+ }
+ }
+
+ /**
* True if database connection is open and initialized.
*
* When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
@@ -512,6 +556,46 @@
}
}
+ /**
+ * Performs a database operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> perform(
+ isReadOnly: Boolean,
+ sql: String,
+ block: (SQLiteStatement) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { connection ->
+ connection.usePrepared(sql, block)
+ }
+ }
+
+ /**
+ * Performs a transactional database operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> performTransaction(
+ isReadOnly: Boolean,
+ block: suspend (TransactionScope<R>) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { transactor ->
+ val type = if (isReadOnly) {
+ Transactor.SQLiteTransactionType.DEFERRED
+ } else {
+ Transactor.SQLiteTransactionType.IMMEDIATE
+ }
+ // TODO(b/309990302): Commonize Invalidation Tracker
+ if (!isReadOnly) {
+ invalidationTracker.syncTriggers(openHelper.writableDatabase)
+ }
+ val result = transactor.withTransaction(type, block)
+ if (!isReadOnly && !transactor.inTransaction()) {
+ invalidationTracker.refreshVersionsAsync()
+ }
+ result
+ }
+ }
+
// Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
// methods we are using and also helps unit tests to mock this class without mocking
// all SQLite database methods.
@@ -669,32 +753,25 @@
}
/**
- * Called by the generated code when database is open.
- *
- * You should never call this method manually.
+ * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+ * initialized and opens a database connection.
*
* @param db The database instance.
*/
@Deprecated("No longer called by generated")
protected open fun internalInitInvalidationTracker(db: SupportSQLiteDatabase) {
- invalidationTracker.internalInit(db)
+ internalInitInvalidationTracker(SupportSQLiteConnection(db))
}
/**
- * Called by the generated code when database is open.
- *
- * You should never call this method manually.
+ * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+ * initialized and opens a database connection.
*
* @param connection The database connection.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- protected open fun internalInitInvalidationTracker(connection: SQLiteConnection) {
- if (connection is SupportSQLiteConnection) {
- @Suppress("DEPRECATION")
- internalInitInvalidationTracker(connection.db)
- } else {
- TODO("Not yet migrated to use SQLiteDriver")
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+ invalidationTracker.internalInit(connection)
}
/**
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
index 412b983..b3ec4a1 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
@@ -137,47 +137,22 @@
override fun getColumnCount(): Int {
throwIfClosed()
+ ensureCursor()
return cursor?.columnCount ?: 0
}
override fun getColumnName(index: Int): String {
throwIfClosed()
- val c = throwIfNoRow()
+ ensureCursor()
+ val c = checkNotNull(cursor)
throwIfInvalidColumn(c, index)
return c.getColumnName(index)
}
override fun step(): Boolean {
throwIfClosed()
- if (cursor == null) {
- cursor = db.query(
- object : SupportSQLiteQuery {
- override val sql: String
- get() = this@SupportAndroidSQLiteStatement.sql
-
- override fun bindTo(statement: SupportSQLiteProgram) {
- for (index in 1 until bindingTypes.size) {
- when (bindingTypes[index]) {
- COLUMN_TYPE_LONG ->
- statement.bindLong(index, longBindings[index])
- COLUMN_TYPE_DOUBLE ->
- statement.bindDouble(index, doubleBindings[index])
- COLUMN_TYPE_STRING ->
- statement.bindString(index, stringBindings[index]!!)
- COLUMN_TYPE_BLOB ->
- statement.bindBlob(index, blobBindings[index]!!)
- COLUMN_TYPE_NULL ->
- statement.bindNull(index)
- }
- }
- }
-
- override val argCount: Int
- get() = bindingTypes.size
- }
- )
- }
- return requireNotNull(cursor).moveToNext()
+ ensureCursor()
+ return checkNotNull(cursor).moveToNext()
}
override fun reset() {
@@ -197,6 +172,7 @@
override fun close() {
if (!isClosed) {
+ clearBindings()
reset()
}
isClosed = true
@@ -231,6 +207,37 @@
}
}
+ private fun ensureCursor() {
+ if (cursor == null) {
+ cursor = db.query(
+ object : SupportSQLiteQuery {
+ override val sql: String
+ get() = this@SupportAndroidSQLiteStatement.sql
+
+ override fun bindTo(statement: SupportSQLiteProgram) {
+ for (index in 1 until bindingTypes.size) {
+ when (bindingTypes[index]) {
+ COLUMN_TYPE_LONG ->
+ statement.bindLong(index, longBindings[index])
+ COLUMN_TYPE_DOUBLE ->
+ statement.bindDouble(index, doubleBindings[index])
+ COLUMN_TYPE_STRING ->
+ statement.bindString(index, stringBindings[index]!!)
+ COLUMN_TYPE_BLOB ->
+ statement.bindBlob(index, blobBindings[index]!!)
+ COLUMN_TYPE_NULL ->
+ statement.bindNull(index)
+ }
+ }
+ }
+
+ override val argCount: Int
+ get() = bindingTypes.size
+ }
+ )
+ }
+ }
+
private fun throwIfNoRow(): Cursor {
return cursor ?: throwSQLiteException(ResultCode.SQLITE_MISUSE, "no row")
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
index 1361804..d4c0a75 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
@@ -27,12 +27,42 @@
import androidx.annotation.RestrictTo
import androidx.room.RoomDatabase
import androidx.room.driver.SupportSQLiteConnection
+import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Performs a single database read query operation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <R> performReadBlocking(
+ db: RoomDatabase,
+ sql: String,
+ block: (SQLiteStatement) -> R
+): R {
+ db.assertNotMainThread()
+ db.assertNotSuspendingTransaction()
+ return runBlocking { db.perform(isReadOnly = true, sql, block) }
+}
+
+/**
+ * Performs a single database read query transaction operation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <R> performReadTransactionBlocking(
+ db: RoomDatabase,
+ sql: String,
+ block: (SQLiteStatement) -> R
+): R {
+ db.assertNotMainThread()
+ db.assertNotSuspendingTransaction()
+ return runBlocking { db.performTransaction(isReadOnly = true) { it.usePrepared(sql, block) } }
+}
/**
* Performs the SQLiteQuery on the given database.
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
index 89efc38..55d3ce3 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
@@ -16,66 +16,45 @@
package androidx.room.util
import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
import androidx.room.driver.SupportSQLiteConnection
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.db.SupportSQLiteDatabase
-import java.util.ArrayDeque
/**
* A data class that holds the information about an FTS table.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-class FtsTableInfo(
+actual class FtsTableInfo(
/**
* The table name
*/
@JvmField
- val name: String,
+ actual val name: String,
/**
* The column names
*/
@JvmField
- val columns: Set<String>,
+ actual val columns: Set<String>,
/**
* The set of options. Each value in the set contains the option in the following format:
* <key, value>.
*/
@JvmField
- val options: Set<String>
+ actual val options: Set<String>
) {
- constructor(name: String, columns: Set<String>, createSql: String) :
- this(name, columns, parseOptions(createSql))
+ actual constructor(name: String, columns: Set<String>, createSql: String) :
+ this(name, columns, parseFtsOptions(createSql))
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is FtsTableInfo) return false
- val that = other
- if (name != that.name) return false
- if (columns != that.columns) return false
- return options == that.options
- }
+ override fun equals(other: Any?) = equalsCommon(other)
- override fun hashCode(): Int {
- var result = name.hashCode()
- result = 31 * result + (columns.hashCode())
- result = 31 * result + (options.hashCode())
- return result
- }
+ override fun hashCode() = hashCodeCommon()
- override fun toString(): String {
- return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
- }
+ override fun toString() = toStringCommon()
- companion object {
- // A set of valid FTS Options
- private val FTS_OPTIONS = arrayOf(
- "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
- "order=", "prefix=", "uncompress="
- )
+ actual companion object {
/**
* Reads the table information from the given database.
@@ -86,107 +65,21 @@
*/
@JvmStatic
fun read(database: SupportSQLiteDatabase, tableName: String): FtsTableInfo {
- val columns = readColumns(database, tableName)
- val options = readOptions(database, tableName)
- return FtsTableInfo(tableName, columns, options)
- }
-
- @JvmStatic
- fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
- if (connection is SupportSQLiteConnection) {
- return read(connection.db, tableName)
- } else {
- TODO("Not yet migrated to use SQLiteDriver")
- }
- }
-
- private fun readColumns(database: SupportSQLiteDatabase, tableName: String): Set<String> {
- return buildSet {
- database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
- if (cursor.columnCount > 0) {
- val nameIndex = cursor.getColumnIndex("name")
- while (cursor.moveToNext()) {
- add(cursor.getString(nameIndex))
- }
- }
- }
- }
- }
-
- private fun readOptions(database: SupportSQLiteDatabase, tableName: String): Set<String> {
- val sql = database.query(
- "SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
- ).useCursor { cursor ->
- if (cursor.moveToFirst()) {
- cursor.getString(cursor.getColumnIndexOrThrow("sql"))
- } else {
- ""
- }
- }
- return parseOptions(sql)
+ return read(SupportSQLiteConnection(database), tableName)
}
/**
- * Parses FTS options from the create statement of an FTS table.
+ * Reads the table information from the given database.
*
- * This method assumes the given create statement is a valid well-formed SQLite statement as
- * defined in the [CREATE VIRTUAL TABLE
- * syntax diagram](https://www.sqlite.org/lang_createvtab.html).
- *
- * @param createStatement the "CREATE VIRTUAL TABLE" statement.
- * @return the set of FTS option key and values in the create statement.
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A FtsTableInfo containing the columns and options for the provided table name.
*/
- @VisibleForTesting
@JvmStatic
- fun parseOptions(createStatement: String): Set<String> {
- if (createStatement.isEmpty()) {
- return emptySet()
- }
-
- // Module arguments are within the parenthesis followed by the module name.
- val argsString = createStatement.substring(
- createStatement.indexOf('(') + 1,
- createStatement.lastIndexOf(')')
- )
-
- // Split the module argument string by the comma delimiter, keeping track of quotation
- // so that if the delimiter is found within a string literal we don't substring at the
- // wrong index. SQLite supports four ways of quoting keywords, see:
- // https://www.sqlite.org/lang_keywords.html
- val args = mutableListOf<String>()
- val quoteStack = ArrayDeque<Char>()
- var lastDelimiterIndex = -1
- argsString.forEachIndexed { i, value ->
- when (value) {
- '\'', '"', '`' ->
- if (quoteStack.isEmpty()) {
- quoteStack.push(value)
- } else if (quoteStack.peek() == value) {
- quoteStack.pop()
- }
- '[' -> if (quoteStack.isEmpty()) {
- quoteStack.push(value)
- }
- ']' -> if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
- quoteStack.pop()
- }
- ',' -> if (quoteStack.isEmpty()) {
- args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
- lastDelimiterIndex = i
- }
- }
- }
-
- // Add final argument.
- args.add(argsString.substring(lastDelimiterIndex + 1).trim())
-
- // Match args against valid options, otherwise they are column definitions.
- val options = args.filter { arg ->
- FTS_OPTIONS.any { validOption ->
- arg.startsWith(validOption)
- }
- }.toSet()
- return options
+ actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+ val columns = readFtsColumns(connection, tableName)
+ val options = readFtsOptions(connection, tableName)
+ return FtsTableInfo(tableName, columns, options)
}
}
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
new file mode 100644
index 0000000..6b4d5d2
--- /dev/null
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import android.os.Build
+import androidx.sqlite.SQLiteStatement
+
+/**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ *
+ * The implementation also contains Android-specific patches to workaround issues on older devices.
+ */
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int {
+ var index = this.columnIndexOf(name)
+ if (index >= 0) {
+ return index
+ }
+ index = this.columnIndexOf("`$name`")
+ return if (index >= 0) {
+ index
+ } else {
+ this.findColumnIndexBySuffix(name)
+ }
+}
+
+/**
+ * Finds a column by name by appending `.` in front of it and checking by suffix match. Also checks
+ * for the version wrapped with `` (backticks)m a workaround for b/157261134 for API levels 25 and
+ * below e.g. "foo" will match "any.foo" and "`any.foo`".
+ */
+private fun SQLiteStatement.findColumnIndexBySuffix(name: String): Int {
+ // This workaround is only on APIs < 26. So just return not found on newer APIs
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1 || name.isEmpty()) {
+ return -1
+ }
+ val columnCount = getColumnCount()
+ val dotSuffix = ".$name"
+ val backtickSuffix = ".$name`"
+ for (i in 0 until columnCount) {
+ val columnName = getColumnName(i)
+ // Do not check if column name is not long enough. 1 char for table name, 1 char for '.'
+ if (columnName.length >= name.length + 2) {
+ if (columnName.endsWith(dotSuffix)) {
+ return i
+ } else if (columnName[0] == '`' && columnName.endsWith(backtickSuffix)) {
+ return i
+ }
+ }
+ }
+ return -1
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
index 4e6ddf6..e77a2b8 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
@@ -15,18 +15,13 @@
*/
package androidx.room.util
-import android.database.Cursor
import android.os.Build
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
-import androidx.room.ColumnInfo
import androidx.room.ColumnInfo.SQLiteTypeAffinity
import androidx.room.driver.SupportSQLiteConnection
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.db.SupportSQLiteDatabase
-import java.util.Locale
-import java.util.TreeMap
/**
* A data class that holds the information about a table.
@@ -36,22 +31,20 @@
* documentation for more details.
*
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-// if you change this class, you must change TableInfoValidationWriter.kt
-class TableInfo(
+actual class TableInfo actual constructor(
/**
* The table name.
*/
@JvmField
- val name: String,
+ actual val name: String,
@JvmField
- val columns: Map<String, Column>,
+ actual val columns: Map<String, Column>,
@JvmField
- val foreignKeys: Set<ForeignKey>,
+ actual val foreignKeys: Set<ForeignKey>,
@JvmField
- val indices: Set<Index>? = null
+ actual val indices: Set<Index>?
) {
/**
* Identifies from where the info object was created.
@@ -63,60 +56,36 @@
/**
* For backward compatibility with dbs created with older versions.
*/
- @SuppressWarnings("unused")
+ @Deprecated("No longer used by generated code.")
constructor(
name: String,
columns: Map<String, Column>,
foreignKeys: Set<ForeignKey>
) : this(name, columns, foreignKeys, emptySet<Index>())
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is TableInfo) return false
- if (name != other.name) return false
- if (columns != other.columns) {
- return false
- }
- if (foreignKeys != other.foreignKeys) {
- return false
- }
- return if (indices == null || other.indices == null) {
- // if one us is missing index information, seems like we couldn't acquire the
- // information so we better skip.
- true
- } else indices == other.indices
- }
+ actual override fun equals(other: Any?) = equalsCommon(other)
- override fun hashCode(): Int {
- var result = name.hashCode()
- result = 31 * result + columns.hashCode()
- result = 31 * result + foreignKeys.hashCode()
- // skip index, it is not reliable for comparison.
- return result
- }
+ actual override fun hashCode() = hashCodeCommon()
- override fun toString(): String {
- return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
- "indices=$indices}")
- }
+ actual override fun toString() = toStringCommon()
- companion object {
+ actual companion object {
/**
* Identifier for when the info is created from an unknown source.
*/
- const val CREATED_FROM_UNKNOWN = 0
+ actual const val CREATED_FROM_UNKNOWN = 0
/**
* Identifier for when the info is created from an entity definition, such as generated code
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
*/
- const val CREATED_FROM_ENTITY = 1
+ actual const val CREATED_FROM_ENTITY = 1
/**
* Identifier for when the info is created from the database itself, reading information
* from a PRAGMA, such as table_info.
*/
- const val CREATED_FROM_DATABASE = 2
+ actual const val CREATED_FROM_DATABASE = 2
/**
* Reads the table information from the given database.
@@ -125,63 +94,72 @@
* @param tableName The table name.
* @return A TableInfo containing the schema information for the provided table name.
*/
+ @Deprecated("No longer used by generated code.")
@JvmStatic
fun read(database: SupportSQLiteDatabase, tableName: String): TableInfo {
- return readTableInfo(
- database = database,
- tableName = tableName
- )
+ return read(SupportSQLiteConnection(database), tableName)
}
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
@JvmStatic
- fun read(connection: SQLiteConnection, tableName: String): TableInfo {
- if (connection is SupportSQLiteConnection) {
- return read(connection.db, tableName)
- } else {
- TODO("Not yet migrated to use SQLiteDriver")
- }
+ actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+ return readTableInfo(connection, tableName)
}
}
/**
* Holds the information about a database column.
*/
- class Column(
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class Column actual constructor(
/**
* The column name.
*/
@JvmField
- val name: String,
+ actual val name: String,
/**
* The column type affinity.
*/
@JvmField
- val type: String,
+ actual val type: String,
/**
* Whether or not the column can be NULL.
*/
@JvmField
- val notNull: Boolean,
+ actual val notNull: Boolean,
@JvmField
- val primaryKeyPosition: Int,
+ actual val primaryKeyPosition: Int,
@JvmField
- val defaultValue: String?,
+ actual val defaultValue: String?,
@CreatedFrom
@JvmField
- val createdFrom: Int
+ actual val createdFrom: Int
) {
/**
* The column type after it is normalized to one of the basic types according to
* https://www.sqlite.org/datatype3.html Section 3.1.
*
- *
* This is the value Room uses for equality check.
*/
@SQLiteTypeAffinity
@JvmField
- val affinity: Int = findAffinity(type)
+ actual val affinity: Int = findAffinity(type)
- @Deprecated("Use {@link Column#Column(String, String, boolean, int, String, int)} instead.")
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ actual val isPrimaryKey: Boolean
+ get() = primaryKeyPosition > 0
+
+ @Deprecated("No longer used by generated code.")
constructor(name: String, type: String, notNull: Boolean, primaryKeyPosition: Int) : this(
name,
type,
@@ -191,230 +169,55 @@
CREATED_FROM_UNKNOWN
)
- /**
- * Implements https://www.sqlite.org/datatype3.html section 3.1
- *
- * @param type The type that was given to the sqlite
- * @return The normalized type which is one of the 5 known affinities
- */
- @SQLiteTypeAffinity
- private fun findAffinity(type: String?): Int {
- if (type == null) {
- return ColumnInfo.BLOB
- }
- val uppercaseType = type.uppercase(Locale.US)
- if (uppercaseType.contains("INT")) {
- return ColumnInfo.INTEGER
- }
- if (uppercaseType.contains("CHAR") ||
- uppercaseType.contains("CLOB") ||
- uppercaseType.contains("TEXT")
- ) {
- return ColumnInfo.TEXT
- }
- if (uppercaseType.contains("BLOB")) {
- return ColumnInfo.BLOB
- }
- if (uppercaseType.contains("REAL") ||
- uppercaseType.contains("FLOA") ||
- uppercaseType.contains("DOUB")
- ) {
- return ColumnInfo.REAL
- }
- // sqlite returns NUMERIC here but it is like a catch all. We already
- // have UNDEFINED so it is better to use UNDEFINED for consistency.
- return ColumnInfo.UNDEFINED
- }
-
companion object {
- /**
- * Checks if the default values provided match. Handles the special case in which the
- * default value is surrounded by parenthesis (e.g. encountered in b/182284899).
- *
- * Surrounding parenthesis are removed by SQLite when reading from the database, hence
- * this function will check if they are present in the actual value, if so, it will
- * compare the two values by ignoring the surrounding parenthesis.
- *
- */
- @VisibleForTesting
@JvmStatic
- fun defaultValueEquals(current: String, other: String?): Boolean {
- if (current == other) {
- return true
- } else if (containsSurroundingParenthesis(current)) {
- return current.substring(1, current.length - 1).trim() == other
- }
- return false
- }
-
- /**
- * Checks for potential surrounding parenthesis, if found, removes them and checks if
- * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
- * and returns true.
- */
- private fun containsSurroundingParenthesis(current: String): Boolean {
- if (current.isEmpty()) {
- return false
- }
- var surroundingParenthesis = 0
- current.forEachIndexed { i, c ->
- if (i == 0 && c != '(') {
- return false
- }
- if (c == '(') {
- surroundingParenthesis++
- } else if (c == ')') {
- surroundingParenthesis--
- if (surroundingParenthesis == 0 && i != current.length - 1) {
- return false
- }
- }
- }
- return surroundingParenthesis == 0
- }
+ fun defaultValueEquals(current: String, other: String?) =
+ defaultValueEqualsCommon(current, other)
}
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is Column) return false
- if (Build.VERSION.SDK_INT >= 20) {
- if (primaryKeyPosition != other.primaryKeyPosition) return false
- } else {
- if (isPrimaryKey != other.isPrimaryKey) return false
- }
- if (name != other.name) return false
- if (notNull != other.notNull) return false
- // Only validate default value if it was defined in an entity, i.e. if the info
- // from the compiler itself has it. b/136019383
- if (
- createdFrom == CREATED_FROM_ENTITY &&
- other.createdFrom == CREATED_FROM_DATABASE &&
- defaultValue != null &&
- !defaultValueEquals(defaultValue, other.defaultValue)
- ) {
- return false
- } else if (
- createdFrom == CREATED_FROM_DATABASE &&
- other.createdFrom == CREATED_FROM_ENTITY &&
- other.defaultValue != null &&
- !defaultValueEquals(other.defaultValue, defaultValue)
- ) {
- return false
- } else if (
- createdFrom != CREATED_FROM_UNKNOWN &&
- createdFrom == other.createdFrom &&
- (if (defaultValue != null)
- !defaultValueEquals(defaultValue, other.defaultValue)
- else other.defaultValue != null)
- ) {
- return false
- }
- return affinity == other.affinity
- }
+ actual override fun equals(other: Any?) = equalsCommon(other)
- /**
- * Returns whether this column is part of the primary key or not.
- *
- * @return True if this column is part of the primary key, false otherwise.
- */
- val isPrimaryKey: Boolean
- get() = primaryKeyPosition > 0
+ actual override fun hashCode() = hashCodeCommon()
- override fun hashCode(): Int {
- var result = name.hashCode()
- result = 31 * result + affinity
- result = 31 * result + if (notNull) 1231 else 1237
- result = 31 * result + primaryKeyPosition
- // Default value is not part of the hashcode since we conditionally check it for
- // equality which would break the equals + hashcode contract.
- // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
- return result
- }
-
- override fun toString(): String {
- return ("Column{name='$name', type='$type', affinity='$affinity', " +
- "notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
- "defaultValue='${defaultValue ?: "undefined"}'}")
- }
+ actual override fun toString() = toStringCommon()
}
/**
* Holds the information about an SQLite foreign key
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- class ForeignKey(
+ actual class ForeignKey actual constructor(
@JvmField
- val referenceTable: String,
+ actual val referenceTable: String,
@JvmField
- val onDelete: String,
+ actual val onDelete: String,
@JvmField
- val onUpdate: String,
+ actual val onUpdate: String,
@JvmField
- val columnNames: List<String>,
+ actual val columnNames: List<String>,
@JvmField
- val referenceColumnNames: List<String>
+ actual val referenceColumnNames: List<String>
) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ForeignKey) return false
- if (referenceTable != other.referenceTable) return false
- if (onDelete != other.onDelete) return false
- if (onUpdate != other.onUpdate) return false
- return if (columnNames != other.columnNames) false else referenceColumnNames ==
- other.referenceColumnNames
- }
+ actual override fun equals(other: Any?) = equalsCommon(other)
- override fun hashCode(): Int {
- var result = referenceTable.hashCode()
- result = 31 * result + onDelete.hashCode()
- result = 31 * result + onUpdate.hashCode()
- result = 31 * result + columnNames.hashCode()
- result = 31 * result + referenceColumnNames.hashCode()
- return result
- }
+ actual override fun hashCode() = hashCodeCommon()
- override fun toString(): String {
- return ("ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
- "onUpdate='$onUpdate', columnNames=$columnNames, " +
- "referenceColumnNames=$referenceColumnNames}")
- }
- }
-
- /**
- * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
- * sorting in the generated foreign key object.
- */
- internal class ForeignKeyWithSequence(
- val id: Int,
- val sequence: Int,
- val from: String,
- val to: String
- ) : Comparable<ForeignKeyWithSequence> {
- override fun compareTo(other: ForeignKeyWithSequence): Int {
- val idCmp = id - other.id
- return if (idCmp == 0) {
- sequence - other.sequence
- } else {
- idCmp
- }
- }
+ actual override fun toString() = toStringCommon()
}
/**
* Holds the information about an SQLite index
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- class Index(
+ actual class Index actual constructor(
@JvmField
- val name: String,
+ actual val name: String,
@JvmField
- val unique: Boolean,
+ actual val unique: Boolean,
@JvmField
- val columns: List<String>,
+ actual val columns: List<String>,
@JvmField
- var orders: List<String>
+ actual var orders: List<String>
) {
init {
orders = orders.ifEmpty {
@@ -422,12 +225,12 @@
}
}
- companion object {
+ actual companion object {
// should match the value in Index.kt
- const val DEFAULT_PREFIX = "index_"
+ actual const val DEFAULT_PREFIX = "index_"
}
- @Deprecated("Use {@link #Index(String, boolean, List, List)}")
+ @Deprecated("No longer used by generated code.")
constructor(name: String, unique: Boolean, columns: List<String>) : this(
name,
unique,
@@ -435,221 +238,22 @@
List<String>(columns.size) { androidx.room.Index.Order.ASC.name }
)
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is Index) return false
- if (unique != other.unique) {
- return false
- }
- if (columns != other.columns) {
- return false
- }
- if (orders != other.orders) {
- return false
- }
- return if (name.startsWith(DEFAULT_PREFIX)) {
- other.name.startsWith(DEFAULT_PREFIX)
- } else {
- name == other.name
- }
- }
+ actual override fun equals(other: Any?) = equalsCommon(other)
- override fun hashCode(): Int {
- var result = if (name.startsWith(DEFAULT_PREFIX)) {
- DEFAULT_PREFIX.hashCode()
- } else {
- name.hashCode()
- }
- result = 31 * result + if (unique) 1 else 0
- result = 31 * result + columns.hashCode()
- result = 31 * result + orders.hashCode()
- return result
- }
+ actual override fun hashCode() = hashCodeCommon()
- override fun toString(): String {
- return ("Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}")
- }
- }
-}
-
-internal fun readTableInfo(database: SupportSQLiteDatabase, tableName: String): TableInfo {
- val columns = readColumns(database, tableName)
- val foreignKeys = readForeignKeys(database, tableName)
- val indices = readIndices(database, tableName)
- return TableInfo(tableName, columns, foreignKeys, indices)
-}
-
-private fun readForeignKeys(
- database: SupportSQLiteDatabase,
- tableName: String
-): Set<TableInfo.ForeignKey> {
- // this seems to return everything in order but it is not documented so better be safe
- database.query("PRAGMA foreign_key_list(`$tableName`)").useCursor { cursor ->
- val idColumnIndex = cursor.getColumnIndex("id")
- val seqColumnIndex = cursor.getColumnIndex("seq")
- val tableColumnIndex = cursor.getColumnIndex("table")
- val onDeleteColumnIndex = cursor.getColumnIndex("on_delete")
- val onUpdateColumnIndex = cursor.getColumnIndex("on_update")
- val ordered = readForeignKeyFieldMappings(cursor)
-
- // Reset cursor as readForeignKeyFieldMappings has moved it
- cursor.moveToPosition(-1)
- return buildSet {
- while (cursor.moveToNext()) {
- val seq = cursor.getInt(seqColumnIndex)
- if (seq != 0) {
- continue
- }
- val id = cursor.getInt(idColumnIndex)
- val myColumns = mutableListOf<String>()
- val refColumns = mutableListOf<String>()
-
- ordered.filter {
- it.id == id
- }.forEach { key ->
- myColumns.add(key.from)
- refColumns.add(key.to)
- }
-
- add(
- TableInfo.ForeignKey(
- referenceTable = cursor.getString(tableColumnIndex),
- onDelete = cursor.getString(onDeleteColumnIndex),
- onUpdate = cursor.getString(onUpdateColumnIndex),
- columnNames = myColumns,
- referenceColumnNames = refColumns
- )
- )
- }
- }
- }
-}
-
-private fun readForeignKeyFieldMappings(cursor: Cursor): List<TableInfo.ForeignKeyWithSequence> {
- val idColumnIndex = cursor.getColumnIndex("id")
- val seqColumnIndex = cursor.getColumnIndex("seq")
- val fromColumnIndex = cursor.getColumnIndex("from")
- val toColumnIndex = cursor.getColumnIndex("to")
-
- return buildList {
- while (cursor.moveToNext()) {
- add(
- TableInfo.ForeignKeyWithSequence(
- id = cursor.getInt(idColumnIndex),
- sequence = cursor.getInt(seqColumnIndex),
- from = cursor.getString(fromColumnIndex),
- to = cursor.getString(toColumnIndex)
- )
- )
- }
- }.sorted()
-}
-
-private fun readColumns(
- database: SupportSQLiteDatabase,
- tableName: String
-): Map<String, TableInfo.Column> {
- database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
- if (cursor.columnCount <= 0) {
- return emptyMap()
- }
-
- val nameIndex = cursor.getColumnIndex("name")
- val typeIndex = cursor.getColumnIndex("type")
- val notNullIndex = cursor.getColumnIndex("notnull")
- val pkIndex = cursor.getColumnIndex("pk")
- val defaultValueIndex = cursor.getColumnIndex("dflt_value")
-
- return buildMap {
- while (cursor.moveToNext()) {
- val name = cursor.getString(nameIndex)
- val type = cursor.getString(typeIndex)
- val notNull = 0 != cursor.getInt(notNullIndex)
- val primaryKeyPosition = cursor.getInt(pkIndex)
- val defaultValue = cursor.getString(defaultValueIndex)
- put(
- key = name,
- value = TableInfo.Column(
- name = name,
- type = type,
- notNull = notNull,
- primaryKeyPosition = primaryKeyPosition,
- defaultValue = defaultValue,
- createdFrom = TableInfo.CREATED_FROM_DATABASE
- )
- )
- }
- }
+ actual override fun toString() = toStringCommon()
}
}
/**
- * @return null if we cannot read the indices due to older sqlite implementations.
+ * Checks if the primary key match.
*/
-private fun readIndices(database: SupportSQLiteDatabase, tableName: String): Set<TableInfo.Index>? {
- database.query("PRAGMA index_list(`$tableName`)").useCursor { cursor ->
- val nameColumnIndex = cursor.getColumnIndex("name")
- val originColumnIndex = cursor.getColumnIndex("origin")
- val uniqueIndex = cursor.getColumnIndex("unique")
- if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
- // we cannot read them so better not validate any index.
- return null
- }
- return buildSet {
- while (cursor.moveToNext()) {
- val origin = cursor.getString(originColumnIndex)
- if ("c" != origin) {
- // Ignore auto-created indices
- continue
- }
- val name = cursor.getString(nameColumnIndex)
- val unique = cursor.getInt(uniqueIndex) == 1
- // Read index but if we cannot read it properly so better not read it
- val index = readIndex(database, name, unique) ?: return null
- add(index)
- }
- }
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+ if (Build.VERSION.SDK_INT >= 20) {
+ if (primaryKeyPosition != other.primaryKeyPosition) return false
+ } else {
+ if (isPrimaryKey != other.isPrimaryKey) return false
}
-}
-
-/**
- * @return null if we cannot read the index due to older sqlite implementations.
- */
-private fun readIndex(
- database: SupportSQLiteDatabase,
- name: String,
- unique: Boolean
-): TableInfo.Index? {
- return database.query("PRAGMA index_xinfo(`$name`)").useCursor { cursor ->
- val seqnoColumnIndex = cursor.getColumnIndex("seqno")
- val cidColumnIndex = cursor.getColumnIndex("cid")
- val nameColumnIndex = cursor.getColumnIndex("name")
- val descColumnIndex = cursor.getColumnIndex("desc")
- if (
- seqnoColumnIndex == -1 ||
- cidColumnIndex == -1 ||
- nameColumnIndex == -1 ||
- descColumnIndex == -1
- ) {
- // we cannot read them so better not validate any index.
- return null
- }
- val columnsMap = TreeMap<Int, String>()
- val ordersMap = TreeMap<Int, String>()
- while (cursor.moveToNext()) {
- val cid = cursor.getInt(cidColumnIndex)
- if (cid < 0) {
- // Ignore SQLite row ID
- continue
- }
- val seq = cursor.getInt(seqnoColumnIndex)
- val columnName = cursor.getString(nameColumnIndex)
- val order = if (cursor.getInt(descColumnIndex) > 0) "DESC" else "ASC"
- columnsMap[seq] = columnName
- ordersMap[seq] = order
- }
- val columns = columnsMap.values.toList()
- val orders = ordersMap.values.toList()
- TableInfo.Index(name, unique, columns, orders)
- }
+ return true
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
index 4b5d1a8..18a7f71 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
@@ -23,43 +23,30 @@
/**
* A data class that holds the information about a view.
*
- *
* This derives information from sqlite_master.
*
- *
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-class ViewInfo(
+actual class ViewInfo actual constructor(
/**
* The view name
*/
@JvmField
- val name: String,
+ actual val name: String,
/**
* The SQL of CREATE VIEW.
*/
@JvmField
- val sql: String?
+ actual val sql: String?
) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ViewInfo) return false
- return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
- }
+ actual override fun equals(other: Any?) = equalsCommon(other)
- override fun hashCode(): Int {
- var result = name.hashCode()
- result = 31 * result + (sql?.hashCode() ?: 0)
- return result
- }
+ actual override fun hashCode() = hashCodeCommon()
- override fun toString(): String {
- return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}')
- }
+ actual override fun toString() = toStringCommon()
- companion object {
+ actual companion object {
/**
* Reads the view information from the given database.
*
@@ -67,27 +54,22 @@
* @param viewName The view name.
* @return A ViewInfo containing the schema information for the provided view name.
*/
+ @Deprecated("No longer used by generated code.")
@JvmStatic
fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo {
- return database.query(
- "SELECT name, sql FROM sqlite_master " +
- "WHERE type = 'view' AND name = '$viewName'"
- ).useCursor { cursor ->
- if (cursor.moveToFirst()) {
- ViewInfo(cursor.getString(0), cursor.getString(1))
- } else {
- ViewInfo(viewName, null)
- }
- }
+ return read(SupportSQLiteConnection(database), viewName)
}
+ /**
+ * Reads the view information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param viewName The view name.
+ * @return A ViewInfo containing the schema information for the provided view name.
+ */
@JvmStatic
- fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
- if (connection is SupportSQLiteConnection) {
- return read(connection.db, viewName)
- } else {
- TODO("Not yet migrated to use SQLiteDriver")
- }
+ actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+ return readViewInfo(connection, viewName)
}
}
}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index ac18a3e..317eccd 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -96,6 +96,7 @@
mRoomDatabase, shadowTables, viewTables,
"a", "B", "i", "C", "d"
)
+ @Suppress("DEPRECATION")
mTracker.internalInit(mSqliteDb)
reset(mSqliteDb)
}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java
deleted file mode 100644
index f7c264d..0000000
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.util;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.internal.util.collections.Sets;
-
-import java.util.Set;
-
-@RunWith(JUnit4.class)
-public class FtsTableInfoTest {
-
- @Test
- public void test_parseOptions() {
- assertOptions("CREATE VIRTUAL TABLE Book USING FTS4()");
-
- assertOptions("CREATE VIRTUAL TABLE Book USING FTS4( )");
-
- assertOptions("CREATE VIRTUAL TABLE Book USING fts4(author)");
-
- assertOptions("CREATE VIRTUAL TABLE Book USING FTS4(author, matchinfo=fts3)",
- "matchinfo=fts3");
-
- assertOptions("CREATE VIRTUAL TABLE \"Book\" USING FTS4(\"author\", "
- + "matchinfo=fts3)",
- "matchinfo=fts3");
-
- assertOptions("CREATE VIRTUAL TABLE `Fun'Names` USING FTS4(matchinfo=fts3)",
- "matchinfo=fts3");
-
- assertOptions("CREATE VIRTUAL TABLE `Fun'With'Names` USING FTS4(\"odd'column'\", "
- + "`odd'column'again`, [select], 'left[col]is`weird', matchinfo=fts3)",
- "matchinfo=fts3");
-
- assertOptions("CREATE VIRTUAL TABLE 'Book' USING FTS4('content', 'pages', "
- + "'isbn', notindexed=pages, notindexed=isbn)",
- "notindexed=pages", "notindexed=isbn");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
- + "`content`, `pages`, notindexed=pages)",
- "tokenize=porter", "notindexed=pages");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
- + "`content`, `pages`, notindexed=pages)",
- "tokenize=porter", "notindexed=pages");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
- + "\"tokenchars=,\")",
- "tokenize=unicode61 \"tokenchars=,\"");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
- + "`tokenchars=,`)",
- "tokenize=unicode61 `tokenchars=,`");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
- + "\"tokenchars=.=\" \"separators=X\", `pages`, notindexed=pages)",
- "tokenize=unicode61 \"tokenchars=.=\" \"separators=X\"",
- "notindexed=pages");
-
- assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
- + "`author`, languageid=`lid`, matchinfo=fts3, notindexed=`pages`, "
- + "order=desc, prefix=`2,4`)",
- "tokenize=porter", "languageid=`lid`", "matchinfo=fts3",
- "notindexed=`pages`", "order=desc", "prefix=`2,4`");
- }
-
- private void assertOptions(String createSql, String... options) {
- Set<String> actualOptions = FtsTableInfo.parseOptions(createSql);
- Set<String> expectedOptions = Sets.newSet(options);
- assertThat(actualOptions, is(expectedOptions));
- }
-}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt
new file mode 100644
index 0000000..723aa8b
--- /dev/null
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.internal.util.collections.Sets
+
+@RunWith(JUnit4::class)
+class FtsTableInfoTest {
+ @Test
+ fun test_parseOptions() {
+ assertOptions("CREATE VIRTUAL TABLE Book USING FTS4()")
+ assertOptions("CREATE VIRTUAL TABLE Book USING FTS4( )")
+ assertOptions("CREATE VIRTUAL TABLE Book USING fts4(author)")
+ assertOptions(
+ "CREATE VIRTUAL TABLE Book USING FTS4(author, matchinfo=fts3)",
+ "matchinfo=fts3"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE \"Book\" USING FTS4(\"author\", matchinfo=fts3)",
+ "matchinfo=fts3"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Fun'Names` USING FTS4(matchinfo=fts3)",
+ "matchinfo=fts3"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Fun'With'Names` USING FTS4(\"odd'column'\", " +
+ "`odd'column'again`, [select], 'left[col]is`weird', matchinfo=fts3)",
+ "matchinfo=fts3"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE 'Book' USING FTS4('content', 'pages', " +
+ "'isbn', notindexed=pages, notindexed=isbn)",
+ "notindexed=pages", "notindexed=isbn"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+ "`content`, `pages`, notindexed=pages)",
+ "tokenize=porter", "notindexed=pages"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+ "`content`, `pages`, notindexed=pages)",
+ "tokenize=porter", "notindexed=pages"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 \"tokenchars=,\")",
+ "tokenize=unicode61 \"tokenchars=,\""
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 `tokenchars=,`)",
+ "tokenize=unicode61 `tokenchars=,`"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 " +
+ "\"tokenchars=.=\" \"separators=X\", `pages`, notindexed=pages)",
+ "tokenize=unicode61 \"tokenchars=.=\" \"separators=X\"",
+ "notindexed=pages"
+ )
+ assertOptions(
+ "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+ "`author`, languageid=`lid`, matchinfo=fts3, notindexed=`pages`, " +
+ "order=desc, prefix=`2,4`)",
+ "tokenize=porter", "languageid=`lid`", "matchinfo=fts3",
+ "notindexed=`pages`", "order=desc", "prefix=`2,4`"
+ )
+ }
+
+ private fun assertOptions(createSql: String, vararg options: String) {
+ val actualOptions = parseFtsOptions(createSql)
+ val expectedOptions = Sets.newSet(*options)
+ MatcherAssert.assertThat(actualOptions, CoreMatchers.`is`(expectedOptions))
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
new file mode 100644
index 0000000..87f71e6
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.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 androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmSuppressWildcards
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+expect class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+constructor(
+ database: RoomDatabase,
+ shadowTablesMap: Map<String, String>,
+ viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
+ vararg tableNames: String
+) {
+ /**
+ * Internal method to initialize table tracking. Invoked by generated code.
+ */
+ internal fun internalInit(connection: SQLiteConnection)
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
index 539c5bf..13ba545 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
@@ -24,7 +24,9 @@
import androidx.room.migration.Migration
import androidx.room.util.contains
import androidx.room.util.isAssignableFrom
+import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
import kotlin.jvm.JvmName
import kotlin.reflect.KClass
@@ -40,6 +42,16 @@
expect abstract class RoomDatabase {
/**
+ * The invalidation tracker for this database.
+ *
+ * You can use the invalidation tracker to get notified when certain tables in the database
+ * are modified.
+ *
+ * @return The invalidation tracker for the database.
+ */
+ val invalidationTracker: InvalidationTracker
+
+ /**
* Creates a connection manager to manage database connection. Note that this method
* is called when the [RoomDatabase] is initialized.
*
@@ -62,6 +74,16 @@
protected open fun createOpenDelegate(): RoomOpenDelegateMarker
/**
+ * Creates the invalidation tracker
+ *
+ * An implementation of this function is generated by the Room processor. Note that this method
+ * is called when the [RoomDatabase] is initialized.
+ *
+ * @return A new invalidation tracker.
+ */
+ protected abstract fun createInvalidationTracker(): InvalidationTracker
+
+ /**
* Returns a Set of required [AutoMigrationSpec] classes.
*
* An implementation of this function is generated by the Room processor. Note that this method
@@ -126,6 +148,15 @@
internal val requiredTypeConverterClasses: Map<KClass<*>, List<KClass<*>>>
/**
+ * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+ * initialized and opens a database connection.
+ *
+ * @param connection The database connection.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ protected fun internalInitInvalidationTracker(connection: SQLiteConnection)
+
+ /**
* Closes the database.
*
* Once a [RoomDatabase] is closed it should no longer be used.
@@ -133,6 +164,21 @@
fun close()
/**
+ * Performs a database operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ suspend fun <R> perform(isReadOnly: Boolean, sql: String, block: (SQLiteStatement) -> R): R
+
+ /**
+ * Performs a database transaction operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ suspend fun <R> performTransaction(
+ isReadOnly: Boolean,
+ block: suspend (TransactionScope<R>) -> R
+ ): R
+
+ /**
* Journal modes for SQLite database.
*
* @see Builder.setJournalMode
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt
new file mode 100644
index 0000000..05b487d
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class FtsTableInfo {
+ /**
+ * The table name
+ */
+ @JvmField
+ val name: String
+
+ /**
+ * The column names
+ */
+ @JvmField
+ val columns: Set<String>
+
+ /**
+ * The set of options. Each value in the set contains the option in the following format:
+ * <key, value>.
+ */
+ @JvmField
+ val options: Set<String>
+
+ constructor(name: String, columns: Set<String>, createSql: String)
+
+ companion object {
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A FtsTableInfo containing the columns and options for the provided table name.
+ */
+ @JvmStatic
+ fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo
+ }
+}
+
+internal fun FtsTableInfo.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is FtsTableInfo) return false
+ val that = other
+ if (name != that.name) return false
+ if (columns != that.columns) return false
+ return options == that.options
+}
+
+internal fun FtsTableInfo.hashCodeCommon(): Int {
+ var result = name.hashCode()
+ result = 31 * result + (columns.hashCode())
+ result = 31 * result + (options.hashCode())
+ return result
+}
+
+internal fun FtsTableInfo.toStringCommon(): String {
+ return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
new file mode 100644
index 0000000..d4ebdd5
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
@@ -0,0 +1,367 @@
+/*
+ * 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.room.util
+
+import androidx.room.ColumnInfo
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteStatement
+import androidx.sqlite.use
+
+/**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+@ColumnInfo.SQLiteTypeAffinity
+internal fun findAffinity(type: String?): Int {
+ if (type == null) {
+ return ColumnInfo.BLOB
+ }
+ val uppercaseType = type.uppercase()
+ if (uppercaseType.contains("INT")) {
+ return ColumnInfo.INTEGER
+ }
+ if (uppercaseType.contains("CHAR") ||
+ uppercaseType.contains("CLOB") ||
+ uppercaseType.contains("TEXT")
+ ) {
+ return ColumnInfo.TEXT
+ }
+ if (uppercaseType.contains("BLOB")) {
+ return ColumnInfo.BLOB
+ }
+ if (uppercaseType.contains("REAL") ||
+ uppercaseType.contains("FLOA") ||
+ uppercaseType.contains("DOUB")
+ ) {
+ return ColumnInfo.REAL
+ }
+ // SQLite returns NUMERIC here but it is like a catch all. We already
+ // have UNDEFINED so it is better to use UNDEFINED for consistency.
+ return ColumnInfo.UNDEFINED
+}
+
+internal fun readTableInfo(connection: SQLiteConnection, tableName: String): TableInfo {
+ val columns = readColumns(connection, tableName)
+ val foreignKeys = readForeignKeys(connection, tableName)
+ val indices = readIndices(connection, tableName)
+ return TableInfo(tableName, columns, foreignKeys, indices)
+}
+
+private fun readForeignKeys(
+ connection: SQLiteConnection,
+ tableName: String
+): Set<TableInfo.ForeignKey> {
+ // this seems to return everything in order but it is not documented so better be safe
+ connection.prepare("PRAGMA foreign_key_list(`$tableName`)").use { stmt ->
+ val idColumnIndex = stmt.getColumnIndex("id")
+ val seqColumnIndex = stmt.getColumnIndex("seq")
+ val tableColumnIndex = stmt.getColumnIndex("table")
+ val onDeleteColumnIndex = stmt.getColumnIndex("on_delete")
+ val onUpdateColumnIndex = stmt.getColumnIndex("on_update")
+ val ordered = readForeignKeyFieldMappings(stmt)
+
+ // Reset cursor as readForeignKeyFieldMappings has moved it
+ stmt.reset()
+ return buildSet {
+ while (stmt.step()) {
+ val seq = stmt.getLong(seqColumnIndex)
+ if (seq != 0L) {
+ continue
+ }
+ val id = stmt.getLong(idColumnIndex).toInt()
+ val myColumns = mutableListOf<String>()
+ val refColumns = mutableListOf<String>()
+
+ ordered.filter {
+ it.id == id
+ }.forEach { key ->
+ myColumns.add(key.from)
+ refColumns.add(key.to)
+ }
+
+ add(
+ TableInfo.ForeignKey(
+ referenceTable = stmt.getText(tableColumnIndex),
+ onDelete = stmt.getText(onDeleteColumnIndex),
+ onUpdate = stmt.getText(onUpdateColumnIndex),
+ columnNames = myColumns,
+ referenceColumnNames = refColumns
+ )
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
+ * sorting in the generated foreign key object.
+ */
+private class ForeignKeyWithSequence(
+ val id: Int,
+ val sequence: Int,
+ val from: String,
+ val to: String
+) : Comparable<ForeignKeyWithSequence> {
+ override fun compareTo(other: ForeignKeyWithSequence): Int {
+ val idCmp = id - other.id
+ return if (idCmp == 0) {
+ sequence - other.sequence
+ } else {
+ idCmp
+ }
+ }
+}
+
+private fun readForeignKeyFieldMappings(
+ stmt: SQLiteStatement
+): List<ForeignKeyWithSequence> {
+ val idColumnIndex = stmt.getColumnIndex("id")
+ val seqColumnIndex = stmt.getColumnIndex("seq")
+ val fromColumnIndex = stmt.getColumnIndex("from")
+ val toColumnIndex = stmt.getColumnIndex("to")
+
+ return buildList {
+ while (stmt.step()) {
+ add(
+ ForeignKeyWithSequence(
+ id = stmt.getLong(idColumnIndex).toInt(),
+ sequence = stmt.getLong(seqColumnIndex).toInt(),
+ from = stmt.getText(fromColumnIndex),
+ to = stmt.getText(toColumnIndex)
+ )
+ )
+ }
+ }.sorted()
+}
+
+private fun readColumns(
+ connection: SQLiteConnection,
+ tableName: String
+): Map<String, TableInfo.Column> {
+ connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
+ if (!stmt.step()) {
+ return emptyMap()
+ }
+
+ val nameIndex = stmt.getColumnIndex("name")
+ val typeIndex = stmt.getColumnIndex("type")
+ val notNullIndex = stmt.getColumnIndex("notnull")
+ val pkIndex = stmt.getColumnIndex("pk")
+ val defaultValueIndex = stmt.getColumnIndex("dflt_value")
+
+ return buildMap {
+ do {
+ val name = stmt.getText(nameIndex)
+ val type = stmt.getText(typeIndex)
+ val notNull = stmt.getLong(notNullIndex) != 0L
+ val primaryKeyPosition = stmt.getLong(pkIndex).toInt()
+ val defaultValue =
+ if (stmt.isNull(defaultValueIndex)) null else stmt.getText(defaultValueIndex)
+ put(
+ key = name,
+ value = TableInfo.Column(
+ name = name,
+ type = type,
+ notNull = notNull,
+ primaryKeyPosition = primaryKeyPosition,
+ defaultValue = defaultValue,
+ createdFrom = TableInfo.CREATED_FROM_DATABASE
+ )
+ )
+ } while (stmt.step())
+ }
+ }
+}
+
+/**
+ * @return null if we cannot read the indices due to older sqlite implementations.
+ */
+private fun readIndices(connection: SQLiteConnection, tableName: String): Set<TableInfo.Index>? {
+ connection.prepare("PRAGMA index_list(`$tableName`)").use { stmt ->
+ val nameColumnIndex = stmt.getColumnIndex("name")
+ val originColumnIndex = stmt.getColumnIndex("origin")
+ val uniqueIndex = stmt.getColumnIndex("unique")
+ if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
+ // we cannot read them so better not validate any index.
+ return null
+ }
+ return buildSet {
+ while (stmt.step()) {
+ val origin = stmt.getText(originColumnIndex)
+ if ("c" != origin) {
+ // Ignore auto-created indices
+ continue
+ }
+ val name = stmt.getText(nameColumnIndex)
+ val unique = stmt.getLong(uniqueIndex) == 1L
+ // Read index but if we cannot read it properly so better not read it
+ val index = readIndex(connection, name, unique) ?: return null
+ add(index)
+ }
+ }
+ }
+}
+
+/**
+ * @return null if we cannot read the index due to older sqlite implementations.
+ */
+private fun readIndex(
+ connection: SQLiteConnection,
+ name: String,
+ unique: Boolean
+): TableInfo.Index? {
+ return connection.prepare("PRAGMA index_xinfo(`$name`)").use { stmt ->
+ val seqnoColumnIndex = stmt.getColumnIndex("seqno")
+ val cidColumnIndex = stmt.getColumnIndex("cid")
+ val nameColumnIndex = stmt.getColumnIndex("name")
+ val descColumnIndex = stmt.getColumnIndex("desc")
+ if (
+ seqnoColumnIndex == -1 ||
+ cidColumnIndex == -1 ||
+ nameColumnIndex == -1 ||
+ descColumnIndex == -1
+ ) {
+ // we cannot read them so better not validate any index.
+ return null
+ }
+ val columnsMap = mutableMapOf<Int, String>()
+ val ordersMap = mutableMapOf<Int, String>()
+ while (stmt.step()) {
+ val cid = stmt.getLong(cidColumnIndex).toInt()
+ if (cid < 0) {
+ // Ignore SQLite row ID
+ continue
+ }
+ val seq = stmt.getLong(seqnoColumnIndex).toInt()
+ val columnName = stmt.getText(nameColumnIndex)
+ val order = if (stmt.getLong(descColumnIndex) > 0) "DESC" else "ASC"
+ columnsMap[seq] = columnName
+ ordersMap[seq] = order
+ }
+ val columns = columnsMap.entries.sortedBy { it.key }.map { it.value }.toList()
+ val orders = ordersMap.entries.sortedBy { it.key }.map { it.value }.toList()
+ TableInfo.Index(name, unique, columns, orders)
+ }
+}
+
+internal fun readFtsColumns(connection: SQLiteConnection, tableName: String): Set<String> {
+ return buildSet {
+ connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
+ if (!stmt.step()) return@use
+ val nameIndex = stmt.getColumnIndex("name")
+ do {
+ add(stmt.getText(nameIndex))
+ } while (stmt.step())
+ }
+ }
+}
+
+internal fun readFtsOptions(connection: SQLiteConnection, tableName: String): Set<String> {
+ val sql = connection.prepare(
+ "SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
+ ).use { stmt ->
+ if (stmt.step()) {
+ stmt.getText(stmt.getColumnIndex("sql"))
+ } else {
+ ""
+ }
+ }
+ return parseFtsOptions(sql)
+}
+
+// A set of valid FTS Options
+private val FTS_OPTIONS = arrayOf(
+ "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
+ "order=", "prefix=", "uncompress="
+)
+
+/**
+ * Parses FTS options from the create statement of an FTS table.
+ *
+ * This method assumes the given create statement is a valid well-formed SQLite statement as
+ * defined in the [CREATE VIRTUAL TABLE
+ * syntax diagram](https://www.sqlite.org/lang_createvtab.html).
+ *
+ * @param createStatement the "CREATE VIRTUAL TABLE" statement.
+ * @return the set of FTS option key and values in the create statement.
+ */
+internal fun parseFtsOptions(createStatement: String): Set<String> {
+ if (createStatement.isEmpty()) {
+ return emptySet()
+ }
+
+ // Module arguments are within the parenthesis followed by the module name.
+ val argsString = createStatement.substring(
+ createStatement.indexOf('(') + 1,
+ createStatement.lastIndexOf(')')
+ )
+
+ // Split the module argument string by the comma delimiter, keeping track of quotation
+ // so that if the delimiter is found within a string literal we don't substring at the
+ // wrong index. SQLite supports four ways of quoting keywords, see:
+ // https://www.sqlite.org/lang_keywords.html
+ val args = mutableListOf<String>()
+ val quoteStack = ArrayDeque<Char>()
+ var lastDelimiterIndex = -1
+ argsString.forEachIndexed { i, value ->
+ when (value) {
+ '\'', '"', '`' ->
+ if (quoteStack.isEmpty()) {
+ quoteStack.addFirst(value)
+ } else if (quoteStack.firstOrNull() == value) {
+ quoteStack.removeLast()
+ }
+ '[' -> if (quoteStack.isEmpty()) {
+ quoteStack.addFirst(value)
+ }
+ ']' -> if (!quoteStack.isEmpty() && quoteStack.firstOrNull() == '[') {
+ quoteStack.removeLast()
+ }
+ ',' -> if (quoteStack.isEmpty()) {
+ args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
+ lastDelimiterIndex = i
+ }
+ }
+ }
+
+ // Add final argument.
+ args.add(argsString.substring(lastDelimiterIndex + 1).trim())
+
+ // Match args against valid options, otherwise they are column definitions.
+ val options = args.filter { arg ->
+ FTS_OPTIONS.any { validOption ->
+ arg.startsWith(validOption)
+ }
+ }.toSet()
+ return options
+}
+
+internal fun readViewInfo(connection: SQLiteConnection, viewName: String): ViewInfo {
+ return connection.prepare(
+ "SELECT name, sql FROM sqlite_master " +
+ "WHERE type = 'view' AND name = '$viewName'"
+ ).use { stmt ->
+ if (stmt.step()) {
+ ViewInfo(stmt.getText(0), stmt.getText(1))
+ } else {
+ ViewInfo(viewName, null)
+ }
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
new file mode 100644
index 0000000..cebd289
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
@@ -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.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteStatement
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/**
+ * Returns the zero-based index for the given column name, or throws [IllegalArgumentException] if
+ * the column doesn't exist.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun getColumnIndexOrThrow(stmt: SQLiteStatement, name: String): Int {
+ val index: Int = stmt.getColumnIndex(name)
+ if (index >= 0) {
+ return index
+ }
+ val availableColumns = List(stmt.getColumnCount()) { stmt.getColumnName(it) }.joinToString()
+ throw IllegalArgumentException(
+ "Column '$name' does not exist. Available columns: [$availableColumns]"
+ )
+}
+
+/**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ */
+internal expect fun SQLiteStatement.getColumnIndex(name: String): Int
+
+// TODO(b/322183292): Consider optimizing by creating a String->Int map, similar to Android
+internal fun SQLiteStatement.columnIndexOf(name: String): Int {
+ val columnCount = getColumnCount()
+ for (i in 0 until columnCount) {
+ if (name == getColumnName(i)) return i
+ }
+ return -1
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
new file mode 100644
index 0000000..891b8e7
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2017 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class TableInfo(
+ name: String,
+ columns: Map<String, Column>,
+ foreignKeys: Set<ForeignKey>,
+ indices: Set<Index>? = null
+) {
+ /**
+ * The table name.
+ */
+ @JvmField
+ val name: String
+ @JvmField
+ val columns: Map<String, Column>
+ @JvmField
+ val foreignKeys: Set<ForeignKey>
+ @JvmField
+ val indices: Set<Index>?
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+
+ companion object {
+ /**
+ * Identifier for when the info is created from an unknown source.
+ */
+ val CREATED_FROM_UNKNOWN: Int
+
+ /**
+ * Identifier for when the info is created from an entity definition, such as generated code
+ * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+ */
+ val CREATED_FROM_ENTITY: Int
+
+ /**
+ * Identifier for when the info is created from the database itself, reading information
+ * from a PRAGMA, such as table_info.
+ */
+ val CREATED_FROM_DATABASE: Int
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
+ @JvmStatic
+ fun read(connection: SQLiteConnection, tableName: String): TableInfo
+ }
+
+ /**
+ * Holds the information about a database column.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ class Column(
+ name: String,
+ type: String,
+ notNull: Boolean,
+ primaryKeyPosition: Int,
+ defaultValue: String?,
+ createdFrom: Int
+ ) {
+ /**
+ * The column name.
+ */
+ @JvmField
+ val name: String
+ /**
+ * The column type affinity.
+ */
+ @JvmField
+ val type: String
+ /**
+ * Whether or not the column can be NULL.
+ */
+ @JvmField
+ val notNull: Boolean
+ @JvmField
+ val primaryKeyPosition: Int
+ @JvmField
+ val defaultValue: String?
+ @JvmField
+ val createdFrom: Int
+
+ /**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ *
+ * This is the value Room uses for equality check.
+ */
+ @SQLiteTypeAffinity
+ @JvmField
+ val affinity: Int
+
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ val isPrimaryKey: Boolean
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+ }
+
+ /**
+ * Holds the information about an SQLite foreign key
+ *
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ class ForeignKey(
+ referenceTable: String,
+ onDelete: String,
+ onUpdate: String,
+ columnNames: List<String>,
+ referenceColumnNames: List<String>
+ ) {
+ @JvmField
+ val referenceTable: String
+ @JvmField
+ val onDelete: String
+ @JvmField
+ val onUpdate: String
+ @JvmField
+ val columnNames: List<String>
+ @JvmField
+ val referenceColumnNames: List<String>
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+ }
+
+ /**
+ * Holds the information about an SQLite index
+ *
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ class Index(
+ name: String,
+ unique: Boolean,
+ columns: List<String>,
+ orders: List<String>
+ ) {
+
+ @JvmField
+ val name: String
+ @JvmField
+ val unique: Boolean
+ @JvmField
+ val columns: List<String>
+ @JvmField
+ var orders: List<String>
+
+ companion object {
+ // should match the value in Index.kt
+ val DEFAULT_PREFIX: String
+ }
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+ }
+}
+
+internal fun TableInfo.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TableInfo) return false
+ if (name != other.name) return false
+ if (columns != other.columns) {
+ return false
+ }
+ if (foreignKeys != other.foreignKeys) {
+ return false
+ }
+ return if (indices == null || other.indices == null) {
+ // if one us is missing index information, seems like we couldn't acquire the
+ // information so we better skip.
+ true
+ } else indices == other.indices
+}
+
+internal fun TableInfo.hashCodeCommon(): Int {
+ var result = name.hashCode()
+ result = 31 * result + columns.hashCode()
+ result = 31 * result + foreignKeys.hashCode()
+ // skip index, it is not reliable for comparison.
+ return result
+}
+
+internal fun TableInfo.toStringCommon(): String {
+ return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
+ "indices=$indices}")
+}
+
+internal fun TableInfo.Column.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TableInfo.Column) return false
+ if (!equalsInPrimaryKey(other)) return false
+ if (name != other.name) return false
+ if (notNull != other.notNull) return false
+ // Only validate default value if it was defined in an entity, i.e. if the info
+ // from the compiler itself has it. b/136019383
+ if (
+ createdFrom == TableInfo.CREATED_FROM_ENTITY &&
+ other.createdFrom == TableInfo.CREATED_FROM_DATABASE &&
+ defaultValue != null &&
+ !defaultValueEqualsCommon(defaultValue, other.defaultValue)
+ ) {
+ return false
+ } else if (
+ createdFrom == TableInfo.CREATED_FROM_DATABASE &&
+ other.createdFrom == TableInfo.CREATED_FROM_ENTITY &&
+ other.defaultValue != null &&
+ !defaultValueEqualsCommon(other.defaultValue, defaultValue)
+ ) {
+ return false
+ } else if (
+ createdFrom != TableInfo.CREATED_FROM_UNKNOWN &&
+ createdFrom == other.createdFrom &&
+ (if (defaultValue != null)
+ !defaultValueEqualsCommon(defaultValue, other.defaultValue)
+ else other.defaultValue != null)
+ ) {
+ return false
+ }
+ return affinity == other.affinity
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal expect fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean
+
+/**
+ * Checks if the default values provided match. Handles the special case in which the
+ * default value is surrounded by parenthesis (e.g. encountered in b/182284899).
+ *
+ * Surrounding parenthesis are removed by SQLite when reading from the database, hence
+ * this function will check if they are present in the actual value, if so, it will
+ * compare the two values by ignoring the surrounding parenthesis.
+ *
+ */
+internal fun defaultValueEqualsCommon(current: String, other: String?): Boolean {
+ if (current == other) {
+ return true
+ } else if (containsSurroundingParenthesis(current)) {
+ return current.substring(1, current.length - 1).trim() == other
+ }
+ return false
+}
+
+/**
+ * Checks for potential surrounding parenthesis, if found, removes them and checks if
+ * remaining parenthesis are balanced. If so, the surrounding parenthesis are redundant,
+ * and returns true.
+ */
+private fun containsSurroundingParenthesis(current: String): Boolean {
+ if (current.isEmpty()) {
+ return false
+ }
+ var surroundingParenthesis = 0
+ current.forEachIndexed { i, c ->
+ if (i == 0 && c != '(') {
+ return false
+ }
+ if (c == '(') {
+ surroundingParenthesis++
+ } else if (c == ')') {
+ surroundingParenthesis--
+ if (surroundingParenthesis == 0 && i != current.length - 1) {
+ return false
+ }
+ }
+ }
+ return surroundingParenthesis == 0
+}
+
+internal fun TableInfo.Column.hashCodeCommon(): Int {
+ var result = name.hashCode()
+ result = 31 * result + affinity
+ result = 31 * result + if (notNull) 1231 else 1237
+ result = 31 * result + primaryKeyPosition
+ // Default value is not part of the hashcode since we conditionally check it for
+ // equality which would break the equals + hashcode contract.
+ // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
+ return result
+}
+
+internal fun TableInfo.Column.toStringCommon(): String {
+ return "Column{name='$name', type='$type', affinity='$affinity', " +
+ "notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
+ "defaultValue='${defaultValue ?: "undefined"}'}"
+}
+
+internal fun TableInfo.ForeignKey.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TableInfo.ForeignKey) return false
+ if (referenceTable != other.referenceTable) return false
+ if (onDelete != other.onDelete) return false
+ if (onUpdate != other.onUpdate) return false
+ return if (columnNames != other.columnNames) false else referenceColumnNames ==
+ other.referenceColumnNames
+}
+
+internal fun TableInfo.ForeignKey.hashCodeCommon(): Int {
+ var result = referenceTable.hashCode()
+ result = 31 * result + onDelete.hashCode()
+ result = 31 * result + onUpdate.hashCode()
+ result = 31 * result + columnNames.hashCode()
+ result = 31 * result + referenceColumnNames.hashCode()
+ return result
+}
+
+internal fun TableInfo.ForeignKey.toStringCommon(): String {
+ return "ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
+ "onUpdate='$onUpdate', columnNames=$columnNames, " +
+ "referenceColumnNames=$referenceColumnNames}"
+}
+
+internal fun TableInfo.Index.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TableInfo.Index) return false
+ if (unique != other.unique) {
+ return false
+ }
+ if (columns != other.columns) {
+ return false
+ }
+ if (orders != other.orders) {
+ return false
+ }
+ return if (name.startsWith(TableInfo.Index.DEFAULT_PREFIX)) {
+ other.name.startsWith(TableInfo.Index.DEFAULT_PREFIX)
+ } else {
+ name == other.name
+ }
+}
+
+internal fun TableInfo.Index.hashCodeCommon(): Int {
+ var result = if (name.startsWith(TableInfo.Index.DEFAULT_PREFIX)) {
+ TableInfo.Index.DEFAULT_PREFIX.hashCode()
+ } else {
+ name.hashCode()
+ }
+ result = 31 * result + if (unique) 1 else 0
+ result = 31 * result + columns.hashCode()
+ result = 31 * result + orders.hashCode()
+ return result
+}
+
+internal fun TableInfo.Index.toStringCommon(): String {
+ return "Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}"
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt
new file mode 100644
index 0000000..8720f4a
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class ViewInfo(
+ name: String,
+ sql: String?
+) {
+ /**
+ * The view name
+ */
+ @JvmField
+ val name: String
+ /**
+ * The SQL of CREATE VIEW.
+ */
+ @JvmField
+ val sql: String?
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+
+ companion object {
+ /**
+ * Reads the view information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param viewName The view name.
+ * @return A ViewInfo containing the schema information for the provided view name.
+ */
+ @JvmStatic
+ fun read(connection: SQLiteConnection, viewName: String): ViewInfo
+ }
+}
+
+internal fun ViewInfo.equalsCommon(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ViewInfo) return false
+ return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
+}
+
+internal fun ViewInfo.hashCodeCommon(): Int {
+ var result = name.hashCode()
+ result = 31 * result + (sql?.hashCode() ?: 0)
+ return result
+}
+
+internal fun ViewInfo.toStringCommon(): String {
+ return "ViewInfo{name='$name', sql='$sql'}"
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.kt
new file mode 100644
index 0000000..98e4dfe
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.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 androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+actual class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
+ database: RoomDatabase,
+ shadowTablesMap: Map<String, String>,
+ viewTables: Map<String, Set<String>>,
+ vararg tableNames: String
+) {
+ /**
+ * Internal method to initialize table tracking. Invoked by generated code.
+ */
+ internal actual fun internalInit(connection: SQLiteConnection) {
+ }
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
index 37216e0..3299961 100644
--- a/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
@@ -19,7 +19,9 @@
import androidx.annotation.RestrictTo
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
+import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
import kotlin.reflect.KClass
/**
@@ -38,6 +40,16 @@
private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
/**
+ * The invalidation tracker for this database.
+ *
+ * You can use the invalidation tracker to get notified when certain tables in the database
+ * are modified.
+ *
+ * @return The invalidation tracker for the database.
+ */
+ actual val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
+ /**
* Called by Room when it is initialized.
*
* @param configuration The database configuration.
@@ -78,6 +90,16 @@
}
/**
+ * Creates the invalidation tracker
+ *
+ * An implementation of this function is generated by the Room processor. Note that this method
+ * is called when the [RoomDatabase] is initialized.
+ *
+ * @return A new invalidation tracker.
+ */
+ protected actual abstract fun createInvalidationTracker(): InvalidationTracker
+
+ /**
* Returns a Set of required [AutoMigrationSpec] classes.
*
* An implementation of this function is generated by the Room processor. Note that this method
@@ -156,6 +178,17 @@
get() = getRequiredTypeConverterClasses()
/**
+ * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+ * initialized and opens a database connection.
+ *
+ * @param connection The database connection.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+ invalidationTracker.internalInit(connection)
+ }
+
+ /**
* Closes the database.
*
* Once a [RoomDatabase] is closed it should no longer be used.
@@ -165,6 +198,39 @@
}
/**
+ * Performs a database operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> perform(
+ isReadOnly: Boolean,
+ sql: String,
+ block: (SQLiteStatement) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { connection ->
+ connection.usePrepared(sql, block)
+ }
+ }
+
+ /**
+ * Performs a database operation in a transaction.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> performTransaction(
+ isReadOnly: Boolean,
+ block: suspend (TransactionScope<R>) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { transactor ->
+ val type = if (isReadOnly) {
+ Transactor.SQLiteTransactionType.DEFERRED
+ } else {
+ Transactor.SQLiteTransactionType.IMMEDIATE
+ }
+ // TODO: Notify Invalidation Tracker before and after transaction block.
+ transactor.withTransaction(type, block)
+ }
+ }
+
+ /**
* Journal modes for SQLite database.
*
* @see Builder.setJournalMode
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt
new file mode 100644
index 0000000..f2e451d
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class FtsTableInfo(
+ /**
+ * The table name
+ */
+ @JvmField
+ actual val name: String,
+
+ /**
+ * The column names
+ */
+ @JvmField
+ actual val columns: Set<String>,
+
+ /**
+ * The set of options. Each value in the set contains the option in the following format:
+ * <key, value>.
+ */
+ @JvmField
+ actual val options: Set<String>
+) {
+ actual constructor(name: String, columns: Set<String>, createSql: String) :
+ this(name, columns, parseFtsOptions(createSql))
+
+ override fun equals(other: Any?) = equalsCommon(other)
+
+ override fun hashCode() = hashCodeCommon()
+
+ override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A FtsTableInfo containing the columns and options for the provided table name.
+ */
+ @JvmStatic
+ actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+ val columns = readFtsColumns(connection, tableName)
+ val options = readFtsOptions(connection, tableName)
+ return FtsTableInfo(tableName, columns, options)
+ }
+ }
+}
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
similarity index 65%
copy from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
copy to room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
index c5e54fe..62b635e 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package sample;
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.sqlite.SQLiteStatement
/**
- * Usage of a static method with an explicit class.
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
*/
-@SuppressWarnings({"deprecation", "unused"})
-class StaticMethodExplicitClass {
- void main() {
- ReplaceWithUsageJava.toString(this);
- }
-}
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int = columnIndexOf(name)
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt
new file mode 100644
index 0000000..5a8c378
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class TableInfo actual constructor(
+ /**
+ * The table name.
+ */
+ actual val name: String,
+ actual val columns: Map<String, Column>,
+ actual val foreignKeys: Set<ForeignKey>,
+ actual val indices: Set<Index>?
+) {
+ /**
+ * Identifies from where the info object was created.
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
+ internal annotation class CreatedFrom()
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Identifier for when the info is created from an unknown source.
+ */
+ actual const val CREATED_FROM_UNKNOWN = 0
+
+ /**
+ * Identifier for when the info is created from an entity definition, such as generated code
+ * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+ */
+ actual const val CREATED_FROM_ENTITY = 1
+
+ /**
+ * Identifier for when the info is created from the database itself, reading information
+ * from a PRAGMA, such as table_info.
+ */
+ actual const val CREATED_FROM_DATABASE = 2
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
+ actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+ return readTableInfo(connection, tableName)
+ }
+ }
+
+ /**
+ * Holds the information about a database column.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class Column actual constructor(
+ /**
+ * The column name.
+ */
+ actual val name: String,
+ /**
+ * The column type affinity.
+ */
+ actual val type: String,
+ /**
+ * Whether or not the column can be NULL.
+ */
+ actual val notNull: Boolean,
+ actual val primaryKeyPosition: Int,
+ actual val defaultValue: String?,
+ @CreatedFrom
+ actual val createdFrom: Int
+ ) {
+ /**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ *
+ * This is the value Room uses for equality check.
+ */
+ @SQLiteTypeAffinity
+ actual val affinity: Int = findAffinity(type)
+
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ actual val isPrimaryKey: Boolean
+ get() = primaryKeyPosition > 0
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+
+ /**
+ * Holds the information about an SQLite foreign key
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class ForeignKey actual constructor(
+ actual val referenceTable: String,
+ actual val onDelete: String,
+ actual val onUpdate: String,
+ actual val columnNames: List<String>,
+ actual val referenceColumnNames: List<String>
+ ) {
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+
+ /**
+ * Holds the information about an SQLite index
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class Index actual constructor(
+ actual val name: String,
+ actual val unique: Boolean,
+ actual val columns: List<String>,
+ actual var orders: List<String>
+ ) {
+ init {
+ orders = orders.ifEmpty {
+ List(columns.size) { androidx.room.Index.Order.ASC.name }
+ }
+ }
+
+ actual companion object {
+ // should match the value in Index.kt
+ actual const val DEFAULT_PREFIX = "index_"
+ }
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+ return isPrimaryKey == other.isPrimaryKey
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt
new file mode 100644
index 0000000..883d803
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class ViewInfo actual constructor(
+ /**
+ * The view name
+ */
+ actual val name: String,
+ /**
+ * The SQL of CREATE VIEW.
+ */
+ actual val sql: String?
+) {
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Reads the view information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param viewName The view name.
+ * @return A ViewInfo containing the schema information for the provided view name.
+ */
+ actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+ return readViewInfo(connection, viewName)
+ }
+ }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.kt
new file mode 100644
index 0000000..98e4dfe
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.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 androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+actual class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
+ database: RoomDatabase,
+ shadowTablesMap: Map<String, String>,
+ viewTables: Map<String, Set<String>>,
+ vararg tableNames: String
+) {
+ /**
+ * Internal method to initialize table tracking. Invoked by generated code.
+ */
+ internal actual fun internalInit(connection: SQLiteConnection) {
+ }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
index ae9ee7d..ec3eed9 100644
--- a/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
@@ -19,7 +19,9 @@
import androidx.annotation.RestrictTo
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
+import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
import kotlin.reflect.KClass
/**
@@ -38,6 +40,16 @@
private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
/**
+ * The invalidation tracker for this database.
+ *
+ * You can use the invalidation tracker to get notified when certain tables in the database
+ * are modified.
+ *
+ * @return The invalidation tracker for the database.
+ */
+ actual val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
+ /**
* Called by Room when it is initialized.
*
* @param configuration The database configuration.
@@ -78,6 +90,16 @@
}
/**
+ * Creates the invalidation tracker
+ *
+ * An implementation of this function is generated by the Room processor. Note that this method
+ * is called when the [RoomDatabase] is initialized.
+ *
+ * @return A new invalidation tracker.
+ */
+ protected actual abstract fun createInvalidationTracker(): InvalidationTracker
+
+ /**
* Returns a Set of required [AutoMigrationSpec] classes.
*
* An implementation of this function is generated by the Room processor. Note that this method
@@ -156,6 +178,17 @@
get() = getRequiredTypeConverterClasses()
/**
+ * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+ * initialized and opens a database connection.
+ *
+ * @param connection The database connection.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+ invalidationTracker.internalInit(connection)
+ }
+
+ /**
* Closes the database.
*
* Once a [RoomDatabase] is closed it should no longer be used.
@@ -165,6 +198,39 @@
}
/**
+ * Performs a database operation.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> perform(
+ isReadOnly: Boolean,
+ sql: String,
+ block: (SQLiteStatement) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { connection ->
+ connection.usePrepared(sql, block)
+ }
+ }
+
+ /**
+ * Performs a database operation in a transaction.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun <R> performTransaction(
+ isReadOnly: Boolean,
+ block: suspend (TransactionScope<R>) -> R
+ ): R {
+ return connectionManager.useConnection(isReadOnly) { transactor ->
+ val type = if (isReadOnly) {
+ Transactor.SQLiteTransactionType.DEFERRED
+ } else {
+ Transactor.SQLiteTransactionType.IMMEDIATE
+ }
+ // TODO: Notify Invalidation Tracker before and after transaction block.
+ transactor.withTransaction(type, block)
+ }
+ }
+
+ /**
* Journal modes for SQLite database.
*
* @see Builder.setJournalMode
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt
new file mode 100644
index 0000000..bc61b67
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class FtsTableInfo(
+ /**
+ * The table name
+ */
+ actual val name: String,
+
+ /**
+ * The column names
+ */
+ actual val columns: Set<String>,
+
+ /**
+ * The set of options. Each value in the set contains the option in the following format:
+ * <key, value>.
+ */
+ actual val options: Set<String>
+) {
+ actual constructor(name: String, columns: Set<String>, createSql: String) :
+ this(name, columns, parseFtsOptions(createSql))
+
+ override fun equals(other: Any?) = equalsCommon(other)
+
+ override fun hashCode() = hashCodeCommon()
+
+ override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A FtsTableInfo containing the columns and options for the provided table name.
+ */
+ actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+ val columns = readFtsColumns(connection, tableName)
+ val options = readFtsOptions(connection, tableName)
+ return FtsTableInfo(tableName, columns, options)
+ }
+ }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.native.kt
similarity index 100%
rename from room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.kt
rename to room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.native.kt
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
similarity index 61%
copy from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
copy to room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
index c5e54fe..437fe5c 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
@@ -14,14 +14,16 @@
* limitations under the License.
*/
-package sample;
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.sqlite.SQLiteStatement
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
/**
- * Usage of a static method with an explicit class.
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
*/
-@SuppressWarnings({"deprecation", "unused"})
-class StaticMethodExplicitClass {
- void main() {
- ReplaceWithUsageJava.toString(this);
- }
-}
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int = columnIndexOf(name)
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt
new file mode 100644
index 0000000..5a8c378
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class TableInfo actual constructor(
+ /**
+ * The table name.
+ */
+ actual val name: String,
+ actual val columns: Map<String, Column>,
+ actual val foreignKeys: Set<ForeignKey>,
+ actual val indices: Set<Index>?
+) {
+ /**
+ * Identifies from where the info object was created.
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
+ internal annotation class CreatedFrom()
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Identifier for when the info is created from an unknown source.
+ */
+ actual const val CREATED_FROM_UNKNOWN = 0
+
+ /**
+ * Identifier for when the info is created from an entity definition, such as generated code
+ * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+ */
+ actual const val CREATED_FROM_ENTITY = 1
+
+ /**
+ * Identifier for when the info is created from the database itself, reading information
+ * from a PRAGMA, such as table_info.
+ */
+ actual const val CREATED_FROM_DATABASE = 2
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
+ actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+ return readTableInfo(connection, tableName)
+ }
+ }
+
+ /**
+ * Holds the information about a database column.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class Column actual constructor(
+ /**
+ * The column name.
+ */
+ actual val name: String,
+ /**
+ * The column type affinity.
+ */
+ actual val type: String,
+ /**
+ * Whether or not the column can be NULL.
+ */
+ actual val notNull: Boolean,
+ actual val primaryKeyPosition: Int,
+ actual val defaultValue: String?,
+ @CreatedFrom
+ actual val createdFrom: Int
+ ) {
+ /**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ *
+ * This is the value Room uses for equality check.
+ */
+ @SQLiteTypeAffinity
+ actual val affinity: Int = findAffinity(type)
+
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ actual val isPrimaryKey: Boolean
+ get() = primaryKeyPosition > 0
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+
+ /**
+ * Holds the information about an SQLite foreign key
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class ForeignKey actual constructor(
+ actual val referenceTable: String,
+ actual val onDelete: String,
+ actual val onUpdate: String,
+ actual val columnNames: List<String>,
+ actual val referenceColumnNames: List<String>
+ ) {
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+
+ /**
+ * Holds the information about an SQLite index
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ actual class Index actual constructor(
+ actual val name: String,
+ actual val unique: Boolean,
+ actual val columns: List<String>,
+ actual var orders: List<String>
+ ) {
+ init {
+ orders = orders.ifEmpty {
+ List(columns.size) { androidx.room.Index.Order.ASC.name }
+ }
+ }
+
+ actual companion object {
+ // should match the value in Index.kt
+ actual const val DEFAULT_PREFIX = "index_"
+ }
+
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+ }
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+ return isPrimaryKey == other.isPrimaryKey
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt
new file mode 100644
index 0000000..883d803
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class ViewInfo actual constructor(
+ /**
+ * The view name
+ */
+ actual val name: String,
+ /**
+ * The SQL of CREATE VIEW.
+ */
+ actual val sql: String?
+) {
+ actual override fun equals(other: Any?) = equalsCommon(other)
+
+ actual override fun hashCode() = hashCodeCommon()
+
+ actual override fun toString() = toStringCommon()
+
+ actual companion object {
+ /**
+ * Reads the view information from the given database.
+ *
+ * @param connection The database connection to read the information from.
+ * @param viewName The view name.
+ * @return A ViewInfo containing the schema information for the provided view name.
+ */
+ actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+ return readViewInfo(connection, viewName)
+ }
+ }
+}
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
index 9ad6b2d..9d3f9c2 100644
--- a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
@@ -36,6 +36,10 @@
}
}
+ override fun createInvalidationTracker(): InvalidationTracker {
+ return InvalidationTracker(this, emptyMap(), emptyMap())
+ }
+
override fun createAutoMigrations(
autoMigrationSpecs: Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec>
): List<Migration> {
diff --git a/settings.gradle b/settings.gradle
index c5cec53..c0ab326 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -340,9 +340,6 @@
includeProject(":annotation:annotation-experimental")
includeProject(":annotation:annotation-experimental-lint")
includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
-includeProject(":annotation:annotation-replacewith")
-includeProject(":annotation:annotation-replacewith-lint")
-includeProject(":annotation:annotation-replacewith-lint-integration-tests", "annotation/annotation-replacewith-lint/integration-tests")
includeProject(":annotation:annotation-sampled")
includeProject(":appactions:builtintypes:builtintypes", [BuildType.MAIN])
includeProject(":appactions:builtintypes:builtintypes:builtintypes-samples", "appactions/builtintypes/builtintypes/samples", [BuildType.MAIN])
@@ -742,7 +739,7 @@
includeProject(":lifecycle:integration-tests:incrementality", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:integration-tests:lifecycle-testapp", "lifecycle/integration-tests/testapp", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:integration-tests:lifecycle-testapp-kotlin", "lifecycle/integration-tests/kotlintestapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
@@ -770,6 +767,7 @@
includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lint:lint-gradle", [BuildType.MAIN])
includeProject(":lint-checks")
includeProject(":lint-checks:integration-tests")
includeProject(":loader:loader", [BuildType.MAIN])
diff --git a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
index ef5bed9..4b9926c 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
@@ -188,15 +188,8 @@
fun readColumnNameWithoutStep() = testWithConnection { connection ->
connection.execSQL("CREATE TABLE Test (col)")
connection.prepare("SELECT col FROM Test").use {
- if (driverType == TestDriverType.ANDROID_FRAMEWORK) {
- // In Android framework the statement is not compiled until first step, so
- // proper analysis of result column count cannot be done.
- val message = assertFailsWith<SQLiteException> { it.getColumnName(0) }.message
- assertThat(message).isEqualTo("Error code: 21, message: no row")
- } else {
- assertThat(it.getColumnCount()).isEqualTo(1)
- assertThat(it.getColumnName(0)).isEqualTo("col")
- }
+ assertThat(it.getColumnCount()).isEqualTo(1)
+ assertThat(it.getColumnName(0)).isEqualTo("col")
}
}
diff --git a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
index 61a2221..1ee0693 100644
--- a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
+++ b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
@@ -141,30 +141,22 @@
override fun getColumnCount(): Int {
throwIfClosed()
+ ensureCursor()
return cursor?.columnCount ?: 0
}
override fun getColumnName(index: Int): String {
throwIfClosed()
- val c = throwIfNoRow()
+ ensureCursor()
+ val c = checkNotNull(cursor)
throwIfInvalidColumn(c, index)
return c.getColumnName(index)
}
override fun step(): Boolean {
throwIfClosed()
- if (cursor == null) {
- cursor = db.rawQueryWithFactory(
- /* cursorFactory = */ { _, masterQuery, editTable, query ->
- bindTo(query)
- SQLiteCursor(masterQuery, editTable, query)
- },
- /* sql = */ sql,
- /* selectionArgs = */ arrayOfNulls(0),
- /* editTable = */ null
- )
- }
- return requireNotNull(cursor).moveToNext()
+ ensureCursor()
+ return checkNotNull(cursor).moveToNext()
}
override fun reset() {
@@ -218,6 +210,20 @@
}
}
+ private fun ensureCursor() {
+ if (cursor == null) {
+ cursor = db.rawQueryWithFactory(
+ /* cursorFactory = */ { _, masterQuery, editTable, query ->
+ bindTo(query)
+ SQLiteCursor(masterQuery, editTable, query)
+ },
+ /* sql = */ sql,
+ /* selectionArgs = */ arrayOfNulls(0),
+ /* editTable = */ null
+ )
+ }
+ }
+
private fun bindTo(query: SQLiteProgram) {
for (index in 1 until bindingTypes.size) {
when (bindingTypes[index]) {
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
index 93bcbf8..23ab4ad 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
@@ -133,7 +133,7 @@
private fun DomainObjectCollection<ConfigurationVariant>.allNamed(
name: String,
action: Action<ConfigurationVariant>
-) = all { variant ->
+) = configureEach { variant ->
if (variant.name == name) {
action.execute(variant)
}
diff --git a/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt b/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
index 1c8f7e4..c563898 100644
--- a/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
+++ b/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
@@ -26,4 +26,4 @@
-uJava_androidx_test_ext_junitgtest_GtestRunner_initialize
-uJava_androidx_test_ext_junitgtest_GtestRunner_run
-uJava_androidx_test_ext_junitgtest_GtestRunner_addTest
- )
\ No newline at end of file
+ )
diff --git a/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt b/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
index f88adb0..cd8a95e 100644
--- a/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
+++ b/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
@@ -21,4 +21,12 @@
adder
googletest::gtest
junit-gtest::junit-gtest
- )
\ No newline at end of file
+ )
+target_link_options(adder
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
+target_link_options(apptest
+ PRIVATE
+ "-Wl,-z,max-page-size=16384"
+)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 742176d..6366d96 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -25,7 +25,9 @@
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -129,12 +131,20 @@
// Record the start time
final long startTime = SystemClock.uptimeMillis();
- // Update motion event delay to twice of the display refresh rate
+ // Update motion event delay to twice of the maximum display refresh rate
long injectionDelay = MOTION_EVENT_INJECTION_DELAY_MILLIS;
try {
int displayId = pending.peek().displayId();
Display display = mDevice.getDisplayById(displayId);
- float displayRefreshRate = display.getRefreshRate();
+ float displayRefreshRate;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ float[] refreshRates = Api21Impl.getSupportedRefreshRates(display);
+ Arrays.sort(refreshRates);
+ displayRefreshRate = refreshRates[refreshRates.length - 1];
+ } else {
+ // Set to current refresh rate if API version is lower than 21.
+ displayRefreshRate = display.getRefreshRate();
+ }
injectionDelay = (long) (500 / displayRefreshRate);
} catch (Exception e) {
Log.e(TAG, "Fail to update motion event delay", e);
@@ -295,4 +305,15 @@
UiDevice getDevice() {
return mDevice;
}
+
+ @RequiresApi(21)
+ private static class Api21Impl {
+ private Api21Impl() {
+ }
+
+ @DoNotInline
+ static float[] getSupportedRefreshRates(Display display) {
+ return display.getSupportedRefreshRates();
+ }
+ }
}
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt b/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
index 59111c1..4c98782 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
@@ -29,3 +29,4 @@
find_library(log-lib log)
target_link_libraries(tracing_perfetto ${android-lib} ${log-lib} perfetto ${CMAKE_THREAD_LIBS_INIT})
+target_link_options(tracing_perfetto PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 09e776f..d54bbb7 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -40,7 +40,7 @@
implementation(libs.kotlinStdlib)
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:ui:ui-util"))
- implementation(project(":lifecycle:lifecycle-runtime-compose"))
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation("androidx.core:core:1.12.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.0")
diff --git a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
similarity index 94%
rename from wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
rename to wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
index d900da3..31a9ecd 100644
--- a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
+++ b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
@@ -60,14 +60,14 @@
import org.junit.Rule
import org.junit.Test
-class ButtonTest {
+class RoundButtonTest {
@get:Rule
val rule = createComposeRule()
@Test
fun supports_testtag_on_button() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
) {
}
@@ -79,7 +79,7 @@
@Test
fun has_clickaction_when_enabled() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
enabled = true,
modifier = Modifier.testTag(TEST_TAG)
) {
@@ -92,7 +92,7 @@
@Test
fun has_clickaction_when_disabled() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
enabled = false,
modifier = Modifier.testTag(TEST_TAG)
) {
@@ -105,7 +105,7 @@
@Test
fun is_correctly_enabled() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
enabled = true,
modifier = Modifier.testTag(TEST_TAG)
) {
@@ -118,7 +118,7 @@
@Test
fun is_correctly_disabled() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
enabled = false,
modifier = Modifier.testTag(TEST_TAG)
) {
@@ -133,7 +133,7 @@
var clicked = false
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
onClick = { clicked = true },
enabled = true,
modifier = Modifier.testTag(TEST_TAG)
@@ -153,7 +153,7 @@
var clicked = false
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
onClick = { clicked = true },
enabled = false,
modifier = Modifier.testTag(TEST_TAG)
@@ -171,7 +171,7 @@
@Test
fun has_role_button_for_button() {
rule.setContent {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier.testTag(TEST_TAG)
) {
}
@@ -189,7 +189,7 @@
@Test
fun supports_circleshape_under_ltr_for_button() =
rule.isShape(CircleShape, LayoutDirection.Ltr) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
) {
}
@@ -198,7 +198,7 @@
@Test
fun supports_circleshape_under_rtl_for_button() =
rule.isShape(CircleShape, LayoutDirection.Rtl) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
) {
}
@@ -207,7 +207,7 @@
@Test
fun extra_small_button_meets_accessibility_tapsize() {
verifyTapSize(48.dp) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier
.testTag(TEST_TAG)
.size(32.dp)
@@ -219,7 +219,7 @@
@Test
fun extra_small_button_has_correct_visible_size() {
verifyVisibleSize(32.dp) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier
.testTag(TEST_TAG)
.requiredSize(32.dp)
@@ -232,7 +232,7 @@
fun default_button_has_correct_tapsize() {
// Tap size for Button should be the min button size.
verifyTapSize(52.dp) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier
.testTag(TEST_TAG)
) {
@@ -244,7 +244,7 @@
fun default_button_has_correct_visible_size() {
// Tap size for Button should be the min button size.
verifyVisibleSize(52.dp) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
modifier = Modifier
.testTag(TEST_TAG)
.size(52.dp)
@@ -258,7 +258,7 @@
val shape = CutCornerShape(4.dp)
rule.isShape(shape, LayoutDirection.Ltr) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
shape = shape,
modifier = Modifier.testTag(TEST_TAG)
) {
@@ -298,7 +298,7 @@
rule.setContent {
Box(modifier = Modifier.fillMaxSize()) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
content = {
CompositionLocalProvider(
LocalContentTestData provides EXPECTED_LOCAL_TEST_DATA
@@ -339,7 +339,7 @@
.fillMaxSize()
.background(testBackground)
) {
- ButtonWithDefaults(
+ RoundButtonWithDefaults(
backgroundColor = { enabled ->
if (enabled) enabledBackgroundColor else disabledBackgroundColor
},
@@ -388,7 +388,7 @@
}
@Composable
- internal fun ButtonWithDefaults(
+ internal fun RoundButtonWithDefaults(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
enabled: Boolean = true,
@@ -397,7 +397,7 @@
shape: Shape = CircleShape,
border: @Composable (enabled: Boolean) -> BorderStroke? = { null },
content: @Composable BoxScope.() -> Unit
- ) = Button(
+ ) = RoundButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
similarity index 91%
rename from wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt
rename to wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
index c651af3..86a7f66 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
@@ -38,9 +38,10 @@
import androidx.compose.ui.unit.Dp
/**
- * Wear Material [Button] that offers a single slot to take any content (text, icon or image).
+ * Wear Material [RoundButton] that offers a single slot to take any content (text, icon or image)
+ * and is round/circular in shape.
*
- * [Button] can be enabled or disabled. A disabled button will not respond to click events.
+ * [RoundButton] can be enabled or disabled. A disabled button will not respond to click events.
*
* For more information, see the
* [Buttons](https://developer.android.com/training/wearables/components/buttons)
@@ -59,11 +60,11 @@
* @param border Resolves the border for this button in different states.
* @param buttonSize The default size of the button unless overridden by Modifier.size.
* @param ripple Ripple used for this button.
- * @param content The content displayed on the [Button] such as text, icon or image.
+ * @param content The content displayed on the [RoundButton] such as text, icon or image.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
-fun Button(
+fun RoundButton(
onClick: () -> Unit,
modifier: Modifier,
enabled: Boolean,
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index b7f87f3..9073f97 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -44,7 +44,7 @@
implementation(project(":compose:ui:ui-util"))
implementation(project(":wear:compose:compose-material-core"))
implementation("androidx.profileinstaller:profileinstaller:1.3.0")
- implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-common:2.7.0")
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
index da69959..67e5b3c0 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
@@ -154,7 +154,7 @@
border: ButtonBorder = ButtonDefaults.buttonBorder(),
content: @Composable BoxScope.() -> Unit,
) {
- androidx.wear.compose.materialcore.Button(
+ androidx.wear.compose.materialcore.RoundButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
@@ -335,7 +335,7 @@
border: ButtonBorder = ButtonDefaults.buttonBorder(),
content: @Composable BoxScope.() -> Unit,
) {
- androidx.wear.compose.materialcore.Button(
+ androidx.wear.compose.materialcore.RoundButton(
onClick = onClick,
modifier = modifier
.padding(backgroundPadding)
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
index 994e12c..a7c7a0c 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
@@ -17,8 +17,10 @@
package androidx.wear.compose.material
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
@@ -33,7 +35,7 @@
/**
* A slot based composable for creating a list header item. List header items are typically expected
* to be text. The contents provided will have text and colors effects applied based on the
- * MaterialTheme. The contents will be start and end padded.
+ * MaterialTheme. The contents will be start and end padded and should cover up to 3 lines of text.
*
* Example usage:
* @sample androidx.wear.compose.material.samples.ScalingLazyColumnWithHeaders
@@ -50,7 +52,9 @@
content: @Composable RowScope.() -> Unit
) {
Row(
- modifier = modifier.height(48.dp)
+ modifier = modifier
+ .defaultMinSize(minHeight = 48.dp)
+ .height(IntrinsicSize.Min)
.wrapContentSize()
.background(backgroundColor)
.padding(horizontal = 14.dp)
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index f4f6ce2..d480645 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -27,8 +27,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors buttonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method public float getButtonHorizontalPadding();
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index f4f6ce2..d480645 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -27,8 +27,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors buttonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method public float getButtonHorizontalPadding();
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index b7d60ac..fa15794 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -435,7 +435,7 @@
@Composable
private fun MultilineButton(
enabled: Boolean,
- colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
icon: (@Composable BoxScope.() -> Unit)? = null,
label: @Composable RowScope.() -> Unit = {
Text(
@@ -457,7 +457,7 @@
@Composable
private fun Multiline3SlotButton(
enabled: Boolean,
- colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
icon: (@Composable BoxScope.() -> Unit)? = null,
label: @Composable RowScope.() -> Unit = {
Text(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index 5042316..ed0430c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -474,7 +474,7 @@
fun gives_enabled_three_slot_button_correct_colors() {
rule.verifyThreeSlotButtonColors(
status = Status.Enabled,
- expectedColor = { ButtonDefaults.filledButtonColors() },
+ expectedColor = { ButtonDefaults.buttonColors() },
content = { ThreeSlotFilledButton(Status.Enabled) }
)
}
@@ -484,7 +484,7 @@
fun gives_disabled_three_slot_button_correct_colors() {
rule.verifyThreeSlotButtonColors(
status = Status.Disabled,
- expectedColor = { ButtonDefaults.filledButtonColors() },
+ expectedColor = { ButtonDefaults.buttonColors() },
content = { ThreeSlotFilledButton(Status.Disabled) }
)
}
@@ -756,7 +756,7 @@
fun gives_enabled_compact_button_correct_colors() {
rule.verifyCompactButtonColors(
status = Status.Enabled,
- colors = { ButtonDefaults.filledButtonColors() }
+ colors = { ButtonDefaults.buttonColors() }
)
}
@@ -765,7 +765,7 @@
fun gives_disabled_compact_button_correct_colors() {
rule.verifyCompactButtonColors(
status = Status.Disabled,
- colors = { ButtonDefaults.filledButtonColors() }
+ colors = { ButtonDefaults.buttonColors() }
)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 695c72e..daf7691 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -73,7 +73,7 @@
* extend to a maximum of 3 lines in which case, the [Button] height adjusts to accommodate the
* contents.
*
- * [Button] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [Button] takes the [ButtonDefaults.buttonColors] color scheme by default,
* with colored background, contrasting content color and no border. This is a high-emphasis button
* for the primary, most important or most common action on a screen.
*
@@ -97,7 +97,7 @@
* @param shape Defines the button's shape. It is strongly recommended to use the default as this
* shape is a key characteristic of the Wear Material3 Theme
* @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonDefaults.filledButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
* @param border Optional [BorderStroke] that will be used to resolve the border for this
* button in different states.
* @param contentPadding The spacing values to apply internally between the container and the
@@ -113,7 +113,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = FilledButtonTokens.ContainerShape.value,
- colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource? = null,
@@ -148,7 +148,7 @@
* unblocking actions in a flow with less emphasis than [Button].
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
* [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -217,7 +217,7 @@
* for important, non-primary actions that need attention.
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
* [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -286,7 +286,7 @@
* or supplementary actions with the least amount of prominence.
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
* [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -354,7 +354,7 @@
* If a icon is provided then the labels should be "start" aligned, e.g. left aligned in ltr so that
* the text starts next to the icon.
*
- * [Button] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [Button] takes the [ButtonDefaults.buttonColors] color scheme by default,
* with colored background, contrasting content color and no border. This is a high-emphasis button
* for the primary, most important or most common action on a screen.
*
@@ -386,7 +386,7 @@
* shape is a key characteristic of the Wear Material3 Theme
* @param colors [ButtonColors] that will be used to resolve the background and content color for
* this button in different states. See [ButtonDefaults.buttonColors]. Defaults to
- * [ButtonDefaults.filledButtonColors]
+ * [ButtonDefaults.buttonColors]
* @param border Optional [BorderStroke] that will be used to resolve the button border in
* different states.
* @param contentPadding The spacing values to apply internally between the container and the
@@ -406,7 +406,7 @@
icon: (@Composable BoxScope.() -> Unit)? = null,
enabled: Boolean = true,
shape: Shape = FilledButtonTokens.ContainerShape.value,
- colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource? = null,
@@ -451,7 +451,7 @@
* unblocking actions in a flow with less emphasis than [Button].
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
* [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -539,7 +539,7 @@
* for important, non-primary actions that need attention.
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
* [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -625,7 +625,7 @@
* or supplementary actions with the least amount of prominence.
*
* Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
* [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
* [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors].
* Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -719,7 +719,7 @@
* If neither icon nor label is provided then the button will displayed like an icon only button but
* with no contents or background color.
*
- * [CompactButton] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [CompactButton] takes the [ButtonDefaults.buttonColors] color scheme by default,
* with colored background, contrasting content color and no border. This is a high-emphasis button
* for the primary, most important or most common action on a screen.
*
@@ -752,7 +752,7 @@
* horizontally and vertically aligned icon of size [ButtonDefaults.SmallIconSize] when used
* with a label or [ButtonDefaults.IconSize] when used as the only content in the button.
* @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonDefaults.filledButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
* @param enabled Controls the enabled state of the button. When `false`, this button will not
* be clickable
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
@@ -773,7 +773,7 @@
icon: (@Composable BoxScope.() -> Unit)? = null,
enabled: Boolean = true,
shape: Shape = CompactButtonTokens.ContainerShape.value,
- colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.CompactButtonContentPadding,
interactionSource: MutableInteractionSource? = null,
@@ -829,63 +829,13 @@
* Contains the default values used by [Button]
*/
object ButtonDefaults {
- /**
- * Creates a [ButtonColors] with colored background and contrasting content color,
- * the defaults for high emphasis buttons like [Button], for the primary, most important
- * or most common action on a screen.
- *
- * If a button is disabled then the content will have an alpha([DisabledContentAlpha]) value
- * applied and container will have an alpha([DisabledContainerAlpha]) value applied.
- */
- @Composable
- fun filledButtonColors() = MaterialTheme.colorScheme.defaultFilledButtonColors
-
- /**
- * Creates a [ButtonColors] with colored background and contrasting content color,
- * the defaults for high emphasis buttons like [Button], for the primary, most important
- * or most common action on a screen.
- *
- * If a button is disabled then the content will have an alpha([DisabledContentAlpha]) value
- * applied and container will have an alpha([DisabledContainerAlpha]) value applied.
- *
- * @param containerColor The background color of this [Button] when enabled
- * @param contentColor The content color of this [Button] when enabled
- * @param secondaryContentColor The secondary content color of this [Button] when enabled, used
- * for secondaryLabel content
- * @param iconColor The icon color of this [Button] when enabled, used for icon content
- * @param disabledContainerColor The background color of this [Button] when not enabled
- * @param disabledContentColor The content color of this [Button] when not enabled
- * @param disabledSecondaryContentColor The secondary content color of this [Button] when not
- * enabled
- * @param disabledIconColor The content color of this [Button] when not enabled
- */
- @Composable
- fun filledButtonColors(
- containerColor: Color = Color.Unspecified,
- contentColor: Color = Color.Unspecified,
- secondaryContentColor: Color = Color.Unspecified,
- iconColor: Color = Color.Unspecified,
- disabledContainerColor: Color = Color.Unspecified,
- disabledContentColor: Color = Color.Unspecified,
- disabledSecondaryContentColor: Color = Color.Unspecified,
- disabledIconColor: Color = Color.Unspecified
- ): ButtonColors = MaterialTheme.colorScheme.defaultFilledButtonColors.copy(
- containerColor = containerColor,
- contentColor = contentColor,
- secondaryContentColor = secondaryContentColor,
- iconColor = iconColor,
- disabledContainerColor = disabledContainerColor,
- disabledContentColor = disabledContentColor,
- disabledSecondaryContentColor = disabledSecondaryContentColor,
- disabledIconColor = disabledIconColor
- )
/**
* Creates a [ButtonColors] with a muted background and contrasting content color,
* the defaults for medium emphasis buttons like [FilledTonalButton].
* Use [filledTonalButtonColors] for important actions that don't distract from
* other onscreen elements, such as final or unblocking actions in a flow with less emphasis
- * than [filledButtonColors].
+ * than [buttonColors].
*
* If a button is disabled then the content will have an alpha([DisabledContentAlpha])
* value applied and container will have alpha ([DisabledContainerAlpha]) value applied.
@@ -898,7 +848,7 @@
* the defaults for medium emphasis buttons like [FilledTonalButton].
* Use [filledTonalButtonColors] for important actions that don't distract from
* other onscreen elements, such as final or unblocking actions in a flow with less emphasis
- * than [filledButtonColors].
+ * than [buttonColors].
*
* If a button is disabled then the content will have an alpha([DisabledContentAlpha])
* value applied and container will have alpha ([DisabledContainerAlpha]) value applied.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
index 6f1c147..cf82fd0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
@@ -83,7 +83,7 @@
interactionSource: MutableInteractionSource? = null,
content: @Composable BoxScope.() -> Unit,
) {
- androidx.wear.compose.materialcore.Button(
+ androidx.wear.compose.materialcore.RoundButton(
onClick = onClick,
modifier.minimumInteractiveComponentSize(),
enabled = enabled,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
index 50afcca..9a3571b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
@@ -89,7 +89,7 @@
interactionSource: MutableInteractionSource? = null,
content: @Composable BoxScope.() -> Unit,
) {
- androidx.wear.compose.materialcore.Button(
+ androidx.wear.compose.materialcore.RoundButton(
onClick = onClick,
modifier.minimumInteractiveComponentSize(),
enabled = enabled,
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 02f0362..3f37907 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -37,7 +37,7 @@
api("androidx.navigation:navigation-runtime:2.6.0")
api(project(":wear:compose:compose-material"))
api("androidx.activity:activity-compose:1.7.0")
- api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation(libs.kotlinStdlib)
implementation("androidx.navigation:navigation-common:2.6.0")
@@ -51,7 +51,7 @@
androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
androidTestImplementation(libs.truth)
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.0")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.7.0")
androidTestImplementation("androidx.navigation:navigation-testing:2.6.0")
}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 24f6946..c2f20c3 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -69,9 +69,9 @@
androidTestImplementation(project(":activity:activity-compose"))
androidTestImplementation(project(":activity:activity-ktx"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
- androidTestImplementation(project(":lifecycle:lifecycle-common"))
- androidTestImplementation(project(":lifecycle:lifecycle-runtime-ktx"))
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.7.0")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.7.0")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index ba48fd0..d102123 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -30,6 +30,9 @@
import androidx.wear.compose.integration.demos.common.Centralize
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
import androidx.wear.compose.material.samples.AlertDialogSample
import androidx.wear.compose.material.samples.AlertWithButtons
import androidx.wear.compose.material.samples.AlertWithChips
@@ -675,5 +678,28 @@
)
),
ComposableDemo("Settings Demo") { SettingsDemo() },
+ DemoCategory(
+ "ListHeader",
+ listOf(
+ ComposableDemo("Sample") {
+ Centralize {
+ ListHeader {
+ Text("Header", maxLines = 3)
+ }
+ }
+ },
+ ComposableDemo("MultiLine Sample") {
+ Centralize {
+ ListHeader {
+ Text(
+ text = "ListHeader that spans multiple lines in a large " +
+ "font and should expand to fit the contents",
+ style = MaterialTheme.typography.title3
+ )
+ }
+ }
+ }
+ )
+ )
),
)
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index dc4e776..46a2548 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -53,7 +53,7 @@
implementation(project(":wear:compose:compose-material-samples"))
implementation(project(':wear:compose:compose-navigation'))
- androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.7.0")
// Uses project dependency to match collections/compose-runtime
androidTestImplementation api("androidx.annotation:annotation:1.7.0")
}
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
index 7e20791..e3b079e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/build.gradle
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -35,7 +35,7 @@
implementation("androidx.collection:collection:1.2.0")
implementation("androidx.core:core:1.7.0")
implementation("androidx.concurrent:concurrent-futures:1.1.0")
- implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+ implementation("androidx.annotation:annotation-experimental:1.4.0")
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
implementation(project(":wear:protolayout:protolayout-expression"))
diff --git a/wear/protolayout/protolayout-expression/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
index 312cabf..ed1d2f6 100644
--- a/wear/protolayout/protolayout-expression/build.gradle
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -32,7 +32,7 @@
annotationProcessor(libs.nullaway)
api("androidx.annotation:annotation:1.2.0")
- implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+ implementation("androidx.annotation:annotation-experimental:1.4.0")
implementation("androidx.collection:collection:1.2.0")
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
diff --git a/wear/protolayout/protolayout-material/api/current.txt b/wear/protolayout/protolayout-material/api/current.txt
index 01b19f2..7e89155 100644
--- a/wear/protolayout/protolayout-material/api/current.txt
+++ b/wear/protolayout/protolayout-material/api/current.txt
@@ -107,6 +107,7 @@
method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getProgress();
method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getStartAngle();
method public androidx.wear.protolayout.DimensionBuilders.DpProp getStrokeWidth();
+ method public boolean isOuterMarginApplied();
}
public static final class CircularProgressIndicator.Builder {
@@ -116,6 +117,7 @@
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setEndAngle(float);
+ method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setOuterMarginApplied(boolean);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(androidx.wear.protolayout.TypeBuilders.FloatProp);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setStartAngle(float);
diff --git a/wear/protolayout/protolayout-material/api/restricted_current.txt b/wear/protolayout/protolayout-material/api/restricted_current.txt
index 01b19f2..7e89155 100644
--- a/wear/protolayout/protolayout-material/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material/api/restricted_current.txt
@@ -107,6 +107,7 @@
method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getProgress();
method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getStartAngle();
method public androidx.wear.protolayout.DimensionBuilders.DpProp getStrokeWidth();
+ method public boolean isOuterMarginApplied();
}
public static final class CircularProgressIndicator.Builder {
@@ -116,6 +117,7 @@
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setEndAngle(float);
+ method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setOuterMarginApplied(boolean);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(androidx.wear.protolayout.TypeBuilders.FloatProp);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setStartAngle(float);
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index d2acf88..8215bb4 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -36,7 +36,7 @@
implementation(project(":wear:protolayout:protolayout-material-core"))
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
- implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+ implementation("androidx.annotation:annotation-experimental:1.4.0")
androidTestImplementation(libs.junit)
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 6c6c8d6..1d7ce0b 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -312,6 +312,26 @@
new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
.build());
testCases.put(
+ "circularprogressindicator_in_smaller_box_with_margins_full_90",
+ new Box.Builder()
+ .setWidth(dp(50))
+ .setHeight(dp(50))
+ .addContent(
+ new CircularProgressIndicator.Builder().setProgress(0.25f).build())
+ .build());
+ testCases.put(
+ "circularprogressindicator_in_smaller_box_without_margins_full_90",
+ new Box.Builder()
+ .setWidth(dp(50))
+ .setHeight(dp(50))
+ .addContent(
+ new CircularProgressIndicator.Builder()
+ .setProgress(0.25f)
+ .setOuterMarginApplied(false)
+ .build())
+ .build());
+
+ testCases.put(
"default_text_golden" + goldenSuffix, new Text.Builder(context, "Testing").build());
testCases.put(
"not_scaled_text_golden" + NORMAL_SCALE_SUFFIX,
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
index 76ef0f5..f0865e4 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
@@ -117,6 +117,7 @@
@NonNull private DegreesProp mStartAngle = degrees(DEFAULT_START_ANGLE);
@NonNull private DegreesProp mEndAngle = degrees(DEFAULT_END_ANGLE);
@NonNull private FloatProp mProgress = staticFloat(0f);
+ private boolean mIsMarginApplied = true;
/** Creates a builder for the {@link CircularProgressIndicator}. */
public Builder() {}
@@ -231,6 +232,24 @@
}
/**
+ * Sets whether this {@link CircularProgressIndicator} should have outer margin or not.
+ *
+ * <p>If this indicator is used as a smaller element, use this method to remove an
+ * additional margin around it by setting it to {@code false}.
+ *
+ * <p>Otherwise, if this indicator is used as a full screen one or in {@link
+ * androidx.wear.protolayout.material.layouts.EdgeContentLayout2}, it's strongly recommended
+ * to set this to {@code true}.
+ *
+ * <p>If not set, defaults to true.
+ */
+ @NonNull
+ public Builder setOuterMarginApplied(boolean isApplied) {
+ this.mIsMarginApplied = isApplied;
+ return this;
+ }
+
+ /**
* Constructs and returns {@link CircularProgressIndicator} with the provided field and
* look.
*/
@@ -242,16 +261,16 @@
DegreesProp length = getLength();
Modifiers.Builder modifiers =
new Modifiers.Builder()
- .setPadding(
- new Padding.Builder()
- .setRtlAware(true)
- .setAll(DEFAULT_PADDING)
- .build())
.setMetadata(
new ElementMetadata.Builder()
.setTagData(getTagBytes(METADATA_TAG))
.build());
+ if (mIsMarginApplied) {
+ modifiers.setPadding(
+ new Padding.Builder().setRtlAware(true).setAll(DEFAULT_PADDING).build());
+ }
+
if (mContentDescription != null) {
modifiers.setSemantics(
new Semantics.Builder().setContentDescription(mContentDescription).build());
@@ -385,6 +404,12 @@
checkNotNull(checkNotNull(mElement.getModifiers()).getMetadata()));
}
+ /** Returns whether there is a margin around this indicator or not. */
+ public boolean isOuterMarginApplied() {
+ return this.mElement.getModifiers() != null
+ && this.mElement.getModifiers().getPadding() != null;
+ }
+
/**
* Returns CircularProgressIndicator object from the given LayoutElement (e.g. one retrieved
* from a container's content with {@code container.getContents().get(index)}) if that element
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
index 45c7f18..ce4cd40 100644
--- a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
@@ -97,6 +97,7 @@
.setCircularProgressIndicatorColors(colors)
.setStrokeWidth(thickness)
.setContentDescription(contentDescription)
+ .setOuterMarginApplied(false)
.build();
assertProgressIndicator(
@@ -107,6 +108,7 @@
colors,
thickness,
contentDescription);
+ assertThat(circularProgressIndicator.isOuterMarginApplied()).isFalse();
}
@Test
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
index 2515546..1cae83e2 100644
--- a/wear/protolayout/protolayout/build.gradle
+++ b/wear/protolayout/protolayout/build.gradle
@@ -33,7 +33,7 @@
api("androidx.annotation:annotation:1.2.0")
api(project(":wear:protolayout:protolayout-expression"))
- implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+ implementation("androidx.annotation:annotation-experimental:1.4.0")
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
lintChecks(project(":wear:protolayout:protolayout-lint"))
diff --git a/wear/tiles/tiles/build.gradle b/wear/tiles/tiles/build.gradle
index f10a1a8..cb9890d 100644
--- a/wear/tiles/tiles/build.gradle
+++ b/wear/tiles/tiles/build.gradle
@@ -35,7 +35,7 @@
api(project(":wear:protolayout:protolayout-expression"))
api(libs.guavaListenableFuture)
- implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+ implementation("androidx.annotation:annotation-experimental:1.4.0")
implementation("androidx.concurrent:concurrent-futures:1.1.0")
implementation(project(path: ":wear:tiles:tiles-proto"))
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
index e0c1ef1..3717584 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
@@ -75,8 +75,9 @@
* RECEIVE_COMPLICATION_DATA permission has been granted.
*
* This complication data source is only guaranteed to support [ComplicationType.SHORT_TEXT]
- * although it's a good idea for the slot to support [ComplicationType.SMALL_IMAGE] too
- * since OEMs may choose to serve a shortcut to their health app instead of the live value.
+ * and [ComplicationType.GOAL_PROGRESS], although it's a good idea for the slot to support
+ * [ComplicationType.SMALL_IMAGE] too since OEMs may choose to serve a shortcut to their
+ * health app instead of the live value.
*/
public const val DATA_SOURCE_STEP_COUNT: Int = 4
@@ -183,8 +184,7 @@
public const val DATA_SOURCE_DAY_AND_DATE: Int = 16
/**
- * Id for the 'heart rate' complication data source. Note implementations are free to return
- * a
+ * Id for the 'heart rate' complication data source.
*
* This complication data source is only guaranteed to support [ComplicationType.SHORT_TEXT]
* although it's a good idea for the slot to support [ComplicationType.SMALL_IMAGE] too
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
index ad4bf84..25d8a7a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
@@ -41,6 +41,7 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.wear.test.R;
import androidx.wear.widget.util.WakeLockRule;
@@ -186,6 +187,7 @@
});
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322537327
@Test
public void testCircularScrollingGesture() throws Throwable {
onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
index 7327b13..da68d8f 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
@@ -57,6 +57,7 @@
import androidx.test.espresso.util.TreeIterables;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.rule.ActivityTestRule;
import androidx.wear.test.R;
import androidx.wear.widget.drawer.DrawerTestActivity.DrawerStyle;
@@ -156,6 +157,7 @@
onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0")));
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322538394
@Test
public void selectingNavItemChangesTextAndClosedDrawer() {
// GIVEN an open top drawer
@@ -372,6 +374,7 @@
onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true)));
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322538394
@Test
public void actionDrawerPeekIconShouldNotBeNull() {
// GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML
@@ -384,6 +387,7 @@
assertNotNull(peekIconView.getDrawable());
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322538394
@Test
public void tappingActionDrawerPeekIconShouldTriggerFirstAction() {
// GIVEN a drawer layout with a peeking action drawer, title, and mock click listener
@@ -408,6 +412,7 @@
verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
}
+ @SdkSuppress(maxSdkVersion = 33) // b/322538394
@Test
public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() {
// GIVEN a drawer layout with an open action drawer with a title, and mock click listener
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 122f140..4a6200c 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -319,7 +319,7 @@
previousId = work.getStringId();
}
WorkerWrapper workerWrapper = createBuilder(firstWorkId).build();
- workerWrapper.setFailedAndResolve();
+ workerWrapper.setFailedAndResolve(new ListenableWorker.Result.Failure());
assertThat(mWorkSpecDao.getState(firstWorkId), is(FAILED));
assertThat(mWorkSpecDao.getState(previousId), is(FAILED));
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
index daf7ab1..2cbec8d 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
@@ -24,6 +24,7 @@
import androidx.work.Configuration
import androidx.work.Data
import androidx.work.ListenableWorker
+import androidx.work.ListenableWorker.Result.Failure
import androidx.work.Logger
import androidx.work.WorkInfo
import androidx.work.WorkerExceptionInfo
@@ -65,7 +66,6 @@
private var worker: ListenableWorker? = builder.worker
private val workTaskExecutor: TaskExecutor = builder.workTaskExecutor
- private var result = ListenableWorker.Result.failure()
private val configuration: Configuration = builder.configuration
private val clock: Clock = configuration.clock
private val foregroundProcessor: ForegroundProcessor = builder.foregroundProcessor
@@ -154,7 +154,7 @@
inputMergerFactory.createInputMergerWithDefaultFallback(inputMergerClassName)
if (inputMerger == null) {
loge(TAG) { "Could not create Input Merger ${workSpec.inputMergerClassName}" }
- setFailedAndResolve()
+ setFailedAndResolve(Failure())
return
}
val inputs = listOf(workSpec.input) + workSpecDao.getInputsFromPrerequisites(workSpecId)
@@ -194,7 +194,7 @@
"Exception handler threw an exception"
}
}
- setFailedAndResolve()
+ setFailedAndResolve(Failure())
return
}
worker.setUsed()
@@ -241,17 +241,19 @@
// Avoid synthetic accessors.
val workDescription = workDescription
workerResultFuture.addListener({
+ var result: ListenableWorker.Result = Failure()
try {
// If the ListenableWorker returns a null result treat it as a failure.
- val result = workerResultFuture.get()
- if (result == null) {
+ val futureResult = workerResultFuture.get()
+ result = if (futureResult == null) {
loge(TAG) {
workSpec.workerClassName +
" returned a null result. Treating it as a failure."
}
+ Failure()
} else {
- logd(TAG) { "${workSpec.workerClassName} returned a $result." }
- this.result = result
+ logd(TAG) { "${workSpec.workerClassName} returned a $futureResult." }
+ futureResult
}
} catch (exception: InterruptedException) {
loge(TAG, exception) {
@@ -259,7 +261,8 @@
}
try {
configuration.workerExecutionExceptionHandler?.accept(
- WorkerExceptionInfo(workSpec.workerClassName, params, exception))
+ WorkerExceptionInfo(workSpec.workerClassName, params, exception)
+ )
} catch (exception: Exception) {
loge(TAG, exception) {
"Exception handler threw an exception"
@@ -292,7 +295,7 @@
}
}
} finally {
- onWorkFinished()
+ onWorkFinished(result)
}
}, workTaskExecutor.getSerialTaskExecutor())
} else {
@@ -300,7 +303,7 @@
}
}
- private fun onWorkFinished() {
+ private fun onWorkFinished(result: ListenableWorker.Result) {
if (!tryCheckForInterruptionAndResolve()) {
workDatabase.runInTransaction {
val state = workSpecDao.getState(workSpecId)
@@ -403,13 +406,13 @@
_future.set(needsReschedule)
}
- private fun handleResult(result: ListenableWorker.Result) {
+ private fun handleResult(result: ListenableWorker.Result?) {
if (result is ListenableWorker.Result.Success) {
logi(TAG) { "Worker result SUCCESS for $workDescription" }
if (workSpec.isPeriodic) {
resetPeriodicAndResolve()
} else {
- setSucceededAndResolve()
+ setSucceededAndResolve(result)
}
} else if (result is ListenableWorker.Result.Retry) {
logi(TAG) { "Worker result RETRY for $workDescription" }
@@ -419,7 +422,8 @@
if (workSpec.isPeriodic) {
resetPeriodicAndResolve()
} else {
- setFailedAndResolve()
+ // we have here either failure or null
+ setFailedAndResolve(result ?: Failure())
}
}
}
@@ -437,10 +441,10 @@
)
@VisibleForTesting
- fun setFailedAndResolve() {
+ fun setFailedAndResolve(result: ListenableWorker.Result) {
resolve(false) {
iterativelyFailWorkAndDependents(workSpecId)
- val failure = result as ListenableWorker.Result.Failure
+ val failure = result as Failure
// Update Data as necessary.
val output = failure.outputData
workSpecDao.resetWorkSpecNextScheduleTimeOverride(
@@ -493,7 +497,7 @@
}
}
- private fun setSucceededAndResolve() {
+ private fun setSucceededAndResolve(result: ListenableWorker.Result) {
resolve(false) {
workSpecDao.setState(WorkInfo.State.SUCCEEDED, workSpecId)
val success = result as ListenableWorker.Result.Success