Merge "[KSP2] Fix enum tests" into androidx-main
diff --git a/binarycompatibilityvalidator/OWNERS b/binarycompatibilityvalidator/OWNERS
new file mode 100644
index 0000000..a43f7c1
--- /dev/null
+++ b/binarycompatibilityvalidator/OWNERS
@@ -0,0 +1,3 @@
+jeffrygaston@google.com
+aurimas@google.com
+fsladkey@google.com
\ No newline at end of file
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
new file mode 100644
index 0000000..aa18e365
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+/**
+ * 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 {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ implementation(libs.kotlinCompiler)
+ testImplementation(libs.truth)
+ testImplementation(libs.junit)
+}
+
+androidx {
+ name = "AndroidX Binary Compatibility Validator"
+ type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
+ inceptionYear = "2024"
+ description = "Enforce binary compatibility for klibs"
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt
new file mode 100644
index 0000000..871247d
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.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:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.AbiClassifierReference
+import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiType
+import org.jetbrains.kotlin.library.abi.AbiTypeArgument
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+
+// Convenience extensions for accessing properties that may exist without have to cast repeatedly
+// For sources with documentation see https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/LibraryAbi.kt
+
+/** A classifier reference is either a simple class or a type reference **/
+internal val AbiType.classifierReference: AbiClassifierReference?
+ get() = (this as? AbiType.Simple)?.classifierReference
+/** The class name from a regular type e.g. 'Array' **/
+internal val AbiType.className: AbiQualifiedName?
+ get() = classifierReference?.className
+/** A tag from a type type parameter reference e.g. 'T' **/
+internal val AbiType.tag: String?
+ get() = classifierReference?.tag
+/** The string representation of a type, whether it is a simple type or a type reference **/
+internal val AbiType.classNameOrTag: String?
+ get() = className?.toString() ?: tag
+internal val AbiType.nullability: AbiTypeNullability?
+ get() = (this as? AbiType.Simple)?.nullability
+internal val AbiType.arguments: List<AbiTypeArgument>?
+ get() = (this as? AbiType.Simple)?.arguments
+internal val AbiTypeArgument.type: AbiType?
+ get() = (this as? AbiTypeArgument.TypeProjection)?.type
+internal val AbiTypeArgument.variance: AbiVariance?
+ get() = (this as? AbiTypeArgument.TypeProjection)?.variance
+private val AbiClassifierReference.className: AbiQualifiedName?
+ get() = (this as? AbiClassifierReference.ClassReference)?.className
+private val AbiClassifierReference.tag: String?
+ get() = (this as? AbiClassifierReference.TypeParameterReference)?.tag
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
new file mode 100644
index 0000000..52f0e9b
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.binarycompatibilityvalidator
+
+class Cursor private constructor(
+ private val lines: List<String>,
+ private var rowIndex: Int = 0,
+ private var columnIndex: Int = 0
+) {
+ constructor(text: String) : this(text.split("\n"))
+ val currentLine: String
+ get() = lines[rowIndex].slice(columnIndex until lines[rowIndex].length)
+ fun hasNext() = rowIndex < (lines.size - 1)
+ fun nextLine() {
+ rowIndex++
+ columnIndex = 0
+ }
+
+ fun parseSymbol(
+ pattern: String,
+ peek: Boolean = false,
+ skipInlineWhitespace: Boolean = true
+ ): String? {
+ val match = Regex(pattern).find(currentLine)
+ return match?.value?.also {
+ if (!peek) {
+ val offset = it.length + currentLine.indexOf(it)
+ columnIndex += offset
+ if (skipInlineWhitespace) {
+ skipInlineWhitespace()
+ }
+ }
+ }
+ }
+
+ fun parseValidIdentifier(peek: Boolean = false): String? =
+ parseSymbol("^[a-zA-Z_][a-zA-Z0-9_]+", peek)
+
+ fun parseWord(peek: Boolean = false): String? = parseSymbol("[a-zA-Z]+", peek)
+
+ fun copy() = Cursor(lines, rowIndex, columnIndex)
+
+ internal fun skipInlineWhitespace() {
+ while (currentLine.firstOrNull()?.isWhitespace() == true) {
+ columnIndex++
+ }
+ }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
new file mode 100644
index 0000000..2afb33a
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
@@ -0,0 +1,396 @@
+/*
+ * 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.
+ */
+
+// Impl classes from kotlin.library.abi.impl are necessary to instantiate parsed declarations
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.AbiClassKind
+import org.jetbrains.kotlin.library.abi.AbiCompoundName
+import org.jetbrains.kotlin.library.abi.AbiModality
+import org.jetbrains.kotlin.library.abi.AbiPropertyKind
+import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiType
+import org.jetbrains.kotlin.library.abi.AbiTypeArgument
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiTypeParameter
+import org.jetbrains.kotlin.library.abi.AbiValueParameter
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.impl.AbiTypeParameterImpl
+import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl
+import org.jetbrains.kotlin.library.abi.impl.ClassReferenceImpl
+import org.jetbrains.kotlin.library.abi.impl.SimpleTypeImpl
+import org.jetbrains.kotlin.library.abi.impl.StarProjectionImpl
+import org.jetbrains.kotlin.library.abi.impl.TypeParameterReferenceImpl
+import org.jetbrains.kotlin.library.abi.impl.TypeProjectionImpl
+
+// This file contains Cursor methods specific to parsing klib dump files
+
+internal fun Cursor.parseAbiModality(): AbiModality? {
+ val parsed = parseAbiModalityString(peek = true)?.let {
+ AbiModality.valueOf(it)
+ }
+ if (parsed != null) {
+ parseAbiModalityString()
+ }
+ return parsed
+}
+
+internal fun Cursor.parseClassKind(peek: Boolean = false): AbiClassKind? {
+ val parsed = parseClassKindString(peek = true)?.let {
+ AbiClassKind.valueOf(it)
+ }
+ if (parsed != null && !peek) {
+ parseClassKindString()
+ }
+ return parsed
+}
+
+internal fun Cursor.parsePropertyKind(peek: Boolean = false): AbiPropertyKind? {
+ val parsed = parsePropertyKindString(peek = true)?.let {
+ AbiPropertyKind.valueOf(it)
+ }
+ if (parsed != null && !peek) {
+ parsePropertyKindString()
+ }
+ return parsed
+}
+
+internal fun Cursor.hasClassKind(): Boolean {
+ val subCursor = copy()
+ subCursor.skipInlineWhitespace()
+ subCursor.parseAbiModality()
+ subCursor.parseClassModifiers()
+ return subCursor.parseClassKind() != null
+}
+
+internal fun Cursor.hasFunctionKind(): Boolean {
+ val subCursor = copy()
+ subCursor.skipInlineWhitespace()
+ subCursor.parseAbiModality()
+ subCursor.parseFunctionModifiers()
+ return subCursor.parseFunctionKind() != null
+}
+
+internal fun Cursor.hasPropertyKind(): Boolean {
+ val subCursor = copy()
+ subCursor.skipInlineWhitespace()
+ subCursor.parseAbiModality()
+ return subCursor.parsePropertyKind() != null
+}
+
+internal fun Cursor.hasGetter() = hasPropertyAccessor(GetterOrSetter.GETTER)
+internal fun Cursor.hasSetter() = hasPropertyAccessor(GetterOrSetter.SETTER)
+
+internal fun Cursor.hasGetterOrSetter() = hasGetter() || hasSetter()
+
+internal fun Cursor.parseGetterName(peek: Boolean = false): String? {
+ val cursor = subCursor(peek)
+ cursor.parseSymbol("^<get\\-") ?: return null
+ val name = cursor.parseValidIdentifier() ?: return null
+ cursor.parseSymbol("^>") ?: return null
+ return "<get-$name>"
+}
+
+internal fun Cursor.parseSetterName(peek: Boolean = false): String? {
+ val cursor = subCursor(peek)
+ cursor.parseSymbol("^<set\\-") ?: return null
+ val name = cursor.parseValidIdentifier() ?: return null
+ cursor.parseSymbol("^>") ?: return null
+ return "<set-$name>"
+}
+
+internal fun Cursor.parseGetterOrSetterName(peek: Boolean = false) =
+ parseGetterName(peek) ?: parseSetterName(peek)
+
+internal fun Cursor.parseClassModifier(peek: Boolean = false): String? =
+ parseSymbol("^(inner|value|fun|open|annotation|enum)", peek)
+
+internal fun Cursor.parseClassModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseClassModifier(peek = true) != null) {
+ modifiers.add(parseClassModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseFunctionKind(peek: Boolean = false) =
+ parseSymbol("^(constructor|fun)", peek)
+
+internal fun Cursor.parseFunctionModifier(peek: Boolean = false): String? =
+ parseSymbol("^(inline|suspend)", peek)
+
+internal fun Cursor.parseFunctionModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseFunctionModifier(peek = true) != null) {
+ modifiers.add(parseFunctionModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseAbiQualifiedName(peek: Boolean = false): AbiQualifiedName? {
+ val symbol = parseSymbol("^[a-zA-Z0-9\\.]+\\/[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?", peek)
+ ?: return null
+ val (packageName, relativeName) = symbol.split("/")
+ return AbiQualifiedName(
+ AbiCompoundName(packageName),
+ AbiCompoundName(relativeName)
+ )
+}
+
+internal fun Cursor.parseAbiType(peek: Boolean = false): AbiType? {
+ val cursor = subCursor(peek)
+ // A type will either be a qualified name (kotlin/Array) or a type reference (#A)
+ // try to parse a qualified name and a type reference if it doesn't exist
+ val abiQualifiedName = cursor.parseAbiQualifiedName()
+ ?: return cursor.parseTypeReference()
+ val typeArgs = cursor.parseTypeArgs() ?: emptyList()
+ val nullability = cursor.parseNullability(assumeNotNull = true)
+ return SimpleTypeImpl(
+ ClassReferenceImpl(abiQualifiedName),
+ arguments = typeArgs,
+ nullability = nullability
+ )
+}
+
+internal fun Cursor.parseTypeArgs(): List<AbiTypeArgument>? {
+ val typeArgsString = parseTypeParamsString() ?: return null
+ val subCursor = Cursor(typeArgsString)
+ subCursor.parseSymbol("<") ?: return null
+ val typeArgs = mutableListOf<AbiTypeArgument>()
+ while (subCursor.parseTypeArg(peek = true) != null) {
+ typeArgs.add(subCursor.parseTypeArg()!!)
+ subCursor.parseSymbol(",")
+ }
+ return typeArgs
+}
+
+internal fun Cursor.parseTypeArg(peek: Boolean = false): AbiTypeArgument? {
+ val cursor = subCursor(peek)
+ val variance = cursor.parseAbiVariance()
+ cursor.parseSymbol("\\*")?.let {
+ return StarProjectionImpl
+ }
+ val type = cursor.parseAbiType(peek) ?: return null
+ return TypeProjectionImpl(
+ type = type,
+ variance = variance
+ )
+}
+
+internal fun Cursor.parseAbiVariance(): AbiVariance {
+ val variance = parseSymbol("^(out|in)") ?: return AbiVariance.INVARIANT
+ return AbiVariance.valueOf(variance.uppercase())
+}
+
+internal fun Cursor.parseTypeReference(): AbiType? {
+ val typeParamReference = parseTag() ?: return null
+ val typeArgs = parseTypeArgs() ?: emptyList()
+ val nullability = parseNullability()
+ return SimpleTypeImpl(
+ TypeParameterReferenceImpl(typeParamReference),
+ arguments = typeArgs,
+ nullability = nullability
+ )
+}
+
+internal fun Cursor.parseTag() = parseSymbol("^#[a-zA-Z0-9]+")?.removePrefix("#")
+
+internal fun Cursor.parseNullability(assumeNotNull: Boolean = false): AbiTypeNullability {
+ val nullable = parseSymbol("^\\?") != null
+ val definitelyNotNull = parseSymbol("^\\!\\!") != null
+ return when {
+ nullable -> AbiTypeNullability.MARKED_NULLABLE
+ definitelyNotNull -> AbiTypeNullability.DEFINITELY_NOT_NULL
+ else -> if (assumeNotNull) {
+ AbiTypeNullability.DEFINITELY_NOT_NULL
+ } else {
+ AbiTypeNullability.NOT_SPECIFIED
+ }
+ }
+}
+
+internal fun Cursor.parseSuperTypes(): MutableSet<AbiType> {
+ parseSymbol(":")
+ val superTypes = mutableSetOf<AbiType>()
+ while (parseAbiQualifiedName(peek = true) != null) {
+ superTypes.add(parseAbiType()!!)
+ parseSymbol(",")
+ }
+ return superTypes
+}
+
+fun Cursor.parseTypeParams(peek: Boolean = false): List<AbiTypeParameter>? {
+ val typeParamsString = parseTypeParamsString(peek) ?: return null
+ val subCursor = Cursor(typeParamsString)
+ subCursor.parseSymbol("^<")
+ val typeParams = mutableListOf<AbiTypeParameter>()
+ while (subCursor.parseTypeParam(peek = true) != null) {
+ typeParams.add(subCursor.parseTypeParam()!!)
+ subCursor.parseSymbol("^,")
+ }
+ return typeParams
+}
+
+fun Cursor.parseTypeParam(peek: Boolean = false): AbiTypeParameter? {
+ val cursor = subCursor(peek)
+ val tag = cursor.parseTag() ?: return null
+ cursor.parseSymbol("^:")
+ val variance = cursor.parseAbiVariance()
+ val isReified = cursor.parseSymbol("reified") != null
+ val upperBounds = mutableListOf<AbiType>()
+ if (null != cursor.parseAbiType(peek = true)) {
+ upperBounds.add(cursor.parseAbiType()!!)
+ }
+
+ return AbiTypeParameterImpl(
+ tag = tag,
+ variance = variance,
+ isReified = isReified,
+ upperBounds = upperBounds
+ )
+}
+
+internal fun Cursor.parseValueParameters(): List<AbiValueParameter>? {
+ val valueParamString = parseValueParametersString() ?: return null
+ val subCursor = Cursor(valueParamString)
+ val valueParams = mutableListOf<AbiValueParameter>()
+ subCursor.parseSymbol("\\(")
+ while (null != subCursor.parseValueParameter(peek = true)) {
+ valueParams.add(subCursor.parseValueParameter()!!)
+ subCursor.parseSymbol("^,")
+ }
+ return valueParams
+}
+
+internal fun Cursor.parseValueParameter(peek: Boolean = false): AbiValueParameter? {
+ val cursor = subCursor(peek)
+ val modifiers = cursor.parseValueParameterModifiers()
+ val isNoInline = modifiers.contains("noinline")
+ val isCrossinline = modifiers.contains("crossinline")
+ val type = cursor.parseAbiType() ?: return null
+ val isVararg = cursor.parseVarargSymbol() != null
+ val hasDefaultArg = cursor.parseDefaultArg() != null
+ return AbiValueParameterImpl(
+ type = type,
+ isVararg = isVararg,
+ hasDefaultArg = hasDefaultArg,
+ isNoinline = isNoInline,
+ isCrossinline = isCrossinline
+ )
+}
+
+internal fun Cursor.parseValueParameterModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseValueParameterModifier(peek = true) != null) {
+ modifiers.add(parseValueParameterModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseValueParameterModifier(peek: Boolean = false): String? =
+ parseSymbol("^(crossinline|noinline)", peek)
+
+internal fun Cursor.parseVarargSymbol() = parseSymbol("^\\.\\.\\.")
+
+internal fun Cursor.parseDefaultArg() = parseSymbol("^=\\.\\.\\.")
+
+internal fun Cursor.parseFunctionReceiver(): AbiType? {
+ val string = parseFunctionReceiverString() ?: return null
+ val subCursor = Cursor(string)
+ subCursor.parseSymbol("\\(")
+ return subCursor.parseAbiType()
+}
+
+internal fun Cursor.parseReturnType(): AbiType? {
+ parseSymbol("^:\\s")
+ return parseAbiType()
+}
+
+internal fun Cursor.parseTargets(): List<String> {
+ parseSymbol("^Targets:")
+ parseSymbol("^\\[")
+ val targets = mutableListOf<String>()
+ while (parseValidIdentifier(peek = true) != null) {
+ targets.add(parseValidIdentifier()!!)
+ parseSymbol("^,")
+ }
+ parseSymbol("^\\]")
+ return targets
+}
+
+/**
+ * Used to check if declarations after a property are getter / setter methods which should be
+ * attached to that property.
+*/
+private fun Cursor.hasPropertyAccessor(type: GetterOrSetter): Boolean {
+ val subCursor = copy()
+ subCursor.parseAbiModality()
+ subCursor.parseFunctionModifiers()
+ subCursor.parseFunctionKind() ?: return false // if it's not a function it's not a getter/setter
+ val mightHaveTypeParams = subCursor.parseGetterOrSetterName(peek = true) == null
+ if (mightHaveTypeParams) {
+ subCursor.parseTypeParams()
+ }
+ subCursor.parseFunctionReceiver()
+ return when (type) {
+ GetterOrSetter.GETTER -> subCursor.parseGetterName() != null
+ GetterOrSetter.SETTER -> subCursor.parseSetterName() != null
+ }
+}
+
+private fun Cursor.subCursor(peek: Boolean) = if (peek) { copy() } else { this }
+
+private fun Cursor.parseTypeParamsString(peek: Boolean = false): String? {
+ val cursor = subCursor(peek)
+ val result = StringBuilder()
+ cursor.parseSymbol("^<")?.let { result.append(it) } ?: return null
+ var openBracketCount = 1
+ while (openBracketCount > 0) {
+ val nextSymbol = cursor.parseSymbol(".", skipInlineWhitespace = false).also {
+ result.append(it)
+ }
+ when (nextSymbol) {
+ "<" -> openBracketCount++
+ ">" -> openBracketCount--
+ }
+ }
+ cursor.skipInlineWhitespace()
+ return result.toString()
+}
+
+private fun Cursor.parseFunctionReceiverString() =
+ parseSymbol("^\\([a-zA-Z0-9,\\/<>,#\\.\\s]+?\\)\\.")
+
+private fun Cursor.parseValueParametersString() =
+ parseSymbol("^\\(([a-zA-Z0-9,\\/<>,#\\.\\s\\?=]+)?\\)")
+
+private fun Cursor.parseAbiModalityString(peek: Boolean = false) =
+ parseSymbol("^(final|open|abstract|sealed)", peek)?.uppercase()
+
+private fun Cursor.parsePropertyKindString(peek: Boolean = false) =
+ parseSymbol("^(const\\sval|val|var)", peek)?.uppercase()?.replace(" ", "_")
+
+private fun Cursor.parseClassKindString(peek: Boolean = false) =
+ parseSymbol("^(class|interface|object|enum_class|annotation_class)", peek)?.uppercase()
+
+private enum class GetterOrSetter() {
+ GETTER,
+ SETTER
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
new file mode 100644
index 0000000..6390ea9
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
@@ -0,0 +1,509 @@
+/*
+ * 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:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+import org.jetbrains.kotlin.library.abi.AbiClassKind
+import org.jetbrains.kotlin.library.abi.AbiModality
+import org.jetbrains.kotlin.library.abi.AbiPropertyKind
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiValueParameter
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.junit.Test
+
+class CursorExtensionsTest {
+
+ @Test
+ fun parseModalityFailure() {
+ val input = "something else"
+ val cursor = Cursor(input)
+ val modality = cursor.parseAbiModality()
+ assertThat(modality).isNull()
+ assertThat(cursor.currentLine).isEqualTo("something else")
+ }
+
+ @Test
+ fun parseModalitySuccess() {
+ val input = "final whatever"
+ val cursor = Cursor(input)
+ val modality = cursor.parseAbiModality()
+ assertThat(modality).isEqualTo(AbiModality.FINAL)
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseClassModifier() {
+ val input = "inner whatever"
+ val cursor = Cursor(input)
+ val modifier = cursor.parseClassModifier()
+ assertThat(modifier).isEqualTo("inner")
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseClassModifiers() {
+ val input = "inner value fun whatever"
+ val cursor = Cursor(input)
+ val modifiers = cursor.parseClassModifiers()
+ assertThat(modifiers).containsExactly("inner", "fun", "value")
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseFunctionModifiers() {
+ val input = "final inline suspend fun component1(): kotlin/Long"
+ val cursor = Cursor(input)
+ cursor.parseAbiModality()
+ val modifiers = cursor.parseFunctionModifiers()
+ assertThat(modifiers).containsExactly("inline", "suspend")
+ assertThat(cursor.currentLine).isEqualTo("fun component1(): kotlin/Long")
+ }
+
+ @Test
+ fun parseClassKindSimple() {
+ val input = "class"
+ val cursor = Cursor(input)
+ val kind = cursor.parseClassKind()
+ assertThat(kind).isEqualTo(AbiClassKind.CLASS)
+ }
+
+ @Test
+ fun parseClassKindFalsePositive() {
+ val input = "androidx.collection/objectFloatMap"
+ val cursor = Cursor(input)
+ val kind = cursor.parseClassKind()
+ assertThat(kind).isNull()
+ }
+
+ @Test
+ fun hasClassKind() {
+ val input = "final class my.lib/MyClass"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasClassKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test
+ fun parseFunctionKindSimple() {
+ val input = "fun hello"
+ val cursor = Cursor(input)
+ val kind = cursor.parseFunctionKind()
+ assertThat(kind).isEqualTo("fun")
+ assertThat(cursor.currentLine).isEqualTo(cursor.currentLine)
+ }
+
+ @Test fun hasFunctionKind() {
+ val input = " final fun myFun(): kotlin/String "
+ val cursor = Cursor(input)
+ assertThat(cursor.hasFunctionKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasFunctionKindConstructor() {
+ val input = " constructor <init>(kotlin/Int =...)"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasFunctionKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun parseGetterOrSetterName() {
+ val input = "<get-indices>()"
+ val cursor = Cursor(input)
+ val name = cursor.parseGetterOrSetterName()
+ assertThat(name).isEqualTo("<get-indices>")
+ assertThat(cursor.currentLine).isEqualTo("()")
+ }
+
+ @Test fun hasGetter() {
+ val input = "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasGetter()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasSetter() {
+ val input = "final inline fun <set-indices>(): kotlin.ranges/IntRange"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasSetter()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasGetterOrSetter() {
+ val inputs = listOf(
+ "final inline fun <set-indices>(): kotlin.ranges/IntRange",
+ "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+ )
+ inputs.forEach { input ->
+ assertThat(Cursor(input).hasGetterOrSetter()).isTrue()
+ }
+ }
+
+ @Test
+ fun hasPropertyKind() {
+ val input = "final const val my.lib/myProp"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasPropertyKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test
+ fun parsePropertyKindConstVal() {
+ val input = "const val something"
+ val cursor = Cursor(input)
+ val kind = cursor.parsePropertyKind()
+ assertThat(kind).isEqualTo(AbiPropertyKind.CONST_VAL)
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parsePropertyKindVal() {
+ val input = "val something"
+ val cursor = Cursor(input)
+ val kind = cursor.parsePropertyKind()
+ assertThat(kind).isEqualTo(AbiPropertyKind.VAL)
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parseNullability() {
+ val nullable = Cursor("?").parseNullability()
+ val notNull = Cursor("!!").parseNullability()
+ val unspecified = Cursor("another symbol").parseNullability()
+ assertThat(nullable).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(notNull).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+ assertThat(unspecified).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+ }
+
+ @Test fun parseNullabilityWhenAssumingNotNullable() {
+ val unspecified = Cursor("").parseNullability(assumeNotNull = true)
+ assertThat(unspecified).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+ }
+
+ @Test fun parseQualifiedName() {
+ val input = "androidx.collection/MutableScatterMap something"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test fun parseQualifiedNameKotlin() {
+ val input = "kotlin/Function2<#A1, #A, #A1>"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("kotlin/Function2")
+ assertThat(cursor.currentLine).isEqualTo("<#A1, #A, #A1>",)
+ }
+
+ @Test fun parseQualifie0dNameDoesNotGrabNullable() {
+ val input = "androidx.collection/MutableScatterMap? something"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+ assertThat(cursor.currentLine).isEqualTo("? something")
+ }
+
+ @Test
+ fun parseAbiType() {
+ val input = "androidx.collection/ScatterMap<#A, #B> something"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parseAbiTypeWithAnotherType() {
+ val input = "androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+ "something"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo(
+ ", androidx.collection/Other<#A, #B> something"
+ )
+ }
+
+ @Test fun parseAbiTypeWithThreeParams() {
+ val input = "kotlin/Function2<#A1, #A, #A1>"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Function2")
+ }
+
+ @Test
+ fun parseSuperTypes() {
+ val input = ": androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+ "something"
+ val cursor = Cursor(input)
+ val superTypes = cursor.parseSuperTypes().toList()
+ assertThat(superTypes).hasSize(2)
+ assertThat(superTypes.first().className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(superTypes.last().className?.toString()).isEqualTo(
+ "androidx.collection/Other"
+ )
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test fun parseReturnType() {
+ val input = ": androidx.collection/ScatterMap<#A, #B> stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test fun parseReturnTypeNullableWithTypeParamsNullable() {
+ val input = ": #B? stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.tag).isEqualTo("B")
+ assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test fun parseReturnTypeNullableWithTypeParamsNotSpecified() {
+ val input = ": #B stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.tag).isEqualTo("B")
+ assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test
+ fun parseFunctionReceiver() {
+ val input = "(androidx.collection/LongSparseArray<#A>).androidx.collection/keyIterator()"
+ val cursor = Cursor(input)
+ val receiver = cursor.parseFunctionReceiver()
+ assertThat(receiver?.className.toString()).isEqualTo(
+ "androidx.collection/LongSparseArray"
+ )
+ assertThat(cursor.currentLine).isEqualTo("androidx.collection/keyIterator()")
+ }
+
+ @Test
+ fun parseFunctionReceiver2() {
+ val input = "(androidx.collection/LongSparseArray<#A1>).<get-size>(): kotlin/Int"
+ val cursor = Cursor(input)
+ val receiver = cursor.parseFunctionReceiver()
+ assertThat(receiver?.className.toString()).isEqualTo(
+ "androidx.collection/LongSparseArray"
+ )
+ assertThat(cursor.currentLine).isEqualTo("<get-size>(): kotlin/Int")
+ }
+
+ @Test fun parseValueParamCrossinlineDefault() {
+ val input = "crossinline kotlin/Function2<#A, #B, kotlin/Int> =..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()!!
+ assertThat(
+ valueParam.type.className.toString()
+ ).isEqualTo("kotlin/Function2")
+ assertThat(valueParam.hasDefaultArg).isTrue()
+ assertThat(valueParam.isCrossinline).isTrue()
+ assertThat(valueParam.isVararg).isFalse()
+ }
+
+ @Test
+ fun parseValueParamVararg() {
+ val input = "kotlin/Array<out kotlin/Pair<#A, #B>>..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(
+ valueParam?.type?.className?.toString()
+ ).isEqualTo("kotlin/Array")
+ assertThat(valueParam?.hasDefaultArg).isFalse()
+ assertThat(valueParam?.isCrossinline).isFalse()
+ assertThat(valueParam?.isVararg).isTrue()
+ }
+
+ @Test fun parseValueParametersWithTypeArgs() {
+ val input = "kotlin/Array<out #A>..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(valueParam?.type?.arguments).hasSize(1)
+ }
+
+ @Test fun parseValueParametersWithTwoTypeArgs() {
+ val input = "kotlin/Function1<kotlin/Double, kotlin/Boolean>)"
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(valueParam?.type?.arguments).hasSize(2)
+ }
+
+ @Test fun parseValueParametersEmpty() {
+ val input = "() thing"
+ val cursor = Cursor(input)
+ val params = cursor.parseValueParameters()
+ assertThat(params).isEqualTo(emptyList<AbiValueParameter>())
+ assertThat(cursor.currentLine).isEqualTo("thing")
+ }
+
+ @Test fun parseValueParamsSimple() {
+ val input = "(kotlin/Function1<#A, kotlin/Boolean>)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(1)
+ }
+
+ @Test fun parseValueParamsTwoArgs() {
+ val input = "(#A1, kotlin/Function2<#A1, #A, #A1>)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(2)
+ assertThat(valueParams?.first()?.type?.tag).isEqualTo("A1")
+ }
+
+ @Test
+ fun parseValueParamsWithHasDefaultArg() {
+ val input = "(kotlin/Int =...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(1)
+ assertThat(valueParams?.single()?.hasDefaultArg).isTrue()
+ }
+
+ @Test
+ fun parseValueParamsComplex2() {
+ val input = "(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> =..., " +
+ "crossinline kotlin/Function1<#A, #B?> =..., " +
+ "crossinline kotlin/Function4<kotlin/Boolean, #A, #B, #B?, kotlin/Unit> =...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()!!
+ assertThat(valueParams).hasSize(4)
+ assertThat(valueParams.first().type.className?.toString()).isEqualTo("kotlin/Int")
+ val rest = valueParams.subList(1, valueParams.size)
+ assertThat(rest).hasSize(3)
+ assertThat(rest.all { it.hasDefaultArg }).isTrue()
+ assertThat(rest.all { it.isCrossinline }).isTrue()
+ }
+
+ @Test fun parseValueParamsComplex3() {
+ val input = "(kotlin/Array<out kotlin/Pair<#A, #B>>...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()!!
+ assertThat(valueParams).hasSize(1)
+
+ assertThat(valueParams.single().isVararg).isTrue()
+ val type = valueParams.single().type
+ assertThat(type.className.toString()).isEqualTo("kotlin/Array")
+ }
+
+ @Test fun parseTypeParams() {
+ val input = "<#A1: kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(1)
+ val type = typeParams?.single()?.upperBounds?.single()
+ assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.INVARIANT)
+ }
+
+ @Test fun parseTypeParamsWithVariance() {
+ val input = "<#A1: out kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(1)
+ val type = typeParams?.single()?.upperBounds?.single()
+ assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.OUT)
+ }
+
+ @Test fun parseTypeParamsWithTwo() {
+ val input = "<#A: kotlin/Any?, #B: kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(2)
+ val type1 = typeParams?.first()?.upperBounds?.single()
+ val type2 = typeParams?.first()?.upperBounds?.single()
+ assertThat(typeParams?.first()?.tag).isEqualTo("A")
+ assertThat(typeParams?.last()?.tag).isEqualTo("B")
+ assertThat(type1?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type1?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(type2?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type2?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ }
+
+ @Test fun parseTypeParamsReifed() {
+ val input = "<#A1: reified kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParam = cursor.parseTypeParams()?.single()
+ assertThat(typeParam).isNotNull()
+ assertThat(typeParam?.isReified).isTrue()
+ }
+
+ @Test
+ fun parseTypeArgs() {
+ val input = "<out #A>"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(1)
+ val typeArg = typeArgs?.single()
+ assertThat(typeArg?.type?.tag).isEqualTo("A")
+ assertThat(typeArg?.variance).isEqualTo(AbiVariance.OUT)
+ }
+
+ @Test
+ fun parseTwoTypeArgs() {
+ val input = "<kotlin/Double, kotlin/Boolean>"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(2)
+ assertThat(typeArgs?.first()?.type?.className?.toString()).isEqualTo("kotlin/Double")
+ assertThat(typeArgs?.last()?.type?.className?.toString()).isEqualTo("kotlin/Boolean")
+ }
+
+ @Test
+ fun parseTypeArgsWithNestedBrackets() {
+ val input = "<androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B>>," +
+ " something else"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(2)
+ assertThat(cursor.currentLine).isEqualTo(", something else")
+ }
+
+ @Test fun parseVarargSymbol() {
+ val input = "..."
+ val cursor = Cursor(input)
+ val vararg = cursor.parseVarargSymbol()
+ assertThat(vararg).isNotNull()
+ }
+
+ @Test fun parseTargets() {
+ val input = "Targets: [iosX64, linuxX64]"
+ val cursor = Cursor(input)
+ val targets = cursor.parseTargets()
+ assertThat(targets).containsExactly("linuxX64", "iosX64")
+ }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
new file mode 100644
index 0000000..f37f6c5
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CursorTest {
+
+ @Test
+ fun cursorShowsCurrentLine() {
+ val input = "one\ntwo\nthree"
+ val cursor = Cursor(input)
+ assertThat(cursor.currentLine).isEqualTo("one")
+ cursor.nextLine()
+ assertThat(cursor.currentLine).isEqualTo("two")
+ cursor.nextLine()
+ assertThat(cursor.currentLine).isEqualTo("three")
+ assertThat(cursor.hasNext()).isFalse()
+ }
+
+ @Test
+ fun cursorGetsNextWord() {
+ val input = "one two three"
+ val cursor = Cursor(input)
+ val word = cursor.parseWord()
+ assertThat(word).isEqualTo("one")
+ assertThat("two three").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierValid() {
+ val input = "oneTwo3 four"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isEqualTo("oneTwo3")
+ assertThat("four").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierValidStartsWithUnderscore() {
+ val input = "_one_Two3 four"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isEqualTo("_one_Two3")
+ assertThat("four").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierInvalid() {
+ val input = "1twothree"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isNull()
+ }
+
+ @Test
+ fun skipWhitespace() {
+ val input = " test"
+ val cursor = Cursor(input)
+ cursor.skipInlineWhitespace()
+ assertThat(cursor.currentLine).isEqualTo("test")
+ }
+
+ @Test
+ fun skipWhitespaceOnBlankLine() {
+ val input = ""
+ val cursor = Cursor(input)
+ cursor.skipInlineWhitespace()
+ assertThat(cursor.currentLine).isEqualTo("")
+ }
+}
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 5b3af52..4b77b51 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -57,7 +57,7 @@
implementation(project(":benchmark:benchmark-gradle-plugin"))
implementation(project(":inspection:inspection-gradle-plugin"))
implementation(project(":stableaidl:stableaidl-gradle-plugin"))
- implementation(findGradleKotlinDsl())
+ implementation(project.ext.findGradleKotlinDsl())
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation(project(":internal-testutils-gradle-plugin"))
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 46f896e..02be011e 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -300,155 +300,11 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" rootProject.findProperty(ENABLE_ARG) != "false""
- errorLine2=" ~~~~~~~~~~~~">
+ message="Use providers.gradleProperty instead of property"
+ errorLine1=" (this.rootProject.property("ext") as ExtraPropertiesExtension).set("
+ errorLine2=" ~~~~~~~~">
<location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" rootProject.findProperty(ENABLE_ARG) != "false""
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" override val compileSdk: Int by lazy { project.findProperty(COMPILE_SDK).toString().toInt() }"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty(COMPILE_SDK_EXTENSION) as Int?"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty(TARGET_SDK_VERSION).toString().toInt()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty(ALTERNATIVE_PROJECT_URL) as? String"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" return (project.findProperty(ENABLE_DOCUMENTATION) as? String)?.toBoolean() ?: true"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1="fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" return checkNotNull(findProperty(name)) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" parseTargetPlatformsFlag(project.findProperty(ENABLED_KMP_TARGET_PLATFORMS) as? String)"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/KmpPlatforms.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" findProperty(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val value = project.findProperty(STUDIO_TYPE)?.toString()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val value = project.findProperty(STUDIO_TYPE)?.toString()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val group = findProperty("group") as String"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" val artifactId = findProperty("name") as String"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
</issue>
<issue
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 51df347..120a7bb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -26,7 +26,6 @@
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.attributes.Attribute
-import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.tasks.bundling.Zip
import org.gradle.kotlin.dsl.create
import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
@@ -122,7 +121,7 @@
*/
private fun Project.configureForMultiplatform() {
// This is to allow K/N not matching the kotlinVersion
- (this.rootProject.property("ext") as ExtraPropertiesExtension).set(
+ this.rootProject.extensions.extraProperties.set(
"kotlin.native.version",
KOTLIN_NATIVE_VERSION
)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 69dde1f..aeab798 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -221,7 +221,7 @@
* Returns null if there is no alternative project url.
*/
fun Project.getAlternativeProjectUrl(): String? =
- project.findProperty(ALTERNATIVE_PROJECT_URL) as? String
+ project.providers.gradleProperty(ALTERNATIVE_PROJECT_URL).getOrNull()
/**
* Check that version extra meets the specified rules (version is in format major.minor.patch-extra)
@@ -277,18 +277,9 @@
fun Project.isWriteVersionedApiFilesEnabled(): Boolean =
findBooleanProperty(WRITE_VERSIONED_API_FILES) ?: true
-/** Returns whether the project should generate documentation. */
-fun Project.isDocumentationEnabled(): Boolean {
- if (System.getenv().containsKey("ANDROIDX_PROJECTS")) {
- val projects = System.getenv()["ANDROIDX_PROJECTS"] as String
- if (projects != "ALL") return false
- }
- return (project.findProperty(ENABLE_DOCUMENTATION) as? String)?.toBoolean() ?: true
-}
-
/** Returns whether the build is for checking forward compatibility across projects */
fun Project.usingMaxDepVersions(): Boolean {
- return project.hasProperty(USE_MAX_DEP_VERSIONS)
+ return project.providers.gradleProperty(USE_MAX_DEP_VERSIONS).isPresent()
}
/**
@@ -320,7 +311,7 @@
fun Project.isCustomCompileSdkAllowed(): Boolean =
findBooleanProperty(ALLOW_CUSTOM_COMPILE_SDK) ?: true
-fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()
+fun Project.findBooleanProperty(propName: String) = booleanPropertyProvider(propName).get()
fun Project.booleanPropertyProvider(propName: String): Provider<Boolean> {
return project.providers.gradleProperty(propName).map { s -> s.toBoolean() }.orElse(false)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
index 4503c7b..4bb9027 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
@@ -16,6 +16,7 @@
package androidx.build
+import androidx.build.gradle.extraPropertyOrNull
import androidx.build.gradle.isRoot
import groovy.xml.DOMBuilder
import java.net.URI
@@ -195,7 +196,7 @@
}
private fun Project.requireProperty(name: String): String {
- return checkNotNull(findProperty(name)) {
+ return checkNotNull(extraPropertyOrNull(name)) {
"missing $name property. It must be defined in the gradle.properties file"
}
.toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt b/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
index 5759c76d..3d041e1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
@@ -16,6 +16,7 @@
package androidx.build
+import androidx.build.gradle.extraPropertyOrNull
import java.io.File
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
@@ -83,7 +84,7 @@
private fun Project.overrideKotlinNativeDependenciesUrlToLocalDirectory() {
val compilerDaemonDisabled =
- findProperty(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true
+ extraPropertyOrNull(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true
val konanPrebuiltsFolder = getKonanPrebuiltsFolder()
val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir
// use relative path so it doesn't affect gradle remote cache.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
index 115050d..874ebf9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
@@ -105,8 +105,8 @@
androidXExtension: AndroidXExtension
) {
writeVersionFile.configure {
- val group = findProperty("group") as String
- val artifactId = findProperty("name") as String
+ val group = project.getGroup() as String
+ val artifactId = project.getName() as String
val version =
if (androidXExtension.shouldPublish()) {
version().toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 6307291..4dfd2de 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -110,9 +110,8 @@
val instance = AffectedModuleDetectorWrapper()
rootProject.extensions.add(ROOT_PROP_NAME, instance)
- val enabled =
- rootProject.hasProperty(ENABLE_ARG) &&
- rootProject.findProperty(ENABLE_ARG) != "false"
+ val enabledProvider = rootProject.providers.gradleProperty(ENABLE_ARG)
+ val enabled = enabledProvider.isPresent() && enabledProvider.get() != "false"
val distDir = rootProject.getDistributionDirectory()
val outputFile = distDir.resolve(LOG_FILE_NAME)
@@ -134,10 +133,9 @@
instance.wrapped = provider
return
}
- val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?
- if (baseCommitOverride != null) {
- logger.info("using base commit override $baseCommitOverride")
- }
+ val baseCommitOverride: Provider<String> =
+ rootProject.providers.gradleProperty(BASE_COMMIT_ARG)
+
gradle.taskGraph.whenReady {
logger.lifecycle("projects evaluated")
val projectGraph = ProjectGraph(rootProject)
@@ -257,7 +255,7 @@
var cobuiltTestPaths: Set<Set<String>>?
var alwaysBuildIfExists: Set<String>?
var ignoredPaths: Set<String>?
- var baseCommitOverride: String?
+ var baseCommitOverride: Provider<String>?
var gitChangedFilesProvider: Provider<List<String>>
}
@@ -266,10 +264,6 @@
if (parameters.acceptAll) {
AcceptAll(null)
} else {
- if (parameters.baseCommitOverride != null) {
- logger.info("using base commit override ${parameters.baseCommitOverride}")
- }
-
AffectedModuleDetectorImpl(
projectGraph = parameters.projectGraph,
dependencyTracker = parameters.dependencyTracker,
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 c73c126..b96c9d6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -27,6 +27,7 @@
import androidx.build.getDistributionDirectory
import androidx.build.getKeystore
import androidx.build.getLibraryByName
+import androidx.build.getSupportRootFolder
import androidx.build.metalava.versionMetadataUsage
import androidx.build.multiplatformUsage
import androidx.build.versionCatalog
@@ -521,7 +522,7 @@
var taskStartTime: LocalDateTime? = null
task.argsJsonFile.set(
File(
- project.rootProject.getDistributionDirectory(),
+ project.getDistributionDirectory(),
"dackkaArgs-${project.name}.json"
)
)
@@ -542,7 +543,7 @@
dackkaClasspath.from(project.files(dackkaConfiguration))
destinationDir.set(generatedDocsDir)
frameworkSamplesDir.set(
- project.rootProject.layout.projectDirectory.dir("samples")
+ File(project.getSupportRootFolder(), "samples")
)
samplesDeprecatedDir.set(unzippedDeprecatedSamplesSources)
samplesJvmDir.set(unzippedJvmSamplesSources)
@@ -550,7 +551,7 @@
jvmSourcesDir.set(unzippedJvmSourcesDirectory)
multiplatformSourcesDir.set(unzippedMultiplatformSourcesDirectory)
projectListsDirectory.set(
- project.rootProject.layout.projectDirectory.dir("docs-public/package-lists")
+ File(project.getSupportRootFolder(), "docs-public/package-lists")
)
dependenciesClasspath.from(
dependencyClasspath +
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
index fca8225..b2c1cf6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -38,12 +38,12 @@
* @param baseCommitOverride optional value to use to override last merge commit
*/
fun Project.getChangedFilesProvider(
- baseCommitOverride: String?,
+ baseCommitOverride: Provider<String>,
): Provider<List<String>> {
val changeInfoPath = System.getenv("CHANGE_INFO")
val manifestPath = System.getenv("MANIFEST")
return if (changeInfoPath != null && manifestPath != null) {
- if (baseCommitOverride != null) throw GradleException(
+ if (baseCommitOverride.isPresent()) throw GradleException(
"Overriding base commit is not supported when using CHANGE_INFO and MANIFEST"
)
getChangedFilesFromChangeInfoProvider(manifestPath, changeInfoPath)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
index e80fc19..1023659 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
@@ -31,7 +31,6 @@
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.internal.tasks.userinput.UserInputHandler
-import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.gradle.internal.service.ServiceRegistry
@@ -339,7 +338,7 @@
abstract class PlaygroundStudioTask : RootStudioTask() {
@get:Internal
val supportRootFolder =
- (project.rootProject.property("ext") as ExtraPropertiesExtension).let {
+ (project.rootProject.extensions.extraProperties).let {
it.get("supportRootFolder") as File
}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
index 685ccd6..f592083 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
@@ -18,6 +18,7 @@
package androidx.build
+import androidx.build.gradle.extraPropertyOrNull
import java.io.File
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
@@ -26,13 +27,17 @@
abstract class AndroidConfigImpl(private val project: Project) : AndroidConfig {
override val buildToolsVersion: String = "35.0.0-rc1"
- override val compileSdk: Int by lazy { project.findProperty(COMPILE_SDK).toString().toInt() }
+ override val compileSdk: Int by lazy {
+ val sdkString = project.extraPropertyOrNull(COMPILE_SDK)?.toString()
+ check(sdkString != null) { "$COMPILE_SDK is unset" }
+ sdkString.toInt()
+ }
override val minSdk: Int = 21
override val ndkVersion: String = "25.2.9519653"
override val targetSdk: Int by lazy {
- project.findProperty(TARGET_SDK_VERSION).toString().toInt()
+ project.providers.gradleProperty(TARGET_SDK_VERSION).get().toInt()
}
companion object {
@@ -95,7 +100,7 @@
}
fun Project.getPrebuiltsRoot(): File {
- return File(project.rootProject.property("prebuiltsRoot").toString())
+ return File(project.extraPropertyOrNull("prebuiltsRoot").toString())
}
/** @return the project's Android SDK stub JAR as a File. */
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
index ab190af..74e1548 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
@@ -16,6 +16,7 @@
package androidx.build
+import androidx.build.gradle.extraPropertyOrNull
import java.util.Locale
import org.gradle.api.Project
import org.gradle.kotlin.dsl.create
@@ -116,7 +117,9 @@
/** Extension used to store parsed KMP configuration information. */
private open class KmpPlatformsExtension(project: Project) {
val enabledKmpPlatforms =
- parseTargetPlatformsFlag(project.findProperty(ENABLED_KMP_TARGET_PLATFORMS) as? String)
+ parseTargetPlatformsFlag(
+ project.extraPropertyOrNull(ENABLED_KMP_TARGET_PLATFORMS) as? String
+ )
}
fun Project.enableJs(): Boolean = enabledKmpPlatforms.contains(PlatformGroup.JS)
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt b/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
index ec0129f..aedc32f 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
@@ -16,6 +16,7 @@
package androidx.build
+import androidx.build.gradle.extraPropertyOrNull
import org.gradle.api.Project
enum class ProjectLayoutType {
@@ -26,7 +27,7 @@
/** Returns the project layout type for the project (PLAYGROUND or ANDROIDX) */
@JvmStatic
fun from(project: Project): ProjectLayoutType {
- val value = project.findProperty(STUDIO_TYPE)?.toString()
+ val value = project.extraPropertyOrNull(STUDIO_TYPE)
return when (value) {
"playground" -> ProjectLayoutType.PLAYGROUND
null,
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
index 9b9c338..2fa6bde 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
@@ -21,7 +21,6 @@
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.FileTree
-import org.gradle.api.plugins.ExtraPropertiesExtension
/** Writes the appropriate SDK path to local.properties file in specified location. */
fun Project.writeSdkPathToLocalPropertiesFile() {
@@ -50,7 +49,7 @@
/** Returns the root project's platform-specific SDK path as a file. */
fun Project.getSdkPath(): File {
if (
- rootProject.plugins.hasPlugin("AndroidXPlaygroundRootPlugin") ||
+ ProjectLayoutType.from(project) == ProjectLayoutType.PLAYGROUND ||
System.getenv("COMPOSE_DESKTOP_GITHUB_BUILD") != null
) {
// This is not full checkout, use local settings instead.
@@ -99,7 +98,7 @@
/** Sets the path to the canonical root project directory, e.g. {@code frameworks/support}. */
fun Project.setSupportRootFolder(rootDir: File?) {
- val extension = project.property("ext") as ExtraPropertiesExtension
+ val extension = project.extensions.extraProperties
return extension.set("supportRootFolder", rootDir)
}
@@ -110,7 +109,7 @@
* because it is generalized to also work for the "ui" project and playground projects.
*/
fun Project.getSupportRootFolder(): File {
- val extension = project.property("ext") as ExtraPropertiesExtension
+ val extension = project.extensions.extraProperties
return extension.get("supportRootFolder") as File
}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt b/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
index 7d2ebd4..92bbc57 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
@@ -20,3 +20,18 @@
val Project.isRoot
get() = this == rootProject
+
+/**
+ * Implements project.extensions.extraProperties.getOrNull(key)
+ * TODO(https://github.com/gradle/gradle/issues/28857) use simpler replacement when available
+ *
+ * Note that providers.gradleProperty() might return null in cases where this function can
+ * find a value: https://github.com/gradle/gradle/issues/23572
+ */
+fun Project.extraPropertyOrNull(key: String): Any? {
+ val container = project.extensions.extraProperties
+ var result: Any? = null
+ if (container.has(key))
+ result = container.get(key)
+ return result
+}
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
index ad474a8..cf4ffa0 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
@@ -46,7 +46,7 @@
content: @Composable () -> Unit
) {
CompositionLocalProvider(
- LocalRippleConfiguration provides RippleConfiguration(isEnabled = enabled)
+ LocalRippleConfiguration provides if (enabled) RippleConfiguration() else null
) {
FloatingActionButton(
onClick = if (enabled) onClick else { {} },
diff --git a/car/app/app/gradle.properties b/car/app/app/gradle.properties
deleted file mode 100644
index a060082..0000000
--- a/car/app/app/gradle.properties
+++ /dev/null
@@ -1,16 +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.
-#
-androidx.targetSdkVersion = 33
diff --git a/car/app/app/lint-baseline.xml b/car/app/app/lint-baseline.xml
index 16305d3..e10674d 100644
--- a/car/app/app/lint-baseline.xml
+++ b/car/app/app/lint-baseline.xml
@@ -362,6 +362,15 @@
</issue>
<issue
+ id="UnspecifiedRegisterReceiverFlag"
+ message="`mBroadcastReceiver` is missing `RECEIVER_EXPORTED` or `RECEIVER_NOT_EXPORTED` flag for unprotected broadcasts registered for androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"
+ errorLine1=" mContext.registerReceiver(mBroadcastReceiver, filter);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java"/>
+ </issue>
+
+ <issue
id="UnsafeOptInUsageError"
message="This declaration is opt-in and its usage should be marked with `@androidx.car.app.annotations.ExperimentalCarApi` or `@OptIn(markerClass = androidx.car.app.annotations.ExperimentalCarApi.class)`"
errorLine1=" private final List<CarZone> mCarZones;"
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index e026507..f8dc027 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -1132,6 +1132,7 @@
public static sealed interface LazyLayoutPrefetchState.PrefetchHandle {
method public void cancel();
+ method public void markAsUrgent();
}
public final class Lazy_androidKt {
@@ -1156,8 +1157,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchRequestScope {
- method public long getAvailableTimeNanos();
- property public abstract long availableTimeNanos;
+ method public long availableTimeNanos();
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchScheduler {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index cc020da..8ffe7e9 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -1134,6 +1134,7 @@
public static sealed interface LazyLayoutPrefetchState.PrefetchHandle {
method public void cancel();
+ method public void markAsUrgent();
}
public final class Lazy_androidKt {
@@ -1158,8 +1159,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchRequestScope {
- method public long getAvailableTimeNanos();
- property public abstract long availableTimeNanos;
+ method public long availableTimeNanos();
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchScheduler {
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
index 087d8db..c4d5b3a 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -14,14 +14,20 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
@@ -58,6 +64,16 @@
val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
lateinit var state: LazyGridState
+ private val scheduler = TestPrefetchScheduler()
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun rememberState(
+ initialFirstVisibleItemIndex: Int = 0,
+ initialFirstVisibleItemScrollOffset: Int = 0
+ ): LazyGridState = remember {
+ LazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, scheduler)
+ }
@Test
fun notPrefetchingForwardInitially() {
@@ -85,8 +101,8 @@
}
}
- waitForPrefetch(4)
- waitForPrefetch(5)
+ waitForPrefetch()
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertExists()
@@ -106,8 +122,8 @@
}
}
- waitForPrefetch(2)
- waitForPrefetch(3)
+ waitForPrefetch()
+ waitForPrefetch()
rule.onNodeWithTag("2")
.assertExists()
@@ -127,8 +143,8 @@
}
}
- waitForPrefetch(6)
- waitForPrefetch(7)
+ waitForPrefetch()
+ waitForPrefetch()
rule.onNodeWithTag("6")
.assertExists()
@@ -144,8 +160,8 @@
}
}
- waitForPrefetch(0)
- waitForPrefetch(1)
+ waitForPrefetch()
+ waitForPrefetch()
rule.onNodeWithTag("0")
.assertExists()
@@ -165,7 +181,7 @@
}
}
- waitForPrefetch(4)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -174,7 +190,7 @@
}
}
- waitForPrefetch(6)
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertIsDisplayed()
@@ -194,7 +210,7 @@
}
}
- waitForPrefetch(4)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -203,7 +219,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertIsDisplayed()
@@ -225,8 +241,7 @@
}
}
- waitForPrefetch(6)
- waitForPrefetch(7)
+ waitForPrefetch()
rule.onNodeWithTag("6")
.assertExists()
@@ -244,8 +259,7 @@
}
}
- waitForPrefetch(0)
- waitForPrefetch(1)
+ waitForPrefetch()
rule.onNodeWithTag("0")
.assertExists()
@@ -283,7 +297,7 @@
}
}
- waitForPrefetch(6)
+ waitForPrefetch()
rule.onNodeWithTag("8")
.assertExists()
@@ -296,7 +310,7 @@
}
}
- waitForPrefetch(0)
+ waitForPrefetch()
rule.onNodeWithTag("0")
.assertExists()
@@ -316,7 +330,7 @@
) { constraints ->
val placeable = if (emit) {
subcompose(Unit) {
- state = rememberLazyGridState()
+ state = rememberState()
LazyGrid(
2,
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -355,7 +369,7 @@
fun snappingToOtherPositionWhilePrefetchIsScheduled() {
val composedItems = mutableListOf<Int>()
rule.setContent {
- state = rememberLazyGridState()
+ state = rememberState()
LazyGrid(
1,
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -410,7 +424,7 @@
}
}
- waitForPrefetch(13)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking(AutoTestFrameClock()) {
@@ -424,14 +438,13 @@
}
}
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil {
- activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
private fun composeGrid(
firstItem: Int = 0,
@@ -440,7 +453,7 @@
contentPadding: PaddingValues = PaddingValues(0.dp)
) {
rule.setContent {
- state = rememberLazyGridState(
+ state = rememberState(
initialFirstVisibleItemIndex = firstItem,
initialFirstVisibleItemScrollOffset = itemOffset
)
@@ -456,7 +469,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -465,7 +477,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index bb6d01a..5596f14 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -257,7 +257,8 @@
.then(modifier))
}
var needToCompose by mutableStateOf(false)
- val prefetchState = LazyLayoutPrefetchState()
+ val scheduler = TestPrefetchScheduler()
+ val prefetchState = LazyLayoutPrefetchState(scheduler)
rule.setContent {
LazyLayout(itemProvider, prefetchState = prefetchState) {
val item = if (needToCompose) {
@@ -273,9 +274,10 @@
assertThat(measureCount).isEqualTo(0)
prefetchState.schedulePrefetch(0, constraints)
- }
- rule.waitUntil { measureCount == 1 }
+ scheduler.executeActiveRequests()
+ assertThat(measureCount).isEqualTo(1)
+ }
rule.onNodeWithTag("0").assertIsNotDisplayed()
@@ -303,20 +305,18 @@
}
}
}
- val prefetchState = LazyLayoutPrefetchState()
+ val scheduler = TestPrefetchScheduler()
+ val prefetchState = LazyLayoutPrefetchState(scheduler)
rule.setContent {
LazyLayout(itemProvider, prefetchState = prefetchState) {
layout(100, 100) {}
}
}
- val handle = rule.runOnIdle {
- prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
- }
-
- rule.waitUntil { composed }
-
rule.runOnIdle {
+ val handle = prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
+ scheduler.executeActiveRequests()
+ assertThat(composed).isTrue()
handle.cancel()
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.kt
new file mode 100644
index 0000000..ad3b8a6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.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.compose.foundation.lazy.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TestPrefetchScheduler : PrefetchScheduler {
+
+ private var activeRequests = mutableListOf<PrefetchRequest>()
+ override fun schedulePrefetch(prefetchRequest: PrefetchRequest) {
+ activeRequests.add(prefetchRequest)
+ }
+
+ fun executeActiveRequests() {
+ activeRequests.forEach {
+ with(it) { scope.execute() }
+ }
+ activeRequests.clear()
+ }
+
+ private val scope = object : PrefetchRequestScope {
+ override fun availableTimeNanos(): Long = Long.MAX_VALUE
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
index b8a37d5..21f1531 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
@@ -14,11 +14,14 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.list
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
@@ -45,6 +48,10 @@
private val composedItems = mutableSetOf<Int>()
+ private val state = LazyListState().also {
+ it.prefetchingEnabled = false
+ }
+
@Test
fun moveFocus() {
// Arrange.
@@ -52,7 +59,7 @@
lateinit var focusManager: FocusManager
rule.setContent {
focusManager = LocalFocusManager.current
- LazyRow(Modifier.size(rowSize)) {
+ LazyRow(Modifier.size(rowSize), state) {
items(100) { index ->
Box(
Modifier
@@ -71,7 +78,7 @@
rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
// Assert
- rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+ rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
}
@Test
@@ -81,7 +88,7 @@
lateinit var focusManager: FocusManager
rule.setContent {
focusManager = LocalFocusManager.current
- LazyRow(Modifier.size(rowSize)) {
+ LazyRow(Modifier.size(rowSize), state) {
items(100) { index ->
Box(Modifier.size(itemSize).focusable()) {
Box(Modifier.size(itemSize).focusable().testTag("$index"))
@@ -97,7 +104,7 @@
rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
// Assert
- rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+ rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
}
@Test
@@ -107,7 +114,7 @@
lateinit var focusManager: FocusManager
rule.setContent {
focusManager = LocalFocusManager.current
- LazyRow(Modifier.size(rowSize)) {
+ LazyRow(Modifier.size(rowSize), state) {
items(100) { index ->
Box(Modifier.size(itemSize).focusable()) {
Box(Modifier.size(itemSize).focusable()) {
@@ -125,6 +132,6 @@
rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
// Assert
- rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+ rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
}
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
index ebd489b..229f84a 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
@@ -26,6 +26,8 @@
import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
@@ -70,11 +72,19 @@
private val itemsSizePx = 30
private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
private val activeNodes = mutableSetOf<String>()
- private val activeMeasuredNodes = mutableSetOf<String>()
+ private val scheduler = TestPrefetchScheduler()
+
+ @OptIn(ExperimentalFoundationApi::class)
+ private val strategy = object : LazyListPrefetchStrategy by LazyListPrefetchStrategy() {
+ override val prefetchScheduler: PrefetchScheduler = scheduler
+ }
+
+ @OptIn(ExperimentalFoundationApi::class)
+ private fun createState(): LazyListState = LazyListState(prefetchStrategy = strategy)
@Test
fun nestedPrefetchingForwardAfterSmallScroll() {
- val state = LazyListState()
+ val state = createState()
composeList(state)
val prefetchIndex = 2
@@ -85,7 +95,7 @@
}
}
- waitForPrefetch(tagFor(prefetchIndex))
+ waitForPrefetch()
}
// We want to make sure nested children were precomposed before the parent was premeasured
@@ -111,7 +121,7 @@
@Test
fun cancelingPrefetchCancelsItsNestedPrefetches() {
- val state = LazyListState()
+ val state = createState()
composeList(state)
rule.runOnIdle {
@@ -122,7 +132,7 @@
}
}
- waitForPrefetch(tagFor(3))
+ waitForPrefetch()
rule.runOnIdle {
assertThat(activeNodes).contains(tagFor(3))
@@ -141,7 +151,7 @@
}
}
- waitForPrefetch(tagFor(7))
+ waitForPrefetch()
rule.runOnIdle {
runBlocking(AutoTestFrameClock()) {
@@ -160,7 +170,7 @@
@OptIn(ExperimentalFoundationApi::class)
@Test
fun overridingNestedPrefetchCountIsRespected() {
- val state = LazyListState()
+ val state = createState()
composeList(
state,
createNestedLazyListState = {
@@ -177,7 +187,7 @@
}
}
- waitForPrefetch(tagFor(prefetchIndex))
+ waitForPrefetch()
}
// Since the nested prefetch count on the strategy is 1, we only expect index 0 to be
@@ -197,7 +207,7 @@
fun nestedPrefetchIsMeasuredWithProvidedConstraints() {
val nestedConstraints =
Constraints(minWidth = 20, minHeight = 20, maxWidth = 20, maxHeight = 20)
- val state = LazyListState()
+ val state = createState()
composeList(
state,
createNestedLazyListState = {
@@ -214,7 +224,7 @@
}
}
- waitForPrefetch(tagFor(prefetchIndex))
+ waitForPrefetch()
}
assertThat(actions).containsExactly(
@@ -232,7 +242,7 @@
@Test
fun nestedPrefetchStartsFromFirstVisibleItemIndex() {
- val state = LazyListState()
+ val state = createState()
composeList(
state,
createNestedLazyListState = {
@@ -247,7 +257,7 @@
}
}
- waitForPrefetch(tagFor(prefetchIndex))
+ waitForPrefetch()
}
assertThat(actions).containsExactly(
@@ -273,9 +283,9 @@
}
}
- private fun waitForPrefetch(tag: String) {
- rule.waitUntil {
- activeNodes.contains(tag) && activeMeasuredNodes.contains(tag)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
@@ -332,17 +342,14 @@
actions?.add(Action.Compose(index, nestedIndex))
onDispose {
activeNodes.remove(tag)
- activeMeasuredNodes.remove(tag)
}
}
}
private fun Modifier.trackWhenMeasured(index: Int, nestedIndex: Int? = null): Modifier {
- val tag = tagFor(index, nestedIndex)
return this then Modifier.layout { measurable, constraints ->
actions?.add(Action.Measure(index, nestedIndex))
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(tag)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
index 31dd332..0d773c2 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
@@ -29,9 +29,10 @@
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
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
@@ -74,10 +75,11 @@
private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
lateinit var state: LazyListState
+ private val scheduler = TestPrefetchScheduler()
@Test
fun callbacksTriggered_whenScrollForwardsWithoutVisibleItemsChanged() {
- val strategy = RecordingLazyListPrefetchStrategy()
+ val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(prefetchStrategy = strategy)
@@ -104,7 +106,7 @@
@Test
fun callbacksTriggered_whenScrollBackwardsWithoutVisibleItemsChanged() {
- val strategy = RecordingLazyListPrefetchStrategy()
+ val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(firstItem = 10, itemOffset = 10, prefetchStrategy = strategy)
@@ -131,7 +133,7 @@
@Test
fun callbacksTriggered_whenScrollWithVisibleItemsChanged() {
- val strategy = RecordingLazyListPrefetchStrategy()
+ val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(prefetchStrategy = strategy)
@@ -161,7 +163,7 @@
@Test
fun callbacksTriggered_whenItemsChangedWithoutScroll() {
- val strategy = RecordingLazyListPrefetchStrategy()
+ val strategy = RecordingLazyListPrefetchStrategy(scheduler)
val numItems = mutableStateOf(100)
composeList(prefetchStrategy = strategy, numItems = numItems)
@@ -196,20 +198,17 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.onNodeWithTag("2")
.assertExists()
}
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil {
- activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
- private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
-
@OptIn(ExperimentalFoundationApi::class)
private fun composeList(
firstItem: Int = 0,
@@ -228,13 +227,6 @@
state,
) {
items(numItems.value) {
- DisposableEffect(it) {
- activeNodes.add(it)
- onDispose {
- activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
- }
- }
Spacer(
Modifier
.mainAxisSize(itemsSizeDp)
@@ -242,7 +234,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
@@ -256,7 +247,10 @@
/**
* LazyListPrefetchStrategy that just records callbacks without scheduling prefetches.
*/
- private class RecordingLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+ private class RecordingLazyListPrefetchStrategy(
+ override val prefetchScheduler: PrefetchScheduler?
+ ) : LazyListPrefetchStrategy {
+
sealed interface Callback {
data class OnScroll(val delta: Float, val visibleIndices: List<Int>) : Callback
data class OnVisibleItemsUpdated(val visibleIndices: List<Int>) : Callback
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
index 70a88d8..ef1aed5 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
@@ -22,7 +22,10 @@
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
@@ -71,6 +74,13 @@
lateinit var state: LazyListState
+ private val scheduler = TestPrefetchScheduler()
+
+ @OptIn(ExperimentalFoundationApi::class)
+ private val strategy = object : LazyListPrefetchStrategy by LazyListPrefetchStrategy() {
+ override val prefetchScheduler: PrefetchScheduler = scheduler
+ }
+
@Test
fun notPrefetchingForwardInitially() {
composeList()
@@ -97,7 +107,7 @@
}
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertExists()
@@ -115,7 +125,7 @@
}
}
- waitForPrefetch(1)
+ waitForPrefetch()
rule.onNodeWithTag("1")
.assertExists()
@@ -134,7 +144,7 @@
}
}
var prefetchIndex = initialIndex + 2
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -149,7 +159,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -167,7 +177,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -178,7 +188,7 @@
val prefetchIndex = 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("${prefetchIndex - 1}")
.assertIsDisplayed()
@@ -198,7 +208,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -207,7 +217,7 @@
}
}
- waitForPrefetch(1)
+ waitForPrefetch()
rule.onNodeWithTag("2")
.assertIsDisplayed()
@@ -230,7 +240,7 @@
var prefetchIndex = initialIndex + 2
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -245,7 +255,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -281,7 +291,7 @@
}
var prefetchIndex = initialIndex + 1
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("${prefetchIndex + 1}")
.assertExists()
@@ -295,7 +305,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -458,7 +468,7 @@
}
}
- waitForPrefetch(7)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking(AutoTestFrameClock()) {
@@ -472,14 +482,13 @@
}
}
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil {
- activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
private fun composeList(
firstItem: Int = 0,
@@ -488,9 +497,11 @@
contentPadding: PaddingValues = PaddingValues(0.dp)
) {
rule.setContent {
+ @OptIn(ExperimentalFoundationApi::class)
state = rememberLazyListState(
initialFirstVisibleItemIndex = firstItem,
- initialFirstVisibleItemScrollOffset = itemOffset
+ initialFirstVisibleItemScrollOffset = itemOffset,
+ prefetchStrategy = strategy
)
LazyColumnOrRow(
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -504,7 +515,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -514,7 +524,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
index 224a1cc..72084b7 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.AutoTestFrameClock
@@ -22,7 +24,10 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Remeasurement
@@ -63,6 +68,20 @@
val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
internal lateinit var state: LazyStaggeredGridState
+ private val scheduler = TestPrefetchScheduler()
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun rememberState(
+ initialFirstVisibleItemIndex: Int = 0,
+ initialFirstVisibleItemOffset: Int = 0
+ ): LazyStaggeredGridState = remember {
+ LazyStaggeredGridState(
+ intArrayOf(initialFirstVisibleItemIndex),
+ intArrayOf(initialFirstVisibleItemOffset),
+ scheduler
+ )
+ }
@Test
fun notPrefetchingForwardInitially() {
@@ -90,7 +109,7 @@
}
}
- waitForPrefetch(5)
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertExists()
@@ -110,7 +129,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.onNodeWithTag("2")
.assertExists()
@@ -130,7 +149,7 @@
}
}
- waitForPrefetch(9)
+ waitForPrefetch()
rule.onNodeWithTag("8")
.assertExists()
@@ -145,7 +164,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.onNodeWithTag("2")
.assertExists()
@@ -165,7 +184,7 @@
}
}
- waitForPrefetch(4)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -174,7 +193,7 @@
}
}
- waitForPrefetch(6)
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertIsDisplayed()
@@ -194,7 +213,7 @@
}
}
- waitForPrefetch(4)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -203,7 +222,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.onNodeWithTag("4")
.assertIsDisplayed()
@@ -226,7 +245,7 @@
}
}
- waitForPrefetch(13)
+ waitForPrefetch()
rule.onNodeWithTag("12")
.assertExists()
@@ -378,7 +397,7 @@
fun snappingToOtherPositionWhilePrefetchIsScheduled() {
val composedItems = mutableListOf<Int>()
rule.setContent {
- state = rememberLazyStaggeredGridState()
+ state = rememberState()
LazyStaggeredGrid(
1,
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -433,7 +452,7 @@
}
}
- waitForPrefetch(13)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking(AutoTestFrameClock()) {
@@ -450,7 +469,7 @@
@Test
fun scrollingWithStaggeredItemsPrefetchesCorrectly() {
rule.setContent {
- state = rememberLazyStaggeredGridState()
+ state = rememberState()
LazyStaggeredGrid(
2,
Modifier.mainAxisSize(itemsSizeDp * 5f),
@@ -461,7 +480,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -471,7 +489,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
@@ -495,8 +512,8 @@
}
}
- waitForPrefetch(7)
- waitForPrefetch(8)
+ waitForPrefetch()
+ waitForPrefetch()
// ┌─┬─┐
// │2├─┤
@@ -520,14 +537,14 @@
// │6├─┤
// └─┴─┘
- waitForPrefetch(9)
+ waitForPrefetch()
}
@Test
fun fullSpanIsPrefetchedCorrectly() {
val nodeConstraints = mutableMapOf<Int, Constraints>()
rule.setContent {
- state = rememberLazyStaggeredGridState()
+ state = rememberState()
LazyStaggeredGrid(
2,
Modifier.mainAxisSize(itemsSizeDp * 5f).crossAxisSize(itemsSizeDp * 2f),
@@ -546,7 +563,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -555,7 +571,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
nodeConstraints.put(it, constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
@@ -577,7 +592,7 @@
state.scrollBy(itemsSizeDp * 5f)
assertThat(activeNodes).contains(9)
- waitForPrefetch(10)
+ waitForPrefetch()
val expectedConstraints = if (vertical) {
Constraints.fixedWidth(itemsSizePx * 2)
} else {
@@ -589,7 +604,7 @@
@Test
fun fullSpanIsPrefetchedCorrectly_scrollingBack() {
rule.setContent {
- state = rememberLazyStaggeredGridState()
+ state = rememberState()
LazyStaggeredGrid(
2,
Modifier.mainAxisSize(itemsSizeDp * 5f),
@@ -608,7 +623,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -618,7 +632,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
@@ -647,27 +660,26 @@
state.scrollBy(-1.dp)
- waitForPrefetch(10)
+ waitForPrefetch()
}
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil {
- activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
private fun composeStaggeredGrid(
firstItem: Int = 0,
itemOffset: Int = 0,
) {
- state = LazyStaggeredGridState(
- initialFirstVisibleItemIndex = firstItem,
- initialFirstVisibleItemOffset = itemOffset
- )
rule.setContent {
+ state = rememberState(
+ initialFirstVisibleItemIndex = firstItem,
+ initialFirstVisibleItemOffset = itemOffset
+ )
LazyStaggeredGrid(
2,
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -678,7 +690,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
Spacer(
@@ -688,7 +699,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
index bf5658f..dd8c1ce 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -30,9 +30,11 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -131,13 +133,21 @@
key: ((index: Int) -> Any)? = null,
snapPosition: SnapPosition = config.snapPosition.first,
flingBehavior: TargetedFlingBehavior? = null,
+ prefetchScheduler: PrefetchScheduler? = null,
pageContent: @Composable PagerScope.(page: Int) -> Unit = { Page(index = it) }
) {
rule.setContent {
- val state = rememberPagerState(initialPage, initialPageOffsetFraction, pageCount).also {
- pagerState = it
+ val state = if (prefetchScheduler == null) {
+ rememberPagerState(initialPage, initialPageOffsetFraction, pageCount)
+ } else {
+ remember {
+ object : PagerState(initialPage, initialPageOffsetFraction, prefetchScheduler) {
+ override val pageCount: Int get() = pageCount()
+ }
+ }
}
+ pagerState = state
composeView = LocalView.current
focusManager = LocalFocusManager.current
CompositionLocalProvider(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
index d72d1e3..19d6748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.pager
import android.view.accessibility.AccessibilityNodeProvider
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -41,6 +42,7 @@
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+@OptIn(ExperimentalFoundationApi::class)
@RunWith(Parameterized::class)
class PagerAccessibilityTest(config: ParamConfig) : BasePagerTest(config = config) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
index 1f9cc0b..f9414d3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
@@ -56,6 +56,7 @@
var pageSizePx = 300
val pageSizeDp = with(rule.density) { pageSizePx.toDp() }
var touchSlope: Float = 0.0f
+ private val scheduler = TestPrefetchScheduler()
@Test
fun notPrefetchingForwardInitially() {
@@ -83,7 +84,7 @@
}
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertExists()
@@ -102,7 +103,7 @@
}
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertExists()
@@ -126,7 +127,7 @@
up()
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertExists()
@@ -151,7 +152,7 @@
up()
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertExists()
@@ -170,7 +171,7 @@
}
}
var prefetchIndex = initialIndex + 2
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -185,7 +186,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -203,7 +204,7 @@
}
}
- waitForPrefetch(2)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -214,7 +215,7 @@
val prefetchIndex = 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("${prefetchIndex - 1}")
.assertIsDisplayed()
@@ -236,7 +237,7 @@
}
}
- waitForPrefetch(preFetchIndex)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking {
@@ -245,7 +246,7 @@
}
}
- waitForPrefetch(preFetchIndex - 1)
+ waitForPrefetch()
rule.onNodeWithTag("$preFetchIndex")
.assertIsDisplayed()
@@ -268,7 +269,7 @@
var prefetchIndex = initialIndex + 2
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -283,7 +284,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -319,7 +320,7 @@
}
var prefetchIndex = initialIndex + 1
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("${prefetchIndex + 1}")
.assertExists()
@@ -333,7 +334,7 @@
}
prefetchIndex -= 3
- waitForPrefetch(prefetchIndex)
+ waitForPrefetch()
rule.onNodeWithTag("$prefetchIndex")
.assertExists()
@@ -457,7 +458,7 @@
}
}
- waitForPrefetch(7)
+ waitForPrefetch()
rule.runOnIdle {
runBlocking(AutoTestFrameClock()) {
@@ -477,14 +478,13 @@
return consumed
}
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil {
- activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ private fun waitForPrefetch() {
+ rule.runOnIdle {
+ scheduler.executeActiveRequests()
}
}
private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
private fun composePager(
initialPage: Int = 0,
@@ -499,6 +499,7 @@
beyondViewportPageCount = paramConfig.beyondViewportPageCount,
initialPage = initialPage,
initialPageOffsetFraction = initialPageOffsetFraction,
+ prefetchScheduler = scheduler,
pageCount = { 100 },
pageSize = {
object : PageSize {
@@ -516,7 +517,6 @@
activeNodes.add(it)
onDispose {
activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
}
}
@@ -527,7 +527,6 @@
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt
new file mode 100644
index 0000000..04c60b750
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.PrefetchRequest
+import androidx.compose.foundation.lazy.layout.PrefetchRequestScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TestPrefetchScheduler : PrefetchScheduler {
+
+ private var activeRequests = mutableListOf<PrefetchRequest>()
+ override fun schedulePrefetch(prefetchRequest: PrefetchRequest) {
+ activeRequests.add(prefetchRequest)
+ }
+
+ fun executeActiveRequests() {
+ activeRequests.forEach {
+ with(it) { scope.execute() }
+ }
+ activeRequests.clear()
+ }
+
+ private val scope = object : PrefetchRequestScope {
+ override fun availableTimeNanos(): Long = Long.MAX_VALUE
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
index b050f4f..fb0f59d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
@@ -117,11 +117,17 @@
expectedClipboardContent = "Text",
)
+ @Test
+ fun contextMenu_onClickSelectAll() = runClickContextMenuItemTest(
+ labelToClick = ContextMenuItemLabels.SELECT_ALL,
+ expectedSelection = TextRange(0, 14),
+ )
+
@Suppress("SameParameterValue")
private fun runClickContextMenuItemTest(
labelToClick: String,
expectedSelection: TextRange,
- expectedClipboardContent: String,
+ expectedClipboardContent: String? = null,
) {
val initialClipboardText = "clip"
@@ -163,7 +169,8 @@
assertThat(selection!!.toTextRange()).isEqualTo(expectedSelection)
val clipboardContent = clipboardManager.getText()
assertThat(clipboardContent).isNotNull()
- assertThat(clipboardContent!!.text).isEqualTo(expectedClipboardContent)
+ assertThat(clipboardContent!!.text)
+ .isEqualTo(expectedClipboardContent ?: initialClipboardText)
}
// endregion Context Menu Item Click Tests
@@ -178,7 +185,7 @@
cutState = ContextMenuItemState.DOES_NOT_EXIST,
copyState = ContextMenuItemState.DISABLED,
pasteState = ContextMenuItemState.DOES_NOT_EXIST,
- selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+ selectAllState = ContextMenuItemState.ENABLED,
)
}
@@ -192,7 +199,7 @@
cutState = ContextMenuItemState.DOES_NOT_EXIST,
copyState = ContextMenuItemState.ENABLED,
pasteState = ContextMenuItemState.DOES_NOT_EXIST,
- selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+ selectAllState = ContextMenuItemState.ENABLED,
)
}
@@ -206,7 +213,7 @@
cutState = ContextMenuItemState.DOES_NOT_EXIST,
copyState = ContextMenuItemState.ENABLED,
pasteState = ContextMenuItemState.DOES_NOT_EXIST,
- selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+ selectAllState = ContextMenuItemState.DISABLED,
)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
index 6c4fadc..f4e77b8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
@@ -168,7 +168,7 @@
assertTextToolbarTopAt(pointerAreaRect.top)
scrollLines(fromLine = 5, toLine = 3)
- assertThat(textToolbarShown).isFalse()
+ assertTextToolbarTopAt(pointerAreaRect.top)
scrollLines(fromLine = 5, toLine = 7)
assertThat(textToolbarShown).isTrue()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
index 54abefd..fd93543 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
@@ -126,16 +126,19 @@
}
val latestFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(view.drawingTime)
val nextFrameNs = latestFrameVsyncNs + frameIntervalNs
- val oneOverTimeTaskAllowed = System.nanoTime() > nextFrameNs
- val scope = PrefetchRequestScopeImpl(nextFrameNs, oneOverTimeTaskAllowed)
+ val scope = PrefetchRequestScopeImpl(nextFrameNs)
var scheduleForNextFrame = false
while (prefetchRequests.isNotEmpty() && !scheduleForNextFrame) {
- val request = prefetchRequests[0]
- val hasMoreWorkToDo = with(request) { scope.execute() }
- if (hasMoreWorkToDo) {
- scheduleForNextFrame = true
+ if (scope.availableTimeNanos() > 0) {
+ val request = prefetchRequests[0]
+ val hasMoreWorkToDo = with(request) { scope.execute() }
+ if (hasMoreWorkToDo) {
+ scheduleForNextFrame = true
+ } else {
+ prefetchRequests.removeAt(0)
+ }
} else {
- prefetchRequests.removeAt(0)
+ scheduleForNextFrame = true
}
}
@@ -182,24 +185,10 @@
class PrefetchRequestScopeImpl(
private val nextFrameTimeNs: Long,
- isOneOverTimeTaskAllowed: Boolean
) : PrefetchRequestScope {
- private var canDoOverTimeTask = isOneOverTimeTaskAllowed
-
- override val availableTimeNanos: Long
- get() {
- // This logic is meant to be temporary until we replace the isOneOverTimeTaskAllowed
- // logic with something more general. For now, we assume that a PrefetchRequest
- // impl will check availableTimeNanos once per task and we give it a large amount
- // of time the first time it checks if we allow an overtime task.
- return if (canDoOverTimeTask) {
- canDoOverTimeTask = false
- Long.MAX_VALUE
- } else {
- max(0, nextFrameTimeNs - System.nanoTime())
- }
- }
+ override fun availableTimeNanos() =
+ max(0, nextFrameTimeNs - System.nanoTime())
}
companion object {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index 19d75d2..4f24fb9 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -78,6 +78,7 @@
state: ContextMenuState,
): ContextMenuScope.() -> Unit {
val copyString = TextContextMenuItems.Copy.resolvedString()
+ val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
return {
listOf(
item(
@@ -88,7 +89,14 @@
state.close()
},
),
- // TODO(b/240143283) Add select all item
+ item(
+ label = selectAllString,
+ enabled = !isEntireContainerSelected(),
+ onClick = {
+ selectAll()
+ state.close()
+ },
+ ),
)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
index 98160e5..ca6265d 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
@@ -282,7 +282,7 @@
var boundingBoxes: Map<Int, Rect> = emptyMap()
private val selectableKey = 1L
- private val fakeSelectAllSelection: Selection = Selection(
+ var fakeSelectAllSelection: Selection? = Selection(
start = Selection.AnchorInfo(
direction = ResolvedTextDirection.Ltr,
offset = 0,
@@ -309,7 +309,7 @@
)
}
- override fun getSelectAllSelection(): Selection {
+ override fun getSelectAllSelection(): Selection? {
return fakeSelectAllSelection
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
index a1b2d08..085643e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
@@ -16,16 +16,21 @@
package androidx.compose.foundation.text.selection
+import androidx.collection.LongObjectMap
import androidx.collection.emptyLongObjectMap
import androidx.collection.longObjectMapOf
+import androidx.collection.mutableLongObjectMapOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.util.fastForEach
import com.google.common.truth.Truth.assertThat
+import kotlin.test.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -211,14 +216,14 @@
}
@Test
- fun mergeSelections_selectAll() {
+ fun mergeSelections_selectAllInSelectable() {
val anotherSelectableId = 100L
val selectableAnother = mock<Selectable>()
whenever(selectableAnother.selectableId).thenReturn(anotherSelectableId)
selectionRegistrar.subscribe(selectableAnother)
- selectionManager.selectAll(
+ selectionManager.selectAllInSelectable(
selectableId = selectableId,
previousSelection = fakeSelection
)
@@ -805,7 +810,7 @@
any(),
isNull(),
isNull(),
- isNull()
+ any()
)
}
@@ -911,4 +916,362 @@
times(1)
).performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
+
+ // region isEntireContainerSelected Tests
+ @Test
+ fun isEntireContainerSelected_noSelectables_returnsTrue() {
+ isEntireContainerSelectedTest(expectedResult = true)
+ }
+
+ @Test
+ fun isEntireContainerSelected_singleEmptySelectable_returnsTrue() {
+ isEntireContainerSelectedTest(
+ expectedResult = true,
+ IsEntireContainerSelectedData(text = "", selection = null),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_multipleEmptySelectables_returnsTrue() {
+ isEntireContainerSelectedTest(
+ expectedResult = true,
+ IsEntireContainerSelectedData(text = "", selection = null),
+ IsEntireContainerSelectedData(text = "", selection = null),
+ IsEntireContainerSelectedData(text = "", selection = null),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_emptySurroundingNonEmpty_fullySelected_returnsTrue() {
+ isEntireContainerSelectedTest(
+ expectedResult = true,
+ IsEntireContainerSelectedData(text = "", selection = null),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "", selection = null),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_nonEmptySurroundingEmpty_fullySelected_returnsTrue() {
+ isEntireContainerSelectedTest(
+ expectedResult = true,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "", selection = TextRange(0, 0)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_nonEmptyFirstTextNotSelected_returnsFalse() {
+ isEntireContainerSelectedTest(
+ expectedResult = false,
+ IsEntireContainerSelectedData(text = "Text", selection = null),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_nonEmptyLastTextNotSelected_returnsFalse() {
+ isEntireContainerSelectedTest(
+ expectedResult = false,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = null),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_firstTextPartiallySelected_returnsFalse() {
+ isEntireContainerSelectedTest(
+ expectedResult = false,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(1, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_lastTextPartiallySelected_returnsFalse() {
+ isEntireContainerSelectedTest(
+ expectedResult = false,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 3)),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_reversedSelectionFullySelected_returnsTrue() {
+ isEntireContainerSelectedTest(
+ expectedResult = true,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(4, 0)),
+ )
+ }
+
+ @Test
+ fun isEntireContainerSelected_reversedSelectionPartiallySelected_returnsFalse() {
+ isEntireContainerSelectedTest(
+ expectedResult = false,
+ IsEntireContainerSelectedData(text = "Text", selection = TextRange(3, 0)),
+ )
+ }
+
+ /**
+ * Data necessary to set up a [SelectionManager.isEntireContainerSelected] unit test.
+ *
+ * @param text The text for the [Selectable] to return in [Selectable.getText].
+ * @param selection The selection to be associated with the [SelectionRegistrar.subselections].
+ * Null implies "do not include this selectable in the sub-selection".
+ */
+ private data class IsEntireContainerSelectedData(
+ val text: String,
+ val selection: TextRange?,
+ )
+
+ private fun isEntireContainerSelectedTest(
+ expectedResult: Boolean,
+ vararg selectableStates: IsEntireContainerSelectedData,
+ ) {
+ val selectables = selectableStates.mapIndexed { index, item ->
+ FakeSelectable().apply {
+ selectableId = index + 1L
+ textToReturn = AnnotatedString(item.text)
+ }
+ }
+
+ val registrar = SelectionRegistrarImpl().apply {
+ selectables.fastForEach { subscribe(it) }
+ subselections = selectableStates
+ .withIndex()
+ .filter { it.value.selection != null }
+ .associate { (index, item) ->
+ val id = index + 1L
+ val selection = item.selection
+ id to Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = selection!!.start,
+ selectableId = id
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = selection.end,
+ selectableId = id
+ ),
+ handlesCrossed = selection.reversed
+ )
+ }
+ .toLongObjectMap()
+ }
+
+ val manager = SelectionManager(registrar).apply {
+ containerLayoutCoordinates = MockCoordinates()
+ }
+
+ assertThat(manager.isEntireContainerSelected()).run {
+ if (expectedResult) isTrue() else isFalse()
+ }
+ }
+ // endregion isEntireContainerSelected Tests
+
+ // region selectAll Tests
+ @Test
+ fun selectAll_noSelectables_noSelection() {
+ selectAllTest(
+ expectedSelection = null,
+ expectedSubSelectionRanges = emptyMap(),
+ )
+ }
+
+ @Test
+ fun selectAll_singleUnSelectable_noSelection() {
+ selectAllTest(
+ expectedSelection = null,
+ expectedSubSelectionRanges = emptyMap(),
+ SelectAllData(text = "Text", selection = null),
+ )
+ }
+
+ @Test
+ fun selectAll_singleSelectable_selectedAsExpected() {
+ selectAllTest(
+ expectedSelection = expectedSelection(0, 4),
+ expectedSubSelectionRanges = mapOf(1L to TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun selectAll_multiSelectable_selectedAsExpected() {
+ selectAllTest(
+ expectedSelection = expectedSelection(
+ startOffset = 0,
+ endOffset = 4,
+ startSelectableId = 1L,
+ endSelectableId = 3L,
+ ),
+ expectedSubSelectionRanges = mapOf(
+ 1L to TextRange(0, 4),
+ 2L to TextRange(0, 4),
+ 3L to TextRange(0, 4),
+ ),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun selectAll_multiSelectable_skipFirst_selectedAsExpected() {
+ selectAllTest(
+ expectedSelection = expectedSelection(
+ startOffset = 0,
+ endOffset = 4,
+ startSelectableId = 2L,
+ endSelectableId = 3L,
+ ),
+ expectedSubSelectionRanges = mapOf(
+ 2L to TextRange(0, 4),
+ 3L to TextRange(0, 4),
+ ),
+ SelectAllData(text = "Text", selection = null),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun selectAll_multiSelectable_skipMiddle_selectedAsExpected() {
+ selectAllTest(
+ expectedSelection = expectedSelection(
+ startOffset = 0,
+ endOffset = 4,
+ startSelectableId = 1L,
+ endSelectableId = 3L,
+ ),
+ expectedSubSelectionRanges = mapOf(
+ 1L to TextRange(0, 4),
+ 3L to TextRange(0, 4),
+ ),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = null),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ )
+ }
+
+ @Test
+ fun selectAll_multiSelectable_skipLast_selectedAsExpected() {
+ selectAllTest(
+ expectedSelection = expectedSelection(
+ startOffset = 0,
+ endOffset = 4,
+ startSelectableId = 1L,
+ endSelectableId = 2L,
+ ),
+ expectedSubSelectionRanges = mapOf(
+ 1L to TextRange(0, 4),
+ 2L to TextRange(0, 4),
+ ),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = TextRange(0, 4)),
+ SelectAllData(text = "Text", selection = null),
+ )
+ }
+
+ private fun expectedSelection(
+ startOffset: Int,
+ endOffset: Int,
+ startSelectableId: Long = 1L,
+ endSelectableId: Long = 1L,
+ handlesCrossed: Boolean = false,
+ ): Selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectableId = startSelectableId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectableId = endSelectableId
+ ),
+ handlesCrossed = handlesCrossed
+ )
+
+ /**
+ * Data necessary to set up a [SelectionManager.selectAll] unit test.
+ *
+ * @param text The text for the [FakeSelectable] to return in [Selectable.getText].
+ * @param selection The selection for the [FakeSelectable] to return in
+ * [Selectable.getSelectAllSelection].
+ */
+ private data class SelectAllData(
+ val text: String,
+ val selection: TextRange?,
+ )
+
+ private fun selectAllTest(
+ expectedSelection: Selection?,
+ expectedSubSelectionRanges: Map<Long, TextRange>,
+ vararg selectableStates: SelectAllData,
+ ) {
+ val selectables = selectableStates.mapIndexed { index, item ->
+ val id = index + 1L
+ val range = item.selection
+ FakeSelectable().apply {
+ selectableId = id
+ textToReturn = AnnotatedString(item.text)
+ fakeSelectAllSelection = range?.let {
+ Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = it.start,
+ selectableId = id
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = it.end,
+ selectableId = id
+ ),
+ handlesCrossed = it.reversed
+ )
+ }
+ }
+ }
+
+ val registrar = SelectionRegistrarImpl().apply {
+ selectables.fastForEach { subscribe(it) }
+ }
+
+ val expectedSubSelections = expectedSubSelectionRanges.mapValues { (id, range) ->
+ expectedSelection(
+ startOffset = range.start,
+ endOffset = range.end,
+ startSelectableId = id,
+ endSelectableId = id,
+ handlesCrossed = range.start > range.end
+ )
+ }
+ .toLongObjectMap()
+
+ SelectionManager(registrar).apply {
+ containerLayoutCoordinates = MockCoordinates()
+ onSelectionChange = { newSelection ->
+ if (expectedSelection == null) {
+ fail("Expected no selection update, but received one anyways.")
+ }
+ assertThat(newSelection).isEqualTo(expectedSelection)
+ }
+ selectAll()
+ }
+
+ assertThat(registrar.subselections).isEqualTo(expectedSubSelections)
+ }
+ // endregion selectAll Tests
+
+ private fun <T> Map<Long, T>.toLongObjectMap(): LongObjectMap<T> =
+ mutableLongObjectMapOf<T>().apply {
+ this@toLongObjectMap.keys.forEach { key -> put(key, this@toLongObjectMap[key]!!) }
+ }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
index 20a8d5f..556bcb0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
@@ -42,10 +42,11 @@
}
override fun calculateDistanceTo(targetIndex: Int): Float {
- val visibleItem =
- state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+ val layoutInfo = state.layoutInfo
+ if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+ val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
return if (visibleItem == null) {
- val averageSize = visibleItemsAverageSize
+ val averageSize = calculateVisibleItemsAverageSize(layoutInfo)
val indexesDiff = targetIndex - firstVisibleItemIndex
(averageSize * indexesDiff).toFloat() - firstVisibleItemScrollOffset
} else {
@@ -57,11 +58,9 @@
state.scroll(block = block)
}
- private val visibleItemsAverageSize: Int
- get() {
- val layoutInfo = state.layoutInfo
- val visibleItems = layoutInfo.visibleItemsInfo
- val itemsSum = visibleItems.fastSumBy { it.size }
- return itemsSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
- }
+ private fun calculateVisibleItemsAverageSize(layoutInfo: LazyListLayoutInfo): Int {
+ val visibleItems = layoutInfo.visibleItemsInfo
+ val itemsSum = visibleItems.fastSumBy { it.size }
+ return itemsSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
+ }
}
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
index dc29f8c..83840e9 100644
--- 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
@@ -147,21 +147,38 @@
} 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()
+ if (indexToPrefetch in 0 until layoutInfo.totalItemsCount) {
+ if (indexToPrefetch != this@DefaultLazyListPrefetchStrategy.indexToPrefetch) {
+ 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
+ )
}
- this@DefaultLazyListPrefetchStrategy.wasScrollingForward = scrollingForward
- this@DefaultLazyListPrefetchStrategy.indexToPrefetch = indexToPrefetch
- currentPrefetchHandle = schedulePrefetch(
- indexToPrefetch
- )
+ if (scrollingForward) {
+ val lastItem = layoutInfo.visibleItemsInfo.last()
+ val spacing = layoutInfo.mainAxisItemSpacing
+ val distanceToPrefetchItem =
+ lastItem.offset + lastItem.size + spacing - layoutInfo.viewportEndOffset
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToPrefetchItem < -delta) {
+ currentPrefetchHandle?.markAsUrgent()
+ }
+ } else {
+ val firstItem = layoutInfo.visibleItemsInfo.first()
+ val distanceToPrefetchItem = layoutInfo.viewportStartOffset - firstItem.offset
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToPrefetchItem < delta) {
+ currentPrefetchHandle?.markAsUrgent()
+ }
+ }
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
index 78e6ac9..5fa07f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -37,27 +37,25 @@
override val itemCount: Int get() = state.layoutInfo.totalItemsCount
- private val visibleItemsAverageSize: Int
- get() = calculateLineAverageMainAxisSize(state.layoutInfo)
-
override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
state.snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true)
}
override fun calculateDistanceTo(targetIndex: Int): Float {
- val visibleItem =
- state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+ val layoutInfo = state.layoutInfo
+ if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+ val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
return if (visibleItem == null) {
val slotsPerLine = state.slotsPerLine
- val averageLineMainAxisSize = visibleItemsAverageSize
+ val averageLineMainAxisSize = calculateLineAverageMainAxisSize(layoutInfo)
val before = targetIndex < firstVisibleItemIndex
val linesDiff =
(targetIndex - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
slotsPerLine
(averageLineMainAxisSize * linesDiff).toFloat() - firstVisibleItemScrollOffset
} else {
- if (state.layoutInfo.orientation == Orientation.Vertical) {
+ if (layoutInfo.orientation == Orientation.Vertical) {
visibleItem.offset.y
} else {
visibleItem.offset.x
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index ab042b2..d12d075 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
import androidx.compose.foundation.gestures.stopScroll
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -31,6 +32,7 @@
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -81,19 +83,26 @@
* A state object that can be hoisted to control and observe scrolling.
*
* In most cases, this will be created via [rememberLazyGridState].
- *
- * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
- * @param firstVisibleItemScrollOffset the initial value for
- * [LazyGridState.firstVisibleItemScrollOffset]
*/
@OptIn(ExperimentalFoundationApi::class)
@Stable
-class LazyGridState constructor(
+class LazyGridState internal constructor(
firstVisibleItemIndex: Int = 0,
- firstVisibleItemScrollOffset: Int = 0
+ firstVisibleItemScrollOffset: Int = 0,
+ prefetchScheduler: PrefetchScheduler?,
) : ScrollableState {
/**
+ * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [LazyGridState.firstVisibleItemScrollOffset]
+ */
+ constructor(
+ firstVisibleItemIndex: Int = 0,
+ firstVisibleItemScrollOffset: Int = 0
+ ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, null)
+
+ /**
* The holder class for the current scroll position.
*/
private val scrollPosition =
@@ -413,23 +422,40 @@
}
closestNextItemToPrefetch = info.visibleItemsInfo.first().index - 1
}
- if (lineToPrefetch != this.lineToPrefetch &&
- closestNextItemToPrefetch 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 line is already visible it is safe to call the method anyway
- // as it will be no-op
- currentLinePrefetchHandles.forEach { it.cancel() }
+ if (closestNextItemToPrefetch in 0 until info.totalItemsCount) {
+ if (lineToPrefetch != this.lineToPrefetch) {
+ 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 line is already visible it is safe to call the method anyway
+ // as it will be no-op
+ currentLinePrefetchHandles.forEach { it.cancel() }
+ }
+ this.wasScrollingForward = scrollingForward
+ this.lineToPrefetch = lineToPrefetch
+ currentLinePrefetchHandles.clear()
+ info.prefetchInfoRetriever(lineToPrefetch).fastForEach {
+ currentLinePrefetchHandles.add(
+ prefetchState.schedulePrefetch(it.first, it.second)
+ )
+ }
}
- this.wasScrollingForward = scrollingForward
- this.lineToPrefetch = lineToPrefetch
- currentLinePrefetchHandles.clear()
- info.prefetchInfoRetriever(lineToPrefetch).fastForEach {
- currentLinePrefetchHandles.add(
- prefetchState.schedulePrefetch(it.first, it.second)
- )
+ if (scrollingForward) {
+ val lastItem = info.visibleItemsInfo.last()
+ val distanceToPrefetchItem = lastItem.offsetOnMainAxis(info.orientation) +
+ lastItem.mainAxisSizeWithSpacings - info.viewportEndOffset
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToPrefetchItem < -delta) {
+ currentLinePrefetchHandles.forEach { it.markAsUrgent() }
+ }
+ } else {
+ val firstItem = info.visibleItemsInfo.first()
+ val distanceToPrefetchItem = info.viewportStartOffset -
+ firstItem.offsetOnMainAxis(info.orientation)
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToPrefetchItem < delta) {
+ currentLinePrefetchHandles.forEach { it.markAsUrgent() }
+ }
}
}
}
@@ -454,7 +480,7 @@
}
}
- internal val prefetchState = LazyLayoutPrefetchState()
+ internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler)
private val numOfItemsToTeleport: Int get() = 100 * slotsPerLine
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 11c2a13..20d6ca5 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
@@ -79,6 +79,15 @@
* was precomposed already it will be disposed.
*/
fun cancel()
+
+ /**
+ * Marks this prefetch request as urgent, which is a way to communicate that the requested
+ * item is expected to be needed during the next frame.
+ *
+ * For urgent requests we can proceed with doing the prefetch even if the available time
+ * in the frame is less than we spend on similar prefetch requests on average.
+ */
+ fun markAsUrgent()
}
private inner class NestedPrefetchScopeImpl : NestedPrefetchScope {
@@ -169,6 +178,7 @@
@ExperimentalFoundationApi
private object DummyHandle : PrefetchHandle {
override fun cancel() {}
+ override fun markAsUrgent() {}
}
/**
@@ -212,6 +222,7 @@
private val isComposed get() = precomposeHandle != null
private var hasResolvedNestedPrefetches = false
private var nestedPrefetchController: NestedPrefetchController? = null
+ private var isUrgent = false
private val isValid
get() = !isCanceled &&
@@ -225,13 +236,24 @@
}
}
+ override fun markAsUrgent() {
+ isUrgent = true
+ }
+
+ private fun PrefetchRequestScope.shouldExecute(average: Long): Boolean {
+ val available = availableTimeNanos()
+ // even for urgent request we only do the work if we have time available, as otherwise
+ // it is better to just return early to allow the next frame to start and do the work.
+ return (isUrgent && available > 0) || average < available
+ }
+
override fun PrefetchRequestScope.execute(): Boolean {
if (!isValid) {
return false
}
if (!isComposed) {
- if (prefetchMetrics.averageCompositionTimeNanos < availableTimeNanos) {
+ if (shouldExecute(prefetchMetrics.averageCompositionTimeNanos)) {
prefetchMetrics.recordCompositionTiming {
trace("compose:lazy:prefetch:compose") {
performComposition()
@@ -242,27 +264,35 @@
}
}
- // Nested prefetch logic is best-effort: if nested LazyLayout children are
- // added/removed/updated after we've resolved nested prefetch states here or resolved
- // nestedPrefetchRequests below, those changes won't be taken into account.
- if (!hasResolvedNestedPrefetches) {
- if (availableTimeNanos > 0) {
- trace("compose:lazy:prefetch:resolve-nested") {
- nestedPrefetchController = resolveNestedPrefetchStates()
- hasResolvedNestedPrefetches = true
+ // if the request is urgent we better proceed with the measuring straight away instead
+ // of spending time trying to split the work more via nested prefetch. nested prefetch
+ // is always an estimation and it could potentially do work we will not need in the end,
+ // but the measuring will only do exactly the needed work (including composing nested
+ // lazy layouts)
+ if (!isUrgent) {
+ // Nested prefetch logic is best-effort: if nested LazyLayout children are
+ // added/removed/updated after we've resolved nested prefetch states here or resolved
+ // nestedPrefetchRequests below, those changes won't be taken into account.
+ if (!hasResolvedNestedPrefetches) {
+ if (availableTimeNanos() > 0) {
+ trace("compose:lazy:prefetch:resolve-nested") {
+ nestedPrefetchController = resolveNestedPrefetchStates()
+ hasResolvedNestedPrefetches = true
+ }
+ } else {
+ return true
}
- } else {
+ }
+
+ val hasMoreWork =
+ nestedPrefetchController?.run { executeNestedPrefetches() } ?: false
+ if (hasMoreWork) {
return true
}
}
- val hasMoreWork = nestedPrefetchController?.run { executeNestedPrefetches() } ?: false
- if (hasMoreWork) {
- return true
- }
-
if (!isMeasured && constraints != null) {
- if (prefetchMetrics.averageMeasureTimeNanos < availableTimeNanos) {
+ if (shouldExecute(prefetchMetrics.averageMeasureTimeNanos)) {
prefetchMetrics.recordMeasureTiming {
trace("compose:lazy:prefetch:measure") {
performMeasure(constraints)
@@ -349,7 +379,7 @@
trace("compose:lazy:prefetch:nested") {
while (stateIndex < states.size) {
if (requestsByState[stateIndex] == null) {
- if (availableTimeNanos <= 0) {
+ if (availableTimeNanos() <= 0) {
// When we have time again, we'll resolve nested requests for this
// state
return true
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
index d3497f8..131eb4f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
@@ -75,5 +75,5 @@
* How much time is available to do prefetch work. Implementations of [PrefetchRequest] should
* do their best to fit their work into this time without going over.
*/
- val availableTimeNanos: Long
+ fun availableTimeNanos(): Long
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
index 9d9853c..3b8621c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
@@ -44,16 +44,17 @@
}
override fun calculateDistanceTo(targetIndex: Int): Float {
- val visibleItem =
- state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+ val layoutInfo = state.layoutInfo
+ if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+ val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
return if (visibleItem == null) {
- val averageMainAxisItemSize = visibleItemsAverageSize
+ val averageMainAxisItemSize = calculateVisibleItemsAverageSize(layoutInfo)
val laneCount = state.laneCount
val lineDiff = targetIndex / laneCount - firstVisibleItemIndex / laneCount
averageMainAxisItemSize * lineDiff.toFloat() - firstVisibleItemScrollOffset
} else {
- if (state.layoutInfo.orientation == Orientation.Vertical) {
+ if (layoutInfo.orientation == Orientation.Vertical) {
visibleItem.offset.y
} else {
visibleItem.offset.x
@@ -65,17 +66,15 @@
state.scroll(block = block)
}
- private val visibleItemsAverageSize: Int
- get() {
- val layoutInfo = state.layoutInfo
- val visibleItems = layoutInfo.visibleItemsInfo
- val itemSizeSum = visibleItems.fastSumBy {
- if (layoutInfo.orientation == Orientation.Vertical) {
- it.size.height
- } else {
- it.size.width
- }
+ private fun calculateVisibleItemsAverageSize(layoutInfo: LazyStaggeredGridLayoutInfo): Int {
+ val visibleItems = layoutInfo.visibleItemsInfo
+ val itemSizeSum = visibleItems.fastSumBy {
+ if (layoutInfo.orientation == Orientation.Vertical) {
+ it.size.height
+ } else {
+ it.size.width
}
- return itemSizeSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
}
+ return itemSizeSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
+ }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index 09cb286..3a15ecb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -34,6 +34,7 @@
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
@@ -82,9 +83,10 @@
* In most cases, it should be created via [rememberLazyStaggeredGridState].
*/
@OptIn(ExperimentalFoundationApi::class)
-class LazyStaggeredGridState private constructor(
+class LazyStaggeredGridState internal constructor(
initialFirstVisibleItems: IntArray,
initialFirstVisibleOffsets: IntArray,
+ prefetchScheduler: PrefetchScheduler?
) : ScrollableState {
/**
* @param initialFirstVisibleItemIndex initial value for [firstVisibleItemIndex]
@@ -95,7 +97,8 @@
initialFirstVisibleItemOffset: Int = 0
) : this(
intArrayOf(initialFirstVisibleItemIndex),
- intArrayOf(initialFirstVisibleItemOffset)
+ intArrayOf(initialFirstVisibleItemOffset),
+ null
)
/**
@@ -178,7 +181,7 @@
internal var prefetchingEnabled: Boolean = true
/** prefetch state used for precomputing items in the direction of scroll */
- internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState()
+ internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState(prefetchScheduler)
/** state controlling the scroll */
private val scrollableState = ScrollableState { -onScroll(-it) }
@@ -584,7 +587,7 @@
)
},
restore = {
- LazyStaggeredGridState(it[0], it[1])
+ LazyStaggeredGridState(it[0], it[1], null)
}
)
}
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 edf220c..a4310c5 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
@@ -36,6 +36,7 @@
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
@@ -147,18 +148,26 @@
/**
* The state that can be used to control [VerticalPager] and [HorizontalPager]
- * @param currentPage The initial page to be displayed
- * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
- * the layout.
*/
@OptIn(ExperimentalFoundationApi::class)
@Stable
-abstract class PagerState(
+abstract class PagerState internal constructor(
currentPage: Int = 0,
- @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f
+ @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
+ prefetchScheduler: PrefetchScheduler? = null
) : ScrollableState {
/**
+ * @param currentPage The initial page to be displayed
+ * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
+ * the layout.
+ */
+ constructor(
+ currentPage: Int = 0,
+ @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f
+ ) : this(currentPage, currentPageOffsetFraction, null)
+
+ /**
* The total amount of pages present in this pager. The source of this data should be
* observable.
*/
@@ -431,7 +440,7 @@
*/
val currentPageOffsetFraction: Float get() = scrollPosition.currentPageOffsetFraction
- internal val prefetchState = LazyLayoutPrefetchState()
+ internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler)
internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
@@ -716,21 +725,38 @@
} else {
info.visiblePagesInfo.first().index - info.beyondViewportPageCount - PagesToPrefetch
}
- if (indexToPrefetch != this.indexToPrefetch &&
- indexToPrefetch in 0 until pageCount
- ) {
- if (wasPrefetchingForward != isPrefetchingForward) {
- // 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()
+ if (indexToPrefetch in 0 until pageCount) {
+ if (indexToPrefetch != this.indexToPrefetch) {
+ if (wasPrefetchingForward != isPrefetchingForward) {
+ // 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.wasPrefetchingForward = isPrefetchingForward
+ this.indexToPrefetch = indexToPrefetch
+ currentPrefetchHandle = prefetchState.schedulePrefetch(
+ indexToPrefetch, premeasureConstraints
+ )
}
- this.wasPrefetchingForward = isPrefetchingForward
- this.indexToPrefetch = indexToPrefetch
- currentPrefetchHandle = prefetchState.schedulePrefetch(
- indexToPrefetch, premeasureConstraints
- )
+ if (isPrefetchingForward) {
+ val lastItem = info.visiblePagesInfo.last()
+ val pageSize = info.pageSize + info.pageSpacing
+ val distanceToReachNextItem =
+ lastItem.offset + pageSize - info.viewportEndOffset
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToReachNextItem < delta) {
+ currentPrefetchHandle?.markAsUrgent()
+ }
+ } else {
+ val firstItem = info.visiblePagesInfo.first()
+ val distanceToReachNextItem = info.viewportStartOffset - firstItem.offset
+ // if in the next frame we will get the same delta will we reach the item?
+ if (distanceToReachNextItem < -delta) {
+ currentPrefetchHandle?.markAsUrgent()
+ }
+ }
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 4fad5b6..4cb66ac 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -58,6 +58,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFold
import androidx.compose.ui.util.fastForEach
@@ -277,7 +278,7 @@
selectionRegistrar.onSelectionUpdateSelectAll =
{ isInTouchMode, selectableId ->
- val (newSelection, newSubselection) = selectAll(
+ val (newSelection, newSubselection) = selectAllInSelectable(
selectableId = selectableId,
previousSelection = selection,
)
@@ -409,7 +410,7 @@
return coordinates
}
- internal fun selectAll(
+ internal fun selectAllInSelectable(
selectableId: Long,
previousSelection: Selection?
): Pair<Selection?, LongObjectMap<Selection>> {
@@ -428,6 +429,70 @@
}
/**
+ * Returns whether the selection encompasses the entire container.
+ */
+ internal fun isEntireContainerSelected(): Boolean {
+ val selectables = selectionRegistrar.sort(requireContainerCoordinates())
+
+ // If there are no selectables, then an empty selection spans the entire container.
+ if (selectables.isEmpty()) return true
+
+ // Since some text exists, we must make sure that every selectable is fully selected.
+ return selectables.fastAll {
+ val text = it.getText()
+ if (text.isEmpty()) return@fastAll true // empty text is inherently fully selected
+
+ // If a non-empty selectable isn't included in the sub-selections,
+ // then some text in the container is not selected.
+ val subSelection = selectionRegistrar.subselections[it.selectableId]
+ ?: return@fastAll false
+
+ val selectionStart = subSelection.start.offset
+ val selectionEnd = subSelection.end.offset
+
+ // The selection could be reversed,
+ // so just verify that the difference between the two offsets matches the text length
+ (selectionStart - selectionEnd).absoluteValue == text.length
+ }
+ }
+
+ /**
+ * Creates and sets a selection spanning the entire container.
+ */
+ internal fun selectAll() {
+ val selectables = selectionRegistrar.sort(requireContainerCoordinates())
+ if (selectables.isEmpty()) return
+
+ var firstSubSelection: Selection? = null
+ var lastSubSelection: Selection? = null
+ val newSubSelections = mutableLongObjectMapOf<Selection>().apply {
+ selectables.fastForEach { selectable ->
+ val subSelection = selectable.getSelectAllSelection() ?: return@fastForEach
+ if (firstSubSelection == null) firstSubSelection = subSelection
+ lastSubSelection = subSelection
+ put(selectable.selectableId, subSelection)
+ }
+ }
+
+ if (newSubSelections.isEmpty()) return
+
+ // first/last sub selections are implied to be non-null from here on out
+ val newSelection = if (firstSubSelection === lastSubSelection) {
+ firstSubSelection
+ } else {
+ Selection(
+ start = firstSubSelection!!.start,
+ end = lastSubSelection!!.end,
+ handlesCrossed = false,
+ )
+ }
+
+ selectionRegistrar.subselections = newSubSelections
+ onSelectionChange(newSelection)
+ previousSelectionLayout = null
+ }
+
+ /**
* Returns whether the start and end anchors are equal.
*
* It is possible that this returns true, but the selection is still empty because it has
@@ -521,9 +586,13 @@
}
val textToolbar = textToolbar ?: return
- if (showToolbar && isInTouchMode && isNonEmptySelection()) {
+ if (showToolbar && isInTouchMode) {
val rect = getContentRect() ?: return
- textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
+ textToolbar.showMenu(
+ rect = rect,
+ onCopyRequested = if (isNonEmptySelection()) ::toolbarCopy else null,
+ onSelectAllRequested = if (isEntireContainerSelected()) null else ::selectAll,
+ )
} else if (textToolbar.status == TextToolbarStatus.Shown) {
textToolbar.hide()
}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index a905398..bda7255 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -49,6 +49,19 @@
</intent-filter>
</activity>
<activity
+ android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
+ android:label="C StaticScrollingWithChrome Init"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".TrivialStartupTracingActivity"
android:label="C TrivialTracing"
android:exported="true">
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
new file mode 100644
index 0000000..87810a2
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
@@ -0,0 +1,265 @@
+/*
+ * 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.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.trace
+
+class StaticScrollingContentWithChromeInitialCompositionActivity : ComponentActivity() {
+
+ private val onlyPerformComposition: Boolean
+ get() = intent.action == "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ if (onlyPerformComposition) {
+ ComposeOnlyLayout {
+ StaticScrollingContentWithChrome(
+ modifier = Modifier
+ .onPlaced { _ ->
+ throw RuntimeException(
+ "Content was placed, but should only be composed"
+ )
+ }
+ .drawWithContent {
+ throw RuntimeException(
+ "Content was drawn, but should only be composed"
+ )
+ }
+ )
+ }
+ } else {
+ StaticScrollingContentWithChrome()
+ }
+ }
+ }
+}
+
+/**
+ * A layout that will compose all of the [content], but will not place
+ * (and therefore not layout or draw) any of its children.
+ *
+ * This is useful for this benchmark as we care about the composition time. A major limitation
+ * of this approach is that any content in a SubcomposeLayout will not be composed
+ * and will not contribute to the overall measured time of this test.
+ */
+@Composable
+private fun ComposeOnlyLayout(
+ content: @Composable () -> Unit
+) {
+ Layout(content) { _, _ -> layout(0, 0) {} }
+}
+
+@Preview
+@Composable
+private fun StaticScrollingContentWithChrome(
+ modifier: Modifier = Modifier
+) = trace(sectionName = "StaticScrollingContentWithChrome") {
+ Column(modifier) {
+ TopBar()
+ ScrollingContent(modifier = Modifier.weight(1f))
+ BottomBar()
+ }
+}
+
+@Composable
+private fun TopBar(modifier: Modifier = Modifier) {
+ TopAppBar(
+ modifier = modifier,
+ title = {
+ Column {
+ Text(
+ "Initial Composition Macrobench",
+ style = MaterialTheme.typography.subtitle1,
+ maxLines = 1
+ )
+ Text(
+ "Static Scrolling Content w/ Chrome",
+ style = MaterialTheme.typography.caption,
+ maxLines = 1
+ )
+ }
+ },
+ navigationIcon = {
+ Button(onClick = {}) {
+ Icon(Icons.Default.Close, "Dismiss")
+ }
+ },
+ actions = {
+ Button(onClick = {}) {
+ Icon(Icons.Default.MoreVert, "Actions")
+ }
+ }
+ )
+}
+
+@Composable
+private fun BottomBar(modifier: Modifier = Modifier) {
+ BottomNavigation(modifier = modifier) {
+ BottomNavigationItem(
+ selected = true,
+ onClick = {},
+ icon = { Icon(Icons.Default.Home, "Home") }
+ )
+ BottomNavigationItem(
+ selected = false,
+ onClick = {},
+ icon = { Icon(Icons.Default.Add, "Add") }
+ )
+ }
+}
+
+@Composable
+private fun ScrollingContent(modifier: Modifier = Modifier) {
+ Column(
+ modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(vertical = 16.dp)
+ ) {
+ Item(
+ color = Color.DarkGray,
+ icon = Icons.Filled.Info,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .aspectRatio(16f / 9f)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ )
+
+ repeat(5) { iteration ->
+ CardGroup(
+ title = "Group ${4 * iteration}",
+ groupIcon = Icons.Filled.Person,
+ groupColor = Color(0xFF1967D2)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 1}",
+ groupIcon = Icons.Filled.Favorite,
+ groupColor = Color(0xFFC5221F)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 2}",
+ groupIcon = Icons.Filled.Star,
+ groupColor = Color(0xFFF29900)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 3}",
+ groupIcon = Icons.Filled.Place,
+ groupColor = Color(0xFF188038)
+ )
+ }
+ }
+}
+
+@Composable
+private fun CardGroup(
+ title: String,
+ groupIcon: ImageVector,
+ groupColor: Color,
+ modifier: Modifier = Modifier,
+ count: Int = 10
+) {
+ Column(
+ modifier = modifier
+ ) {
+ Text(
+ title,
+ style = MaterialTheme.typography.h6,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ Row(
+ modifier = Modifier
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 12.dp)
+ ) {
+ repeat(count) {
+ Item(
+ color = groupColor,
+ icon = groupIcon,
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .size(64.dp)
+ .clip(RoundedCornerShape(4.dp))
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Item(
+ color: Color,
+ icon: ImageVector,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.background(color),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(icon, null, tint = Color.White)
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
index 003b307..c070d43 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
@@ -19,6 +19,8 @@
import android.content.Intent
import android.graphics.Point
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.platform.app.InstrumentationRegistry
@@ -41,11 +43,12 @@
device = UiDevice.getInstance(instrumentation)
}
+ @OptIn(ExperimentalMetricApi::class)
@Test
fun start() {
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
- metrics = listOf(FrameTimingMetric()),
+ metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
compilationMode = CompilationMode.Full(),
iterations = 8,
setupBlock = {
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
new file mode 100644
index 0000000..b573368
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.testutils.createStartupCompilationParams
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class StaticScrollingContentWithChromeInitialCompositionBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun initialComposition() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = startupMode,
+ packageName = "androidx.compose.integration.macrobenchmark.target"
+ ) {
+ action = "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+ }
+
+ @Test
+ fun firstFrame() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = startupMode,
+ packageName = "androidx.compose.integration.macrobenchmark.target"
+ ) {
+ action = "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY"
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
+ @JvmStatic
+ fun parameters() = createStartupCompilationParams()
+ }
+}
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 7c87844..69623fb 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -646,12 +646,10 @@
}
@SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class RippleConfiguration {
- ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+ ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
method public long getColor();
method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
- method public boolean isEnabled();
property public final long color;
- property public final boolean isEnabled;
property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
}
@@ -662,11 +660,11 @@
}
public final class RippleKt {
- method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> getLocalRippleConfiguration();
+ method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> getLocalRippleConfiguration();
method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
- property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> LocalRippleConfiguration;
+ property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> LocalRippleConfiguration;
property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
}
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 7c87844..69623fb 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -646,12 +646,10 @@
}
@SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class RippleConfiguration {
- ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+ ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
method public long getColor();
method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
- method public boolean isEnabled();
property public final long color;
- property public final boolean isEnabled;
property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
}
@@ -662,11 +660,11 @@
}
public final class RippleKt {
- method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> getLocalRippleConfiguration();
+ method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> getLocalRippleConfiguration();
method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
- property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> LocalRippleConfiguration;
+ property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> LocalRippleConfiguration;
property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
}
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
index 15c1d55..c52867d 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
@@ -1015,10 +1015,6 @@
fun rippleConfiguration_disabled_dragged() {
val interactionSource = MutableInteractionSource()
- val rippleConfiguration = RippleConfiguration(
- isEnabled = false
- )
-
var scope: CoroutineScope? = null
rule.setContent {
@@ -1026,7 +1022,7 @@
MaterialTheme {
Surface {
CompositionLocalProvider(
- LocalRippleConfiguration provides rippleConfiguration
+ LocalRippleConfiguration provides null
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
RippleBoxWithBackground(
@@ -1072,7 +1068,7 @@
val contentColor = Color.Black
- var rippleConfiguration by mutableStateOf(RippleConfiguration())
+ var rippleConfiguration: RippleConfiguration? by mutableStateOf(RippleConfiguration())
var scope: CoroutineScope? = null
@@ -1119,7 +1115,6 @@
}
val newConfiguration = RippleConfiguration(
- isEnabled = true,
color = Color.Red,
rippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
)
@@ -1147,7 +1142,7 @@
}
rule.runOnUiThread {
- rippleConfiguration = RippleConfiguration(isEnabled = false)
+ rippleConfiguration = null
}
with(rule.onNodeWithTag(Tag)) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
index 50cca67..7b371e3 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
@@ -200,7 +200,8 @@
/**
* CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a
* tree-local 'override' for ripples used inside components that you cannot directly control, such
- * as to change the color of a specific component's ripple, or disable it entirely.
+ * as to change the color of a specific component's ripple, or disable it entirely by providing
+ * `null`.
*
* In most cases you should rely on the default theme behavior for consistency with other components
* - this exists as an escape hatch for individual components and is not intended to be used for
@@ -211,15 +212,15 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterialApi
@ExperimentalMaterialApi
-val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration> =
+val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration?> =
compositionLocalOf { RippleConfiguration() }
/**
* Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases
* the default values should be used, for custom design system use cases you should instead
- * build your own custom ripple using [createRippleModifierNode].
+ * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide
+ * `null` using [LocalRippleConfiguration].
*
- * @param isEnabled whether the ripple is enabled. If false, no ripple will be rendered
* @param color the color override for the ripple. If [Color.Unspecified], then the default color
* from the theme will be used instead. Note that if the ripple has a color explicitly set with
* the parameter on [ripple], that will always be used instead of this value.
@@ -229,7 +230,6 @@
@Immutable
@ExperimentalMaterialApi
class RippleConfiguration(
- val isEnabled: Boolean = true,
val color: Color = Color.Unspecified,
val rippleAlpha: RippleAlpha? = null
) {
@@ -237,7 +237,6 @@
if (this === other) return true
if (other !is RippleConfiguration) return false
- if (isEnabled != other.isEnabled) return false
if (color != other.color) return false
if (rippleAlpha != other.rippleAlpha) return false
@@ -245,14 +244,13 @@
}
override fun hashCode(): Int {
- var result = isEnabled.hashCode()
- result = 31 * result + color.hashCode()
+ var result = color.hashCode()
result = 31 * result + (rippleAlpha?.hashCode() ?: 0)
return result
}
override fun toString(): String {
- return "RippleConfiguration(enabled=$isEnabled, color=$color, rippleAlpha=$rippleAlpha)"
+ return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)"
}
}
@@ -353,13 +351,14 @@
}
/**
- * Handles changes to [RippleConfiguration.isEnabled]. Changes to [RippleConfiguration.color] and
- * [RippleConfiguration.rippleAlpha] are handled as part of the ripple definition.
+ * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to
+ * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of
+ * the ripple definition.
*/
private fun updateConfiguration() {
observeReads {
val configuration = currentValueOf(LocalRippleConfiguration)
- if (!configuration.isEnabled) {
+ if (configuration == null) {
removeRipple()
} else {
if (rippleNode == null) attachNewRipple()
@@ -373,8 +372,10 @@
if (userDefinedColor.isSpecified) {
userDefinedColor
} else {
+ // If this is null, the ripple will be removed, so this should always be non-null in
+ // normal use
val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
- if (rippleConfiguration.color.isSpecified) {
+ if (rippleConfiguration?.color?.isSpecified == true) {
rippleConfiguration.color
} else {
RippleDefaults.rippleColor(
@@ -385,8 +386,10 @@
}
}
val calculateRippleAlpha = {
+ // If this is null, the ripple will be removed, so this should always be non-null in
+ // normal use
val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
- rippleConfiguration.rippleAlpha ?: RippleDefaults.rippleAlpha(
+ rippleConfiguration?.rippleAlpha ?: RippleDefaults.rippleAlpha(
contentColor = currentValueOf(LocalContentColor),
lightTheme = currentValueOf(LocalColors).isLight
)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index cfed154..83a3415 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1255,12 +1255,10 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class RippleConfiguration {
- ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+ ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
method public long getColor();
method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
- method public boolean isEnabled();
property public final long color;
- property public final boolean isEnabled;
property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
}
@@ -1271,11 +1269,11 @@
}
public final class RippleKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> getLocalRippleConfiguration();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> getLocalRippleConfiguration();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
- property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> LocalRippleConfiguration;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> LocalRippleConfiguration;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index cfed154..83a3415 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1255,12 +1255,10 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class RippleConfiguration {
- ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+ ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
method public long getColor();
method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
- method public boolean isEnabled();
property public final long color;
- property public final boolean isEnabled;
property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
}
@@ -1271,11 +1269,11 @@
}
public final class RippleKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> getLocalRippleConfiguration();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> getLocalRippleConfiguration();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
- property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> LocalRippleConfiguration;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> LocalRippleConfiguration;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
index a802cae..80cc29d 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
@@ -503,10 +503,6 @@
fun rippleConfiguration_disabled_dragged() {
val interactionSource = MutableInteractionSource()
- val rippleConfiguration = RippleConfiguration(
- isEnabled = false
- )
-
var scope: CoroutineScope? = null
rule.setContent {
@@ -514,7 +510,7 @@
MaterialTheme {
Surface {
CompositionLocalProvider(
- LocalRippleConfiguration provides rippleConfiguration
+ LocalRippleConfiguration provides null
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
RippleBoxWithBackground(
@@ -560,7 +556,7 @@
val contentColor = Color.Black
- var rippleConfiguration by mutableStateOf(RippleConfiguration())
+ var rippleConfiguration: RippleConfiguration? by mutableStateOf(RippleConfiguration())
var scope: CoroutineScope? = null
@@ -607,7 +603,6 @@
}
val newConfiguration = RippleConfiguration(
- isEnabled = true,
color = Color.Red,
rippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
)
@@ -635,7 +630,7 @@
}
rule.runOnUiThread {
- rippleConfiguration = RippleConfiguration(isEnabled = false)
+ rippleConfiguration = null
}
with(rule.onNodeWithTag(Tag)) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
index 322b33f..485b51c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
@@ -165,7 +165,8 @@
/**
* CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a
* tree-local 'override' for ripples used inside components that you cannot directly control, such
- * as to change the color of a specific component's ripple, or disable it entirely.
+ * as to change the color of a specific component's ripple, or disable it entirely by providing
+ * `null`.
*
* In most cases you should rely on the default theme behavior for consistency with other components
* - this exists as an escape hatch for individual components and is not intended to be used for
@@ -176,15 +177,15 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
-val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration> =
+val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration?> =
compositionLocalOf { RippleConfiguration() }
/**
* Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases
* the default values should be used, for custom design system use cases you should instead
- * build your own custom ripple using [createRippleModifierNode].
+ * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide
+ * `null` using [LocalRippleConfiguration].
*
- * @param isEnabled whether the ripple is enabled. If false, no ripple will be rendered
* @param color the color override for the ripple. If [Color.Unspecified], then the default color
* from the theme will be used instead. Note that if the ripple has a color explicitly set with
* the parameter on [ripple], that will always be used instead of this value.
@@ -194,7 +195,6 @@
@Immutable
@ExperimentalMaterial3Api
class RippleConfiguration(
- val isEnabled: Boolean = true,
val color: Color = Color.Unspecified,
val rippleAlpha: RippleAlpha? = null
) {
@@ -202,7 +202,6 @@
if (this === other) return true
if (other !is RippleConfiguration) return false
- if (isEnabled != other.isEnabled) return false
if (color != other.color) return false
if (rippleAlpha != other.rippleAlpha) return false
@@ -210,14 +209,13 @@
}
override fun hashCode(): Int {
- var result = isEnabled.hashCode()
- result = 31 * result + color.hashCode()
+ var result = color.hashCode()
result = 31 * result + (rippleAlpha?.hashCode() ?: 0)
return result
}
override fun toString(): String {
- return "RippleConfiguration(enabled=$isEnabled, color=$color, rippleAlpha=$rippleAlpha)"
+ return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)"
}
}
@@ -311,13 +309,14 @@
}
/**
- * Handles changes to [RippleConfiguration.isEnabled]. Changes to [RippleConfiguration.color] and
- * [RippleConfiguration.rippleAlpha] are handled as part of the ripple definition.
+ * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to
+ * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of
+ * the ripple definition.
*/
private fun updateConfiguration() {
observeReads {
val configuration = currentValueOf(LocalRippleConfiguration)
- if (!configuration.isEnabled) {
+ if (configuration == null) {
removeRipple()
} else {
if (rippleNode == null) attachNewRipple()
@@ -331,8 +330,10 @@
if (userDefinedColor.isSpecified) {
userDefinedColor
} else {
+ // If this is null, the ripple will be removed, so this should always be non-null in
+ // normal use
val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
- if (rippleConfiguration.color.isSpecified) {
+ if (rippleConfiguration?.color?.isSpecified == true) {
rippleConfiguration.color
} else {
currentValueOf(LocalContentColor)
@@ -341,8 +342,10 @@
}
val calculateRippleAlpha = {
+ // If this is null, the ripple will be removed, so this should always be non-null in
+ // normal use
val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
- rippleConfiguration.rippleAlpha ?: RippleDefaults.RippleAlpha
+ rippleConfiguration?.rippleAlpha ?: RippleDefaults.RippleAlpha
}
rippleNode = delegate(createRippleModifierNode(
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
new file mode 100644
index 0000000..9b61a35
--- /dev/null
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
@@ -0,0 +1,437 @@
+/*
+ * 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.runtime.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.random.Random
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SlotTableIntegrationBenchmark : ComposeBenchmarkBase() {
+
+ @UiThreadTest
+ @Test
+ fun create() = runBlockingTestWithFrameClock {
+ measureCompose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeManyGroups() = runBlockingTestWithFrameClock {
+ var includeGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ if (includeGroups) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { includeGroups = false }
+ reset { includeGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeAlternatingGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ key(index) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = false }
+ reset { insertAlternatingGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeManyReplaceGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ Pixel(color = Color(
+ red = 0,
+ green = 2 * index,
+ blue = 0
+ ))
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = false }
+ reset { insertAlternatingGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertManyGroups() = runBlockingTestWithFrameClock {
+ var includeGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ if (includeGroups) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { includeGroups = true }
+ reset { includeGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertAlternatingGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ key(index) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = true }
+ reset { insertAlternatingGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertManyReplaceGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ Pixel(color = Color(
+ red = 0,
+ green = 2 * index,
+ blue = 0
+ ))
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = true }
+ reset { insertAlternatingGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateManyNestedGroups() = runBlockingTestWithFrameClock {
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ val random = remember(seed) { Random(seed) }
+ MatryoshkaLayout(
+ depth = 100,
+ content = {
+ MinimalBox {
+ Pixel(color = Color(random.nextInt()))
+ Pixel(color = Color.Red)
+ Pixel(color = Color.Green)
+ Pixel(color = Color.Blue)
+ }
+ MinimalBox {
+ NonRenderingText("abcdef")
+ }
+ NonRenderingText(
+ text = random.nextString(),
+ textColor = Color(random.nextInt()),
+ textSize = random.nextInt(6, 32).dp,
+ ellipsize = random.nextBoolean(),
+ minLines = random.nextInt(),
+ maxLines = random.nextInt(),
+ )
+ }
+ )
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateDisjointGroups() = runBlockingTestWithFrameClock {
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ MinimalBox {
+ repeat(10) { container ->
+ MinimalBox {
+ MatryoshkaLayout(
+ depth = 100,
+ content = { depth ->
+ if (depth > 50) {
+ val random = Random(seed * container + depth)
+ NonRenderingText(
+ text = random.nextString(),
+ textColor = Color(random.nextInt()),
+ textSize = random.nextInt(6, 32).dp,
+ ellipsize = random.nextBoolean(),
+ minLines = random.nextInt(),
+ maxLines = random.nextInt(),
+ )
+ } else {
+ NonRenderingText("foo")
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateDeepCompositionLocalHierarchy() = runBlockingTestWithFrameClock {
+ val PixelColorLocal = compositionLocalOf { Color.Unspecified }
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ val random = remember(seed) { Random(seed) }
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun reverseGroups() = runBlockingTestWithFrameClock {
+ val originalItems = (1..100).toList()
+ var keys by mutableStateOf(originalItems)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ keys.forEach {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ update { keys = keys.reversed() }
+ reset { keys = originalItems }
+ }
+ }
+}
+
+@Composable
+private fun Pixel(color: Color) {
+ Layout(
+ modifier = Modifier.background(color)
+ ) { _, _ ->
+ layout(1, 1) {}
+ }
+}
+
+@Composable
+private fun NonRenderingText(
+ text: String,
+ textColor: Color = Color.Unspecified,
+ textSize: Dp = Dp.Unspecified,
+ ellipsize: Boolean = false,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ use(text)
+ use(textColor.value.toInt())
+ use(textSize.value)
+ use(ellipsize)
+ use(minLines)
+ use(maxLines)
+ Layout { _, _ ->
+ layout(1, 1) {}
+ }
+}
+
+@Composable
+private fun MinimalBox(
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Layout(content, modifier, MinimalBoxMeasurePolicy)
+}
+
+@Composable
+private fun MatryoshkaLayout(
+ depth: Int,
+ content: @Composable (depth: Int) -> Unit
+) {
+ if (depth <= 0) {
+ content(0)
+ } else {
+ Layout(
+ content = {
+ content(depth)
+ MatryoshkaLayout(depth - 1, content)
+ },
+ measurePolicy = MinimalBoxMeasurePolicy
+ )
+ }
+}
+
+private val MinimalBoxMeasurePolicy = MeasurePolicy { measurables, constraints ->
+ val placeables = measurables.map { it.measure(constraints) }
+ val (usedWidth, usedHeight) = placeables.fold(
+ initial = IntOffset(0, 0)
+ ) { (maxWidth, maxHeight), placeable ->
+ IntOffset(
+ maxOf(maxWidth, placeable.measuredWidth),
+ maxOf(maxHeight, placeable.measuredHeight)
+ )
+ }
+
+ layout(
+ width = usedWidth,
+ height = usedHeight
+ ) {
+ placeables.forEach { it.place(0, 0) }
+ }
+}
+
+private fun Random.nextString(length: Int = 16) = buildString(length) {
+ repeat(length) { append(nextInt('A'.code, 'z'.code).toChar()) }
+}
+
+@Suppress("UNUSED_PARAMETER") private fun use(value: Any?) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Int) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Long) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Float) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Double) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Boolean) {}
diff --git a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
index 37dcfb0..88c4553 100644
--- a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
+++ b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
@@ -141,4 +141,21 @@
-Offset(Float.NaN, Float.NaN)
}
}
+
+ @Test
+ fun testIsFinite() {
+ assertTrue(Offset(10.0f, 20.0f).isFinite)
+ assertTrue(Offset(0.0f, 0.0f).isFinite)
+ assertTrue(Offset(10.0f, -20.0f).isFinite)
+
+ assertFalse(Offset(10.0f, Float.POSITIVE_INFINITY).isFinite)
+ assertFalse(Offset(10.0f, Float.NEGATIVE_INFINITY).isFinite)
+ assertFalse(Offset(Float.POSITIVE_INFINITY, 20.0f).isFinite)
+ assertFalse(Offset(Float.NEGATIVE_INFINITY, 20.0f).isFinite)
+ assertFalse(Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY).isFinite)
+
+ assertFails {
+ Offset(Float.NaN, Float.NaN).isFinite
+ }
+ }
}
diff --git a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
index 01d8f1d..a6b033c 100644
--- a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
+++ b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.geometry
-import org.junit.Assert
+import kotlin.test.assertFails
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
@@ -27,11 +30,11 @@
@Test
fun sizeTimesInt() {
- Assert.assertEquals(
+ assertEquals(
Size(10f, 10f),
Size(2.5f, 2.5f) * 4f
)
- Assert.assertEquals(
+ assertEquals(
Size(10f, 10f),
4f * Size(2.5f, 2.5f)
)
@@ -39,7 +42,7 @@
@Test
fun sizeDivInt() {
- Assert.assertEquals(
+ assertEquals(
Size(10f, 10f),
Size(40f, 40f) / 4f
)
@@ -47,24 +50,24 @@
@Test
fun sizeTimesFloat() {
- Assert.assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
- Assert.assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
+ assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
+ assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
}
@Test
fun sizeDivFloat() {
- Assert.assertEquals(Size(10f, 10f), Size(40f, 40f) / 4f)
+ assertEquals(Size(10f, 10f), Size(40f, 40f) / 4f)
}
@Test
fun sizeTimesDouble() {
- Assert.assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
- Assert.assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
+ assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
+ assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
}
@Test
fun sizeDivDouble() {
- Assert.assertEquals(
+ assertEquals(
Size(10f, 10f),
Size(40f, 40f) / 4.0f
)
@@ -73,23 +76,23 @@
@Test
fun testSizeCopy() {
val size = Size(100f, 200f)
- Assert.assertEquals(size, size.copy())
+ assertEquals(size, size.copy())
}
@Test
fun testSizeCopyOverwriteWidth() {
val size = Size(100f, 200f)
val copy = size.copy(width = 50f)
- Assert.assertEquals(50f, copy.width)
- Assert.assertEquals(200f, copy.height)
+ assertEquals(50f, copy.width)
+ assertEquals(200f, copy.height)
}
@Test
fun testSizeCopyOverwriteHeight() {
val size = Size(100f, 200f)
val copy = size.copy(height = 300f)
- Assert.assertEquals(100f, copy.width)
- Assert.assertEquals(300f, copy.height)
+ assertEquals(100f, copy.width)
+ assertEquals(300f, copy.height)
}
@Test
@@ -137,38 +140,61 @@
fun testSizeLerp() {
val size1 = Size(100f, 200f)
val size2 = Size(300f, 500f)
- Assert.assertEquals(Size(200f, 350f), lerp(size1, size2, 0.5f))
+ assertEquals(Size(200f, 350f), lerp(size1, size2, 0.5f))
}
@Test
fun testIsSpecified() {
- Assert.assertFalse(Size.Unspecified.isSpecified)
- Assert.assertTrue(Size(1f, 1f).isSpecified)
+ assertFalse(Size.Unspecified.isSpecified)
+ assertTrue(Size(1f, 1f).isSpecified)
}
@Test
fun testIsUnspecified() {
- Assert.assertTrue(Size.Unspecified.isUnspecified)
- Assert.assertFalse(Size(1f, 1f).isUnspecified)
+ assertTrue(Size.Unspecified.isUnspecified)
+ assertFalse(Size(1f, 1f).isUnspecified)
}
@Test
fun testTakeOrElseTrue() {
- Assert.assertTrue(Size(1f, 1f).takeOrElse { Size.Unspecified }.isSpecified)
+ assertTrue(Size(1f, 1f).takeOrElse { Size.Unspecified }.isSpecified)
}
@Test
fun testTakeOrElseFalse() {
- Assert.assertTrue(Size.Unspecified.takeOrElse { Size(1f, 1f) }.isSpecified)
+ assertTrue(Size.Unspecified.takeOrElse { Size(1f, 1f) }.isSpecified)
}
@Test
fun testUnspecifiedSizeToString() {
- Assert.assertEquals("Size.Unspecified", Size.Unspecified.toString())
+ assertEquals("Size.Unspecified", Size.Unspecified.toString())
}
@Test
fun testSpecifiedSizeToString() {
- Assert.assertEquals("Size(10.0, 20.0)", Size(10f, 20f).toString())
+ assertEquals("Size(10.0, 20.0)", Size(10f, 20f).toString())
+ }
+
+ @Test
+ fun testIsEmpty() {
+ assertFalse(Size(10.0f, 20.0f).isEmpty())
+ assertFalse(Size(10.0f, Float.POSITIVE_INFINITY).isEmpty())
+ assertFalse(Size(Float.POSITIVE_INFINITY, 20.0f).isEmpty())
+
+ assertTrue(Size(0.0f, 20.0f).isEmpty())
+ assertTrue(Size(10.0f, 0.0f).isEmpty())
+ assertTrue(Size(0.0f, 0.0f).isEmpty())
+ assertTrue(Size(-10.0f, 20.0f).isEmpty())
+ assertTrue(Size(10.0f, -20.0f).isEmpty())
+ assertTrue(Size(0.0f, Float.POSITIVE_INFINITY).isEmpty())
+ assertTrue(Size(Float.POSITIVE_INFINITY, 0.0f).isEmpty())
+ assertTrue(Size(0.0f, Float.NEGATIVE_INFINITY).isEmpty())
+ assertTrue(Size(Float.NEGATIVE_INFINITY, 0.0f).isEmpty())
+ assertTrue(Size(Float.NEGATIVE_INFINITY, 20.0f).isEmpty())
+ assertTrue(Size(10.0f, Float.NEGATIVE_INFINITY).isEmpty())
+
+ assertFails {
+ Size.Unspecified.isEmpty()
+ }
}
}
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
index 2ebbbd0..2a00b19 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
@@ -21,9 +21,11 @@
// Masks everything but the sign bit
internal const val UnsignedFloatMask = 0x7fffffffL
+internal const val DualUnsignedFloatMask = 0x7fffffff_7fffffffL
// Any value greater than this is a NaN
internal const val FloatInfinityBase = 0x7f800000L
+internal const val DualFloatInfinityBase = 0x7f800000_7f800000L
// Same as Offset/Size.Unspecified.packedValue, but avoids a getstatic
internal const val UnspecifiedPackedFloats = 0x7fc00000_7fc00000L // NaN_NaN
@@ -31,6 +33,8 @@
// 0x80000000_80000000UL.toLong() but expressed as a const value
// Mask for the sign bit of the two floats packed in a long
internal const val DualFloatSignBit = -0x7fffffff_80000000L
+// Set the highest bit of each 32 bit chunk in a 64 bit word
+internal const val Uint64High32 = -0x7fffffff_80000000L
// 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,
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
index e09f97f..459e711 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
@@ -91,7 +91,7 @@
* x or y parameter
*/
fun copy(x: Float = unpackFloat1(packedValue), y: Float = unpackFloat2(packedValue)) =
- Offset(x, y)
+ Offset(packFloats(x, y))
companion object {
/**
@@ -100,26 +100,23 @@
* This can be used to represent the origin of a coordinate space.
*/
@Stable
- val Zero = Offset(0.0f, 0.0f)
+ val Zero = Offset(0x0L)
/**
* An offset with infinite x and y components.
*
- * See also:
- *
- * * [isInfinite], which checks whether either component is infinite.
- * * [isFinite], which checks whether both components are finite.
+ * See also [isFinite] to check whether both components are finite.
*/
// This is included for completeness, because [Size.infinite] exists.
@Stable
- val Infinite = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
+ val Infinite = Offset(DualFloatInfinityBase)
/**
* Represents an unspecified [Offset] value, usually a replacement for `null`
* when a primitive value is desired.
*/
@Stable
- val Unspecified = Offset(Float.NaN, Float.NaN)
+ val Unspecified = Offset(UnspecifiedPackedFloats)
}
@Stable
@@ -193,8 +190,10 @@
"Offset is unspecified"
}
return Offset(
- unpackFloat1(packedValue) - unpackFloat1(other.packedValue),
- unpackFloat2(packedValue) - unpackFloat2(other.packedValue),
+ packFloats(
+ unpackFloat1(packedValue) - unpackFloat1(other.packedValue),
+ unpackFloat2(packedValue) - unpackFloat2(other.packedValue)
+ )
)
}
@@ -214,8 +213,10 @@
"Offset is unspecified"
}
return Offset(
- unpackFloat1(packedValue) + unpackFloat1(other.packedValue),
- unpackFloat2(packedValue) + unpackFloat2(other.packedValue),
+ packFloats(
+ unpackFloat1(packedValue) + unpackFloat1(other.packedValue),
+ unpackFloat2(packedValue) + unpackFloat2(other.packedValue)
+ )
)
}
@@ -232,8 +233,10 @@
"Offset is unspecified"
}
return Offset(
- unpackFloat1(packedValue) * operand,
- unpackFloat2(packedValue) * operand,
+ packFloats(
+ unpackFloat1(packedValue) * operand,
+ unpackFloat2(packedValue) * operand
+ )
)
}
@@ -250,8 +253,10 @@
"Offset is unspecified"
}
return Offset(
- unpackFloat1(packedValue) / operand,
- unpackFloat2(packedValue) / operand,
+ packFloats(
+ unpackFloat1(packedValue) / operand,
+ unpackFloat2(packedValue) / operand
+ )
)
}
@@ -268,8 +273,10 @@
"Offset is unspecified"
}
return Offset(
- unpackFloat1(packedValue) % operand,
- unpackFloat2(packedValue) % operand,
+ packFloats(
+ unpackFloat1(packedValue) % operand,
+ unpackFloat2(packedValue) % operand
+ )
)
}
@@ -306,8 +313,10 @@
"Offset is unspecified"
}
return Offset(
- lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
- lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+ packFloats(
+ lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
+ lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+ )
)
}
@@ -319,9 +328,11 @@
checkPrecondition(packedValue != UnspecifiedPackedFloats) {
"Offset is unspecified"
}
- val x = (packedValue shr 32) and FloatInfinityBase
- val y = packedValue and FloatInfinityBase
- return x != FloatInfinityBase && y != FloatInfinityBase
+ // Mask out the sign bit and do an equality check in each 32-bit lane
+ // against the "infinity base" mask (to check whether each packed float
+ // is infinite or not).
+ val v = (packedValue and DualUnsignedFloatMask) xor DualFloatInfinityBase
+ return (((v shr 1) or Uint64High32) - v) and Uint64High32 == 0L
}
/**
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
index 8bd6c27..df0c802 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
@@ -74,7 +74,7 @@
* width or height parameter
*/
fun copy(width: Float = unpackFloat1(packedValue), height: Float = unpackFloat2(packedValue)) =
- Size(width, height)
+ Size(packFloats(width, height))
companion object {
@@ -82,7 +82,7 @@
* An empty size, one with a zero width and a zero height.
*/
@Stable
- val Zero = Size(0.0f, 0.0f)
+ val Zero = Size(0x0L)
/**
* A size whose [width] and [height] are unspecified. This is a sentinel
@@ -90,7 +90,7 @@
* Access to width or height on an unspecified size is not allowed.
*/
@Stable
- val Unspecified = Size(Float.NaN, Float.NaN)
+ val Unspecified = Size(UnspecifiedPackedFloats)
}
/**
@@ -103,7 +103,17 @@
if (packedValue == UnspecifiedPackedFloats) {
throwIllegalStateException("Size is unspecified")
}
- return unpackFloat1(packedValue) <= 0.0f || unpackFloat2(packedValue) <= 0.0f
+ // Mask the sign bits, shift them to the right and replicate them by multiplying by -1.
+ // This will give us a mask of 0xffff_ffff for negative packed floats, and 0x0000_0000
+ // for positive packed floats. We invert the mask and do an and operation with the
+ // original value to set any negative float to 0.0f.
+ val v = packedValue and ((packedValue and DualFloatSignBit ushr 31) * -0x1).inv()
+ // At this point any negative float is set to 0, so the sign bit is always 0.
+ // We take the 2 packed floats and "and" them together: if any of the two floats
+ // is 0.0f (either because the original value is 0.0f or because it was negative and
+ // we turned it into 0.0f with the line above), the result of the and operation will
+ // be 0 and we know our Size is empty.
+ return ((v ushr 32) and (v and 0xffffffffL)) == 0L
}
/**
@@ -119,8 +129,10 @@
throwIllegalStateException("Size is unspecified")
}
return Size(
- unpackFloat1(packedValue) * operand,
- unpackFloat2(packedValue) * operand,
+ packFloats(
+ unpackFloat1(packedValue) * operand,
+ unpackFloat2(packedValue) * operand
+ )
)
}
@@ -137,8 +149,10 @@
throwIllegalStateException("Size is unspecified")
}
return Size(
- unpackFloat1(packedValue) / operand,
- unpackFloat2(packedValue) / operand,
+ packFloats(
+ unpackFloat1(packedValue) / operand,
+ unpackFloat2(packedValue) / operand
+ )
)
}
@@ -221,8 +235,10 @@
throwIllegalStateException("Offset is unspecified")
}
return Size(
- lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
- lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+ packFloats(
+ lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
+ lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+ )
)
}
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index e374fff..4e5c0b0 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -571,9 +571,9 @@
method public static void click(androidx.compose.ui.test.TouchInjectionScope, optional long position);
method public static void doubleClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long delayMillis);
method public static void longClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long durationMillis);
- method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
method public static void pinch(androidx.compose.ui.test.TouchInjectionScope, long start0, long end0, long start1, long end1, optional long durationMillis);
- method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+ method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, long start, long end, optional long durationMillis);
method public static void swipeDown(androidx.compose.ui.test.TouchInjectionScope, optional float startY, optional float endY, optional long durationMillis);
method public static void swipeLeft(androidx.compose.ui.test.TouchInjectionScope, optional float startX, optional float endX, optional long durationMillis);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index fb5c367..fce6f17 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -572,9 +572,9 @@
method public static void click(androidx.compose.ui.test.TouchInjectionScope, optional long position);
method public static void doubleClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long delayMillis);
method public static void longClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long durationMillis);
- method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
method public static void pinch(androidx.compose.ui.test.TouchInjectionScope, long start0, long end0, long start1, long end1, optional long durationMillis);
- method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+ method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, long start, long end, optional long durationMillis);
method public static void swipeDown(androidx.compose.ui.test.TouchInjectionScope, optional float startY, optional float endY, optional long durationMillis);
method public static void swipeLeft(androidx.compose.ui.test.TouchInjectionScope, optional float startX, optional float endX, optional long durationMillis);
diff --git a/compose/ui/ui-test/lint-baseline.xml b/compose/ui/ui-test/lint-baseline.xml
index 40df33c..199ff09 100644
--- a/compose/ui/ui-test/lint-baseline.xml
+++ b/compose/ui/ui-test/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
<issue
id="BanThreadSleep"
diff --git a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
index 6b948c8..29cd365 100644
--- a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
+++ b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.test.samples
import androidx.annotation.Sampled
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.click
import androidx.compose.ui.test.onNodeWithTag
@@ -76,3 +78,35 @@
up()
}
}
+
+@Sampled
+fun touchInputMultiTouchWithHistory() {
+ // Move two fingers in a horizontal line, one on y=100 and one on y=500
+ composeTestRule.onNodeWithTag("myComponent")
+ .performTouchInput {
+ // First, make contact with the screen with both pointers:
+ down(0, Offset(300f, 100f))
+ down(1, Offset(300f, 500f))
+ // Update the pointer locations for the next event
+ updatePointerTo(0, Offset(400f, 100f))
+ updatePointerTo(1, Offset(400f, 500f))
+ // And send the move event with historical data
+ @OptIn(ExperimentalTestApi::class)
+ moveWithHistoryMultiPointer(
+ // Let's add 3 historical events
+ relativeHistoricalTimes = listOf(-12, -8, -4),
+ // Now, for each pointer we supply the historical coordinates
+ historicalCoordinates = listOf(
+ // Pointer 0 moves along y=100
+ listOf(Offset(325f, 100f), Offset(350f, 100f), Offset(375f, 100f)),
+ // Pointer 1 moves along y=500
+ listOf(Offset(325f, 500f), Offset(350f, 500f), Offset(375f, 500f)),
+ ),
+ // The actual move event will be sent 16ms after the previous event
+ delayMillis = 16
+ )
+ // And finish the gesture by lifting both fingers. Can be done in any order
+ up(1)
+ up(0)
+ }
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt
new file mode 100644
index 0000000..3b0a4b8
--- /dev/null
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.test.injectionscope.touch
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Press
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Release
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerType.Companion.Touch
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.multiTouchSwipe
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.runComposeUiTest
+import androidx.compose.ui.test.util.ClickableTestBox
+import androidx.compose.ui.test.util.SinglePointerInputRecorder
+import androidx.compose.ui.test.util.verify
+import androidx.compose.ui.test.util.verifyEvents
+import androidx.test.filters.MediumTest
+import org.junit.Test
+
+@MediumTest
+@OptIn(ExperimentalTestApi::class)
+class SwipeMultiTouchTest {
+ companion object {
+ private const val TAG = "widget"
+ // Duration is 4 * eventPeriod to get easily predictable results
+ private const val DURATION = 64L
+ }
+
+ private val recorder = SinglePointerInputRecorder()
+
+ @Test
+ fun test() = runComposeUiTest {
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ ClickableTestBox(modifier = recorder, tag = TAG)
+ }
+ }
+
+ // Move three fingers over the box from left to right simultaneously
+ // With a duration that is exactly 4 times the eventPeriod, each pointer will be sampled
+ // at t = 0, 16, 32, 48 and 64. That corresponds to x values of 10, 30, 50, 70 and 90.
+
+ val curve1 = line(fromX = 10f, toX = 90f, y = 20f, DURATION)
+ val curve2 = line(fromX = 10f, toX = 90f, y = 50f, DURATION)
+ val curve3 = line(fromX = 10f, toX = 90f, y = 80f, DURATION)
+
+ onNodeWithTag(TAG).performTouchInput {
+ multiTouchSwipe(
+ curves = listOf(curve1, curve2, curve3),
+ durationMillis = DURATION
+ )
+ }
+
+ val pointer1 = PointerId(0)
+ val pointer2 = PointerId(1)
+ val pointer3 = PointerId(2)
+
+ runOnIdle {
+ recorder.apply {
+ verifyEvents(
+ // pointer1 down
+ { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+ // pointer2 down
+ { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+ { verify(0L, pointer2, true, Offset(10f, 50f), Touch, Press) },
+ // pointer3 down
+ { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+ { verify(0L, pointer2, true, Offset(10f, 50f), Touch, Press) },
+ { verify(0L, pointer3, true, Offset(10f, 80f), Touch, Press) },
+ // first move
+ { verify(16L, pointer1, true, Offset(30f, 20f), Touch, Move) },
+ { verify(16L, pointer2, true, Offset(30f, 50f), Touch, Move) },
+ { verify(16L, pointer3, true, Offset(30f, 80f), Touch, Move) },
+ // second move
+ { verify(32L, pointer1, true, Offset(50f, 20f), Touch, Move) },
+ { verify(32L, pointer2, true, Offset(50f, 50f), Touch, Move) },
+ { verify(32L, pointer3, true, Offset(50f, 80f), Touch, Move) },
+ // third move
+ { verify(48L, pointer1, true, Offset(70f, 20f), Touch, Move) },
+ { verify(48L, pointer2, true, Offset(70f, 50f), Touch, Move) },
+ { verify(48L, pointer3, true, Offset(70f, 80f), Touch, Move) },
+ // last move
+ { verify(64L, pointer1, true, Offset(90f, 20f), Touch, Move) },
+ { verify(64L, pointer2, true, Offset(90f, 50f), Touch, Move) },
+ { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Move) },
+ // pointer1 up
+ { verify(64L, pointer1, false, Offset(90f, 20f), Touch, Release) },
+ { verify(64L, pointer2, true, Offset(90f, 50f), Touch, Release) },
+ { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Release) },
+ // pointer2 up
+ { verify(64L, pointer2, false, Offset(90f, 50f), Touch, Release) },
+ { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Release) },
+ // pointer3 up
+ { verify(64L, pointer3, false, Offset(90f, 80f), Touch, Release) },
+ )
+ }
+ }
+ }
+
+ @Suppress("SameParameterValue")
+ private fun line(fromX: Float, toX: Float, y: Float, durationMillis: Long): (Long) -> Offset {
+ return { Offset(fromX + (toX - fromX) * it / durationMillis, y) }
+ }
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
index d814f96..5992018 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
@@ -234,15 +234,18 @@
/**
* Sends a move event [delayMillis] after the last sent event without updating any of the
- * pointer positions.
+ * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
+ * to the move event. This corresponds to the scenario where a touch screen generates touch
+ * events quicker than can be dispatched and batches them together.
*
- * This overload supports gestures with multiple pointers.
+ * @sample androidx.compose.ui.test.samples.touchInputMultiTouchWithHistory
*
* @param relativeHistoricalTimes Time of each historical event, as a millisecond relative to
* the time the actual event is sent. For example, -10L means 10ms earlier.
* @param historicalCoordinates Coordinates of each historical event, in the same coordinate
* space as [moveTo]. The outer list must have the same size as the number of pointers in the
- * event, and each inner list must have the same size as [relativeHistoricalTimes].
+ * event, and each inner list must have the same size as [relativeHistoricalTimes]. The `i`th
+ * pointer is assigned the `i`th history, with the pointers sorted on ascending pointerId.
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
@@ -255,7 +258,9 @@
/**
* Sends a move event [delayMillis] after the last sent event without updating any of the
- * pointer positions.
+ * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
+ * to the move event. This corresponds to the scenario where a touch screen generates touch
+ * events quicker than can be dispatched and batches them together.
*
* This overload is a convenience method for the common case where the gesture only has one
* pointer.
@@ -464,7 +469,7 @@
*/
fun TouchInjectionScope.swipe(
curve: (Long) -> Offset,
- durationMillis: Long,
+ durationMillis: Long = 200,
keyTimes: List<Long> = emptyList()
) {
@OptIn(ExperimentalTestApi::class)
@@ -491,7 +496,7 @@
@ExperimentalTestApi
fun TouchInjectionScope.multiTouchSwipe(
curves: List<(Long) -> Offset>,
- durationMillis: Long,
+ durationMillis: Long = 200,
keyTimes: List<Long> = emptyList()
) {
val startTime = 0L
diff --git a/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt b/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt
new file mode 100644
index 0000000..ff3ce54
--- /dev/null
+++ b/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt
@@ -0,0 +1,220 @@
+List of 3rd party licenses:
+-----------------------------------------------------------------------------
+* kotlinx.coroutines library.
+
+ ****** NOTICE:
+
+=========================================================================
+== NOTICE file corresponding to the section 4 d of ==
+== the Apache License, Version 2.0, ==
+== in this case for the kotlinx.coroutines library. ==
+=========================================================================
+
+kotlinx.coroutines library.
+Copyright 2016-2024 JetBrains s.r.o and contributors
+
+ ****** LICENSE:
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
+
+ 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.
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e34dcad..cd86e73 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -26,7 +26,7 @@
autoValue = "1.6.3"
binaryCompatibilityValidator = "0.15.0-Beta.2"
byteBuddy = "1.14.9"
-asm = "9.3"
+asm = "9.7"
cmake = "3.22.1"
composeCompilerPlugin = "1.5.11" # Update when pulling in new stable binaries
dagger = "2.49"
diff --git a/input/input-motionprediction/api/1.0.0-beta04.txt b/input/input-motionprediction/api/1.0.0-beta04.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/1.0.0-beta04.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/api/res-1.0.0-beta04.txt b/input/input-motionprediction/api/res-1.0.0-beta04.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/input/input-motionprediction/api/res-1.0.0-beta04.txt
diff --git a/input/input-motionprediction/api/restricted_1.0.0-beta04.txt b/input/input-motionprediction/api/restricted_1.0.0-beta04.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/restricted_1.0.0-beta04.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
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 65a95d3..d5a7f7d 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
@@ -176,7 +176,11 @@
fun packageInspector(libraryProject: Project, inspectorProjectPath: String) {
val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)
if (inspectorProject == null) {
- check(libraryProject.property("androidx.studio.type") == "playground") {
+ val extraProperties = libraryProject.extensions.extraProperties
+ check(
+ extraProperties.has("androidx.studio.type") &&
+ extraProperties.get("androidx.studio.type") == "playground"
+ ) {
"Cannot find $inspectorProjectPath. This is optional only for playground builds."
}
// skip setting up inspector project
diff --git a/libraryversions.toml b/libraryversions.toml
index 722925b2..97e7a8d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -80,7 +80,7 @@
HILT = "1.2.0-rc01"
HILT_NAVIGATION = "1.2.0-rc01"
HILT_NAVIGATION_COMPOSE = "1.2.0-rc01"
-INPUT_MOTIONPREDICTION = "1.0.0-beta03"
+INPUT_MOTIONPREDICTION = "1.0.0-beta04"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
JAVASCRIPTENGINE = "1.0.0-beta01"
diff --git a/lifecycle/lifecycle-compiler/src/main/resources/NOTICE.txt b/lifecycle/lifecycle-compiler/src/main/resources/META-INF/NOTICE.txt
similarity index 100%
rename from lifecycle/lifecycle-compiler/src/main/resources/NOTICE.txt
rename to lifecycle/lifecycle-compiler/src/main/resources/META-INF/NOTICE.txt
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index a8f7b18..b944fbb 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -131,6 +131,8 @@
"findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"findProperty" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "property" to
+ Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"get" to Replacement(TASK_PROVIDER, null, EAGER_CONFIGURATION_ISSUE),
"getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
diff --git a/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java b/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
index d3653f5..1a4f88e 100644
--- a/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
@@ -393,8 +393,9 @@
bob.setIconBitmap(Api21Impl.getIconBitmap(description));
bob.setIconUri(Api21Impl.getIconUri(description));
Bundle extras = Api21Impl.getExtras(description);
+ extras = MediaSessionCompat.unparcelWithClassLoader(extras);
if (extras != null) {
- extras = new Bundle(MediaSessionCompat.unparcelWithClassLoader(extras));
+ extras = new Bundle(extras);
}
Uri mediaUri = null;
if (extras != null) {
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 7a8b34a..777c33f 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -50,8 +50,6 @@
classpath = extraClasspath,
symbolProcessorProviders = symbolProcessorProviders,
processorOptions = processorOptions,
- // b/328813158 to remove targeting of Java 17
- javacArguments = listOf("-source", "17", "-target", "17")
)
)
}
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 42a9e97..2bc4be2 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -81,7 +81,6 @@
shadowed(libs.kotlinMetadataJvm) {
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
}
- implementation(libs.intellijAnnotations)
implementation(libs.kspApi)
implementation(libs.kotlinStdlibJdk8) // KSP defines older version as dependency, force update.
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
index bf9c0d6..7a80d08 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
@@ -26,6 +26,7 @@
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
import com.google.devtools.ksp.symbol.Variance
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.KModifier
@@ -92,8 +93,16 @@
val mutableBounds = mutableListOf(ANY.copy(nullable = true))
val typeName = createModifiableTypeVariableName(name = name.asString(), bounds = mutableBounds)
typeArgumentTypeLookup[name] = typeName
- val resolvedBounds = bounds.map {
- it.asKTypeName(resolver, typeArgumentTypeLookup)
+ val resolvedBounds = bounds.map { typeReference ->
+ typeReference.asKTypeName(resolver, typeArgumentTypeLookup).let { kTypeName ->
+ typeReference.resolve().let {
+ if (it.nullability == Nullability.PLATFORM) {
+ kTypeName.copy(nullable = true)
+ } else {
+ kTypeName
+ }
+ }
+ }
}.toList()
if (resolvedBounds.isNotEmpty()) {
mutableBounds.addAll(resolvedBounds)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
index 5a9d520..748503b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
@@ -25,6 +25,7 @@
import androidx.room.compiler.processing.XTypeVariableType
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.Nullability
import com.squareup.javapoet.TypeName
import kotlin.reflect.KClass
@@ -53,7 +54,16 @@
)
}
- override val upperBounds: List<XType> = ksTypeVariable.bounds.map(env::wrap).toList()
+ override val upperBounds: List<XType> = ksTypeVariable.bounds.map {
+ val type = it.resolve().let {
+ if (it.nullability == Nullability.PLATFORM) {
+ it.withNullability(XNullability.NULLABLE)
+ } else {
+ it
+ }
+ }
+ env.wrap(it, type)
+ }.toList()
override fun annotations(): Sequence<KSAnnotation> {
return ksTypeVariable.annotations
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
index 64e518b..26fa62a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
@@ -18,10 +18,12 @@
import androidx.room.compiler.processing.XAnnotated
import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeParameterElement
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_FIELD
import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.Nullability
import com.squareup.javapoet.TypeVariableName
internal class KspTypeParameterElement(
@@ -43,7 +45,16 @@
}
override val bounds: List<XType> by lazy {
- declaration.bounds.map { env.wrap(it, it.resolve()) }.toList().ifEmpty {
+ declaration.bounds.map {
+ val type = it.resolve().let {
+ if (it.nullability == Nullability.PLATFORM) {
+ it.withNullability(XNullability.NULLABLE)
+ } else {
+ it
+ }
+ }
+ env.wrap(it, type)
+ }.toList().ifEmpty {
listOf(env.requireType(Any::class).makeNullable())
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index e83bbbe..9a83501 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -244,24 +244,31 @@
validateMethodElement(
element = it.processingEnv.requireTypeElement("foo.bar.Base"),
- tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
- rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+ tTypeName = XTypeName.getTypeVariableName("T", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true))),
+ rTypeName = XTypeName.getTypeVariableName("R", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true)))
)
validateMethodElement(
element = it.processingEnv.requireTypeElement("foo.bar.Child"),
- tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
- rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+ tTypeName = XTypeName.getTypeVariableName("T", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true))),
+ rTypeName = XTypeName.getTypeVariableName("R", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true)))
)
validateMethodTypeAsMemberOf(
element = it.processingEnv.requireTypeElement("foo.bar.Base"),
- tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
- rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+ tTypeName = XTypeName.getTypeVariableName("T", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true))),
+ rTypeName = XTypeName.getTypeVariableName("R", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true)))
)
validateMethodTypeAsMemberOf(
element = it.processingEnv.requireTypeElement("foo.bar.Child"),
tTypeName = String::class.asClassName(),
- rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+ rTypeName = XTypeName.getTypeVariableName("R", listOf(
+ XTypeName.ANY_OBJECT.copy(nullable = true)))
)
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index 4d9dcea..8320561 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -22,8 +22,9 @@
import androidx.room.compiler.processing.util.CONTINUATION_JCLASS_NAME
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.UNIT_JCLASS_NAME
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.getMethodByJvmName
-import androidx.room.compiler.processing.util.runKspTest
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth
@@ -581,7 +582,8 @@
"""
class KotlinSubject {
fun <T> oneTypeVar(): Unit = TODO()
- fun <T : MutableList<*>> oneBoundedTypeVar(): Unit = TODO()
+ fun <T : MutableList<*>?> oneBoundedTypeVar(): Unit = TODO()
+ fun <T : MutableList<*>> oneBoundedTypeVarNotNull(): Unit = TODO()
fun <A, B> twoTypeVar(param: B): A = TODO()
}
""".trimIndent()
@@ -590,15 +592,18 @@
"JavaSubject",
"""
import java.util.List;
+ import org.jetbrains.annotations.NotNull;
class JavaSubject {
<T> void oneTypeVar() {}
<T extends List<?>> void oneBoundedTypeVar() { }
+ <T extends @NotNull List<?>> void oneBoundedTypeVarNotNull() { }
<A, B> A twoTypeVar(B param) { return null; }
}
""".trimIndent()
)
- runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
- listOf("KotlinSubject", "JavaSubject",).forEach { subjectFqn ->
+
+ fun handler(invocation: XTestInvocation) {
+ listOf("KotlinSubject", "JavaSubject").forEach { subjectFqn ->
val subject = invocation.processingEnv.requireTypeElement(subjectFqn)
subject.getMethodByJvmName("oneTypeVar").let {
val typeVar = it.executableType.typeVariables.single()
@@ -618,6 +623,29 @@
bounds = listOf(
List::class.asMutableClassName()
.parametrizedBy(XTypeName.ANY_WILDCARD)
+ .copy(nullable = true)
+ )
+ )
+ )
+ assertThat(typeVar.superTypes.map { it.asTypeName() })
+ .containsExactly(
+ XTypeName.ANY_OBJECT.copy(nullable = true),
+ List::class.asMutableClassName()
+ .parametrizedBy(XTypeName.ANY_WILDCARD)
+ .copy(nullable = true)
+ )
+ assertThat(typeVar.typeArguments).isEmpty()
+ assertThat(typeVar.typeElement).isNull()
+ }
+ subject.getMethodByJvmName("oneBoundedTypeVarNotNull").let {
+ val typeVar = it.executableType.typeVariables.single()
+ assertThat(typeVar.asTypeName())
+ .isEqualTo(
+ XTypeName.getTypeVariableName(
+ name = "T",
+ bounds = listOf(
+ List::class.asMutableClassName()
+ .parametrizedBy(XTypeName.ANY_WILDCARD)
)
)
)
@@ -646,5 +674,7 @@
}
}
}
+ runProcessorTest(sources = listOf(kotlinSrc, javaSrc), handler = ::handler)
+ runProcessorTest(classpath = compileFiles(listOf(kotlinSrc, javaSrc)), handler = ::handler)
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
index d14fea3..f3e289d 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
@@ -182,20 +182,28 @@
val bound0 = t.bounds[0]
assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
if (invocation.isKsp) {
- assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+ assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
}
- val bar = invocation.processingEnv.requireType("Bar")
+ val bar = invocation.processingEnv.requireType("Bar").makeNullable()
assertThat(bound0.isSameType(bar)).isTrue()
- assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
val bound1 = t.bounds[1]
assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
if (invocation.isKsp) {
- assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+ assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
}
- val baz = invocation.processingEnv.requireType("Baz")
+ val baz = invocation.processingEnv.requireType("Baz").makeNullable()
assertThat(bound1.isSameType(baz)).isTrue()
- assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
}
}
@@ -333,20 +341,28 @@
val bound0 = t.bounds[0]
assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
if (invocation.isKsp) {
- assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+ assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
}
- val bar = invocation.processingEnv.requireType("Bar")
+ val bar = invocation.processingEnv.requireType("Bar").makeNullable()
assertThat(bound0.isSameType(bar)).isTrue()
- assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
val bound1 = t.bounds[1]
assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
if (invocation.isKsp) {
- assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+ assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
}
- val baz = invocation.processingEnv.requireType("Baz")
+ val baz = invocation.processingEnv.requireType("Baz").makeNullable()
assertThat(bound1.isSameType(baz)).isTrue()
- assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
}
}
@@ -381,20 +397,28 @@
val bound0 = t.bounds[0]
assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
if (invocation.isKsp) {
- assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+ assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
}
- val bar = invocation.processingEnv.requireType("Bar")
+ val bar = invocation.processingEnv.requireType("Bar").makeNullable()
assertThat(bound0.isSameType(bar)).isTrue()
- assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
val bound1 = t.bounds[1]
assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
if (invocation.isKsp) {
- assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+ assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
}
- val baz = invocation.processingEnv.requireType("Baz")
+ val baz = invocation.processingEnv.requireType("Baz").makeNullable()
assertThat(bound1.isSameType(baz)).isTrue()
- assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ if (invocation.isKsp) {
+ assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+ } else {
+ assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+ }
assertThat(constructor.parameters).hasSize(1)
val parameter = constructor.parameters[0]
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 7b32220..30422ec 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -94,6 +94,7 @@
KTypeVariableName(
"InputStreamType",
KClassName("java.io", "InputStream")
+ .copy(nullable = true)
)
)
)
@@ -120,12 +121,14 @@
val expected = KTypeVariableName(
"InputStreamType",
KClassName("java.io", "InputStream")
+ .copy(nullable = true)
)
assertThat(firstType.asTypeName().kotlin).isEqualTo(expected)
assertThat(
(firstType.asTypeName().kotlin as KTypeVariableName).bounds
).containsExactly(
KClassName("java.io", "InputStream")
+ .copy(nullable = true)
)
}
}
@@ -622,8 +625,16 @@
assertThat(typeElement.type.asTypeName().java.dumpToString(5))
.isEqualTo(expectedTypeStringDump)
if (invocation.isKsp) {
+ val expectedTypeStringDumpKotlin = """
+ SelfReferencing<T>
+ | T
+ | > SelfReferencing<T>?
+ | > | T
+ | > | > SelfReferencing<T>?
+ | > | > | T
+ """.trimIndent()
assertThat(typeElement.type.asTypeName().kotlin.dumpToString(5))
- .isEqualTo(expectedTypeStringDump)
+ .isEqualTo(expectedTypeStringDumpKotlin)
}
val expectedParamStringDump = """
SelfReferencing
diff --git a/room/room-compiler/src/main/resources/NOTICE.txt b/room/room-compiler/src/main/resources/META-INF/NOTICE.txt
similarity index 100%
rename from room/room-compiler/src/main/resources/NOTICE.txt
rename to room/room-compiler/src/main/resources/META-INF/NOTICE.txt
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index bbf992b..028b786 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -335,7 +335,8 @@
"""
) { parsedQuery, invocation ->
val expected = MUTABLE_LIST.parametrizedBy(
- XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT))
+ XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT.copy(
+ nullable = true)))
)
assertThat(parsedQuery.returnType.asTypeName(), `is`(expected))
invocation.assertCompilationResult {
diff --git a/settings.gradle b/settings.gradle
index 20624d7..8473061 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -405,6 +405,7 @@
includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:startup-benchmark", [BuildType.MAIN])
+includeProject(":binarycompatibilityvalidator:binarycompatibilityvalidator", [BuildType.MAIN])
includeProject(":biometric:biometric", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx-samples", "biometric/biometric-ktx/samples", [BuildType.MAIN])
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
index e3cd485..72e6f56 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
@@ -69,6 +69,16 @@
* LazyList and others. [ScalingLazyColumn] has a build-in rotary support, and accepts
* [RotaryScrollableBehavior] directly as a parameter.
*
+ * This modifier handles rotary input devices, used for scrolling. These devices can be categorized
+ * as high-resolution or low-resolution based on their precision.
+ *
+ * - High-res devices: Offer finer control and can detect smaller rotations.
+ * This allows for more precise adjustments during scrolling. One example of a high-res
+ * device is the crown (also known as rotating side button), located on the side of the watch.
+ * - Low-res devices: Have less granular control, registering larger rotations
+ * at a time. Scrolling behavior is adapted to compensate for these larger jumps. Examples
+ * include physical or virtual bezels, positioned around the screen.
+ *
* This modifier supports rotary scrolling and snapping.
* The behaviour is configured by the provided [RotaryScrollableBehavior]:
* either provide [RotaryScrollableDefaults.behavior] for scrolling with/without fling
@@ -103,7 +113,8 @@
/**
* An interface for handling scroll events. Has implementations for handling scroll
* with/without fling [FlingRotaryScrollableBehavior] and for handling snap
- * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior].
+ * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior] (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices).
*/
interface RotaryScrollableBehavior {
@@ -321,7 +332,8 @@
* Handles scroll with fling.
*
* @return A scroll with fling implementation of [RotaryScrollableBehavior] which is suitable
- * for both low-res and high-res inputs.
+ * for both low-res and high-res inputs (see [Modifier.rotaryScrollable] for descriptions
+ * of low-res and high-res devices).
*
* @param scrollableState Scrollable state which will be scrolled while receiving rotary events
* @param flingBehavior Logic describing Fling behavior. If null - fling will not happen
@@ -360,7 +372,8 @@
* Handles scroll with snap.
*
* @return A snap implementation of [RotaryScrollableBehavior] which is either suitable for low-res
- * or high-res input.
+ * or high-res input (see [Modifier.rotaryScrollable] for descriptions of low-res
+ * and high-res devices).
*
* @param layoutInfoProvider Implementation of [RotarySnapLayoutInfoProvider], which connects
* scrollableState to a rotary input for snapping scroll actions.
@@ -419,8 +432,9 @@
/**
* An abstract base class for handling scroll events. Has implementations for handling scroll
- * with/without fling [FlingRotaryScrollableBehavior] and for handling snap [LowResSnapRotaryScrollableBehavior],
- * [HighResSnapRotaryScrollableBehavior].
+ * with/without fling [FlingRotaryScrollableBehavior] and for handling snap
+ * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior] (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
*/
internal abstract class BaseRotaryScrollableBehavior : RotaryScrollableBehavior {
@@ -797,7 +811,8 @@
*
* For a high-res input it has a filtering for events which are coming
* with an opposite sign (this might happen to devices with rsb,
- * especially at the end of the scroll )
+ * especially at the end of the scroll ) - see [Modifier.rotaryScrollable] for descriptions
+ * of low-res and high-res devices.
*
* This scroll behavior supports fling. It can be set with [RotaryFlingHandler].
*/
@@ -873,7 +888,8 @@
}
/**
- * A scroll behavior for RSB(high-res) input with snapping and without fling.
+ * A scroll behavior for RSB(high-res) input with snapping and without fling (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
*
* Threshold for snapping is set dynamically in ThresholdBehavior, which depends
* on the scroll speed and the average size of the items.
@@ -1030,7 +1046,8 @@
}
/**
- * A scroll behavior for Bezel(low-res) input with snapping and without fling
+ * A scroll behavior for Bezel(low-res) input with snapping and without fling (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
*
* This scroll behavior doesn't support fling.
*/
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 fa15794..0518e7a 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
@@ -26,7 +26,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonColors
import androidx.wear.compose.material3.ButtonDefaults
@@ -51,7 +51,7 @@
@Composable
fun ButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -98,7 +98,7 @@
@Composable
fun FilledTonalButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -145,7 +145,7 @@
@Composable
fun OutlinedButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -192,7 +192,7 @@
@Composable
fun ChildButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -239,7 +239,7 @@
@Composable
fun CompactButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -341,7 +341,7 @@
@Composable
fun MultilineButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -384,7 +384,7 @@
@Composable
fun AvatarButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
index 19c409d..9180758 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.AppCard
import androidx.wear.compose.material3.CardDefaults
import androidx.wear.compose.material3.ListHeader
@@ -48,7 +48,7 @@
@Composable
fun CardDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index fcb7c22..8ecebb0 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.FilledIconButton
import androidx.wear.compose.material3.FilledTonalIconButton
@@ -42,7 +42,7 @@
@Composable
fun IconButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
index 2216bdd..86ac15d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
@@ -33,7 +33,7 @@
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.IconButtonDefaults
import androidx.wear.compose.material3.IconToggleButton
@@ -44,7 +44,7 @@
@Composable
fun IconToggleButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
index 7488d61..f5094c4 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
@@ -19,14 +19,14 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.samples.ListHeaderSample
import androidx.wear.compose.material3.samples.ListSubheaderSample
import androidx.wear.compose.material3.samples.ListSubheaderWithIconSample
@Composable
fun ListHeaderDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxWidth()
) {
item {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
index fd546275..cee65ef 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
@@ -31,7 +31,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.RadioButton
@@ -40,7 +40,7 @@
@Composable
fun RadioButtonDemo() {
var selectedRadioIndex by remember { mutableIntStateOf(0) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize().selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
index 0f8bf1b..5e47afb 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.RadioButton
import androidx.wear.compose.material3.Text
@@ -34,7 +34,7 @@
@Composable
fun RadioDemos() {
var selectedIndex by remember { mutableIntStateOf(0) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
index dc81baf..f0415ce 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
@@ -22,8 +22,8 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Icon
@@ -34,7 +34,7 @@
fun SettingsDemo() {
// TODO: Add Scaffold and TimeText when available
val scalingLazyListState = rememberScalingLazyListState()
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
state = scalingLazyListState,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
index 1597b69..feab507 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
@@ -34,10 +34,10 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
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.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.InlineSlider
@@ -90,7 +90,7 @@
var enabledValue by remember { mutableFloatStateOf(5f) }
var disabledValue by remember { mutableFloatStateOf(5f) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
@@ -134,7 +134,7 @@
var valueWithoutSegments by remember { mutableIntStateOf(5) }
var valueWithSegments by remember { mutableIntStateOf(5) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
index b34aba8..8327371 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.SplitRadioButton
import androidx.wear.compose.material3.Text
@@ -37,7 +37,7 @@
@Composable
fun SplitRadioButtonDemo() {
var selectedRadioIndex by remember { mutableIntStateOf(0) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
index 108386c..62e3461 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Checkbox
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.SplitToggleButton
@@ -38,7 +38,7 @@
@Composable
fun SplitToggleButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
index c6e0246..807cd0f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
@@ -42,7 +42,7 @@
@Composable
fun TextButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
index 76d66b5..955f9a8 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TextButtonDefaults
@@ -41,7 +41,7 @@
@Composable
fun TextToggleButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
index fc7d844..fc3460f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Checkbox
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
@@ -40,7 +40,7 @@
@Composable
fun ToggleButtonDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
index 3dcdcc6..f075f0f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
@@ -28,7 +28,7 @@
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Checkbox
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.Switch
@@ -37,7 +37,7 @@
@Composable
fun CheckboxDemos() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
@@ -63,7 +63,7 @@
@Composable
fun SwitchDemos() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
diff --git a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt
deleted file mode 100644
index f53977e..0000000
--- a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt
+++ /dev/null
@@ -1,169 +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.wear.compose.integration.demos.common
-
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.rotary.onRotaryScrollEvent
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.lazy.AutoCenteringParams
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
-import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
-import androidx.wear.compose.foundation.lazy.ScalingParams
-import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.receiveAsFlow
-
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalWearFoundationApi::class)
-@Composable
-fun ScalingLazyColumnWithRSB(
- modifier: Modifier = Modifier,
- state: ScalingLazyListState = rememberScalingLazyListState(),
- contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
- scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
- reverseLayout: Boolean = false,
- snap: Boolean = true,
- horizontalAlignment: Alignment.Horizontal = Alignment.Start,
- verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(
- space = 4.dp,
- alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom
- ),
- autoCentering: AutoCenteringParams? = AutoCenteringParams(),
- content: ScalingLazyListScope.() -> Unit
-) {
- val flingBehavior = if (snap) ScalingLazyColumnDefaults.snapFlingBehavior(
- state = state
- ) else ScrollableDefaults.flingBehavior()
- val focusRequester = rememberActiveFocusRequester()
- ScalingLazyColumn(
- modifier = modifier.rsbScroll(
- scrollableState = state,
- flingBehavior = flingBehavior,
- focusRequester = focusRequester
- ),
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- scalingParams = scalingParams,
- flingBehavior = flingBehavior,
- horizontalAlignment = horizontalAlignment,
- verticalArrangement = verticalArrangement,
- autoCentering = autoCentering,
- content = content
- )
-}
-
-@Suppress("ComposableModifierFactory")
-@Composable
-@Deprecated("Use .rotary modifier instead")
-fun Modifier.rsbScroll(
- scrollableState: ScrollableState,
- flingBehavior: FlingBehavior,
- focusRequester: FocusRequester? = null
-): Modifier {
- val channel = remember {
- Channel<TimestampedDelta>(
- capacity = 10,
- onBufferOverflow = BufferOverflow.DROP_OLDEST
- )
- }
-
- var lastTimeMillis = remember { 0L }
- var smoothSpeed = remember { 0f }
- val speedWindowMillis = 200L
- val timeoutToFling = 100L
-
- return composed {
- var rsbScrollInProgress by remember { mutableStateOf(false) }
- LaunchedEffect(rsbScrollInProgress) {
- if (rsbScrollInProgress) {
- scrollableState.scroll(MutatePriority.UserInput) {
- channel.receiveAsFlow().collectLatest {
- val toScroll = if (lastTimeMillis > 0L && it.time > lastTimeMillis) {
- val timeSinceLastEventMillis = it.time - lastTimeMillis
-
- // Speed is in pixels per second.
- val speed = it.delta * 1000 / timeSinceLastEventMillis
- val cappedElapsedTimeMillis =
- timeSinceLastEventMillis.coerceAtMost(speedWindowMillis)
- smoothSpeed = ((speedWindowMillis - cappedElapsedTimeMillis) * speed +
- cappedElapsedTimeMillis * smoothSpeed) / speedWindowMillis
- smoothSpeed * cappedElapsedTimeMillis / 1000
- } else {
- 0f
- }
- lastTimeMillis = it.time
- scrollBy(toScroll)
-
- // If more than the given time pass, start a fling.
- delay(timeoutToFling)
-
- lastTimeMillis = 0L
-
- if (smoothSpeed != 0f) {
- val launchSpeed = smoothSpeed
- smoothSpeed = 0f
- with(flingBehavior) {
- performFling(launchSpeed)
- }
- rsbScrollInProgress = false
- }
- }
- }
- }
- }
- this
- .onRotaryScrollEvent {
- channel.trySend(TimestampedDelta(it.uptimeMillis, it.verticalScrollPixels))
- rsbScrollInProgress = true
- true
- }
- .let {
- if (focusRequester != null) {
- it
- .focusRequester(focusRequester)
- .focusable()
- } else it
- }
- }
-}
-
-internal data class TimestampedDelta(val time: Long, val delta: Float)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
index 93d971b..22fa29d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
@@ -35,8 +35,8 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.CompactButton
@@ -115,7 +115,7 @@
var enabled by remember { mutableStateOf(true) }
val context = LocalContext.current
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -315,7 +315,7 @@
@Composable
fun ButtonGallery() {
val state = rememberScalingLazyListState()
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
state = state,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize(),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
index b20cf49..fc7b485 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
@@ -32,7 +32,7 @@
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material.AppCard
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.CardDefaults
@@ -42,7 +42,7 @@
@Composable
fun CardDemo() {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
index 0c06756..0c549bd 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
@@ -49,7 +49,6 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipColors
import androidx.wear.compose.material.ChipDefaults
@@ -70,7 +69,7 @@
var enabled by remember { mutableStateOf(true) }
var chipStyle by remember { mutableStateOf(ChipStyle.Primary) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 8929f17..c187929 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -30,6 +30,7 @@
import androidx.wear.compose.foundation.SwipeToDismissKeys
import androidx.wear.compose.foundation.SwipeToDismissValue
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
@@ -38,7 +39,6 @@
import androidx.wear.compose.integration.demos.common.Demo
import androidx.wear.compose.integration.demos.common.DemoCategory
import androidx.wear.compose.integration.demos.common.DemoParameters
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ListHeader
@@ -122,13 +122,12 @@
) {
val state = rememberScalingLazyListState()
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.testTag(DemoListTag),
state = scrollStates[scrollStateIndex],
- snap = false,
autoCentering = AutoCenteringParams(itemIndex = if (category.demos.size >= 2) 2 else 1),
) {
item {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index be9889d..b68787e 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -27,7 +27,6 @@
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -67,12 +66,10 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.wear.compose.integration.demos.common.rsbScroll
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Picker
-import androidx.wear.compose.material.PickerDefaults
import androidx.wear.compose.material.PickerGroup
import androidx.wear.compose.material.PickerGroupItem
import androidx.wear.compose.material.PickerGroupState
@@ -194,7 +191,7 @@
horizontalArrangement = Arrangement.Center,
) {
PickerGroup(
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = hourState,
modifier = Modifier
.size(40.dp, 100.dp),
@@ -207,7 +204,7 @@
contentDescription = hourContentDescription,
option = pickerOption
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = minuteState,
modifier = Modifier
.size(40.dp, 100.dp),
@@ -220,7 +217,7 @@
contentDescription = minuteContentDescription,
option = pickerOption
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = secondState,
modifier = Modifier
.size(40.dp, 100.dp),
@@ -400,7 +397,7 @@
}
Spacer(Modifier.width(16.dp))
PickerGroup(
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = hourState,
modifier = Modifier.size(48.dp, 100.dp),
onSelected = {
@@ -412,7 +409,7 @@
contentDescription = hoursContentDescription,
option = pickerTextOption(textStyle) { "%02d".format(it + 1) }
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = minuteState,
modifier = Modifier.size(48.dp, 100.dp),
onSelected = {
@@ -424,7 +421,7 @@
contentDescription = minutesContentDescription,
option = pickerTextOption(textStyle) { "%02d".format(it) }
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = periodState,
modifier = Modifier.size(64.dp, 100.dp),
contentDescription = periodContentDescription,
@@ -666,7 +663,7 @@
horizontalArrangement = Arrangement.Center
) {
PickerGroup(
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = datePickerState.dayState,
modifier = Modifier.size(dayWidth, 100.dp),
contentDescription = dayContentDescription,
@@ -680,7 +677,7 @@
"%d".format(datePickerState.currentDay(it))
}
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = datePickerState.monthState,
modifier = Modifier.size(monthWidth, 100.dp),
onSelected = {
@@ -694,7 +691,7 @@
shortMonthNames[(datePickerState.currentMonth(it) - 1) % 12]
}
),
- pickerGroupItemWithRSB(
+ PickerGroupItem(
pickerState = datePickerState.yearState,
modifier = Modifier.size(yearWidth, 100.dp),
onSelected = {
@@ -817,27 +814,6 @@
Spacer(Modifier.width(width))
}
-@Suppress("DEPRECATION")
-@Composable
-fun pickerGroupItemWithRSB(
- pickerState: PickerState,
- modifier: Modifier,
- contentDescription: String?,
- onSelected: () -> Unit,
- readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
- option: @Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit
-) = PickerGroupItem(
- pickerState = pickerState,
- modifier = modifier.rsbScroll(
- scrollableState = pickerState,
- flingBehavior = PickerDefaults.flingBehavior(pickerState)
- ),
- contentDescription = contentDescription,
- onSelected = onSelected,
- readOnlyLabel = readOnlyLabel,
- option = option
-)
-
@Composable
fun PickerWithoutGradient() {
val items = listOf("One", "Two", "Three", "Four", "Five")
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
index 4aa990c..a736de1 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
@@ -41,8 +41,8 @@
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.integration.demos.common.Centralize
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.AppCard
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipColors
@@ -62,7 +62,7 @@
fun PlaceholderChips() {
var resetCount by remember { mutableIntStateOf(0) }
Box {
- ScalingLazyColumnWithRSB {
+ ScalingLazyColumn {
item {
ListHeader {
Text(text = "Primary Label Center Aligned", textAlign = TextAlign.Center)
@@ -313,7 +313,7 @@
}
}
- ScalingLazyColumnWithRSB {
+ ScalingLazyColumn {
item {
ListHeader {
Text("Overlaid Placeholders", textAlign = TextAlign.Center)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
index 17a6fa6..628206c 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
@@ -32,7 +32,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.CircularProgressIndicator
@@ -58,7 +58,7 @@
val animatedProgress: Float by animateFloatAsState(targetValue = progress)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 60e8213..7bb195c 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -36,10 +36,11 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
-import androidx.wear.compose.integration.demos.common.rsbScroll
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
@@ -154,9 +155,11 @@
Column(
modifier = Modifier
.verticalScroll(scrollState)
- .rsbScroll(
- scrollableState = scrollState,
- flingBehavior = ScrollableDefaults.flingBehavior(),
+ .rotaryScrollable(
+ RotaryScrollableDefaults.behavior(
+ scrollableState = scrollState,
+ flingBehavior = ScrollableDefaults.flingBehavior()
+ ),
focusRequester = focusRequester
)
) {
@@ -190,9 +193,11 @@
) {
LazyColumn(
state = scrollState,
- modifier = Modifier.rsbScroll(
- scrollableState = scrollState,
- flingBehavior = ScrollableDefaults.flingBehavior(),
+ modifier = Modifier.rotaryScrollable(
+ RotaryScrollableDefaults.behavior(
+ scrollableState = scrollState,
+ flingBehavior = ScrollableDefaults.flingBehavior()
+ ),
focusRequester = focusRequester
)
) {
@@ -230,7 +235,7 @@
PositionIndicator(scalingLazyListState = scrollState)
}
) {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
contentPadding = PaddingValues(10.dp),
state = scrollState,
autoCentering = AutoCenteringParams(itemIndex = 1, itemOffset = 0)
@@ -272,7 +277,7 @@
PositionIndicator(scalingLazyListState = scrollState)
}
) {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
contentPadding = PaddingValues(10.dp),
state = scrollState,
) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
index c187ef4..7137f26 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
@@ -32,9 +32,9 @@
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.ListHeader
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.RadioButton
@@ -60,7 +60,7 @@
var radioIconWithSecondarySelected by remember { mutableStateOf(true) }
var splitWithRadioIconSelected by remember { mutableStateOf(true) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
state = scrollState,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
index 5f80379..cb68ec5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
@@ -22,8 +22,8 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Icon
@@ -41,7 +41,7 @@
TimeText(modifier = Modifier.scrollAway(scalingLazyListState))
}
) {
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
state = scalingLazyListState,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
index a1d22eb..585c7d1 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
@@ -34,7 +34,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.InlineSlider
import androidx.wear.compose.material.InlineSliderColors
@@ -49,7 +49,7 @@
var valueWithSegments by remember { mutableFloatStateOf(2f) }
var enabled by remember { mutableStateOf(true) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
@@ -105,7 +105,7 @@
var valueWithoutSegments by remember { mutableIntStateOf(5) }
var valueWithSegments by remember { mutableIntStateOf(2) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
@@ -167,7 +167,7 @@
var numberOfSegments by remember { mutableFloatStateOf(5f) }
var progress by remember { mutableFloatStateOf(10f) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
index 7807eaf..0306e38 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
@@ -29,13 +29,13 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@Composable
fun ThemeFonts() {
- ScalingLazyColumnWithRSB {
+ ScalingLazyColumn {
item {
ThemeFontRow(style = MaterialTheme.typography.display1, description = "display1")
}
@@ -88,7 +88,7 @@
@Composable
fun ThemeColors() {
- ScalingLazyColumnWithRSB {
+ ScalingLazyColumn {
item {
ThemeColorRow(
backgroundColor = MaterialTheme.colors.background,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
index e932b6a..bcebbf3 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
@@ -32,9 +32,9 @@
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
import androidx.wear.compose.material.Checkbox
import androidx.wear.compose.material.CheckboxDefaults
import androidx.wear.compose.material.ListHeader
@@ -66,7 +66,7 @@
var switchIconWithIconChecked by remember { mutableStateOf(true) }
var splitWithCustomColorChecked by remember { mutableStateOf(true) }
- ScalingLazyColumnWithRSB(
+ ScalingLazyColumn(
state = scrollState,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
index d92eaa5..9693204 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
@@ -40,10 +40,34 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public final class Fingerprint {
+ private static final int[] POW_31 = {
+ 1,
+ 31,
+ 961,
+ 29791,
+ 923521,
+ 28629151,
+ 887503681,
+ 1742810335,
+ -1807454463,
+ -196513505,
+ -1796951359,
+ 129082719,
+ -293403007,
+ -505558625,
+ 1507551809,
+ -510534177,
+ 1353309697,
+ -997072353,
+ -844471871,
+ -408824225
+ };
private static final int DEFAULT_VALUE = 0;
private final int selfTypeValue;
private int selfPropsValue;
private int childNodesValue;
+ // itself + children + grand children + grand grand children + ...
+ private int subNodeCount;
private @Nullable List<Fingerprint> childNodes;
public Fingerprint(int selfTypeValue) {
@@ -51,6 +75,7 @@
this.selfPropsValue = DEFAULT_VALUE;
this.childNodesValue = DEFAULT_VALUE;
this.childNodes = null;
+ this.subNodeCount = 1; // self
}
public Fingerprint(@NonNull NodeFingerprint proto) {
@@ -102,7 +127,12 @@
childNodes = new ArrayList<>();
}
childNodes.add(childNode);
- childNodesValue = (31 * childNodesValue) + childNode.aggregateValueAsInt();
+ // We need to include the number of grandchildren of the new node, otherwise swapping the
+ // place of "b" and "c" in a layout like "[a b] [c d]" could result in the same fingerprint.
+ int coeff = pow31Unsafe(childNode.subNodeCount);
+ childNodesValue =
+ (coeff * childNodesValue) + (childNodes.size() * childNode.aggregateValueAsInt());
+ subNodeCount += childNode.subNodeCount;
}
/** Record a property value being updated. */
@@ -115,6 +145,25 @@
selfPropsValue = (31 * selfPropsValue) + entry;
}
+ /**
+ * An int version of {@link Math#pow(double, double)} }. The result will overflow if it's larger
+ * than {@link Integer#MAX_VALUE}.
+ *
+ * <p>Note that this only support positive exponents
+ */
+ private static int pow31Unsafe(int n) {
+ // Check for available precomputed result (as an optimization).
+ if (n < POW_31.length) {
+ return POW_31[n];
+ }
+
+ int result = POW_31[POW_31.length - 1];
+ for (int i = POW_31.length; i <= n; i++) {
+ result *= 31;
+ }
+ return result;
+ }
+
NodeFingerprint toProto() {
NodeFingerprint.Builder builder = NodeFingerprint.newBuilder();
if (selfTypeValue() != 0) {
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
index 2f99ad8..a6eda1c 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
@@ -28,9 +28,13 @@
public final class FingerprintTest {
private static final int SELF_TYPE_VALUE = 1234;
private static final int FIELD_1 = 1;
- private static final int VALUE_HASH1 = 10;
-
- private static final int DISCARDED_VALUE = -1;
+ private static final int VALUE_HASH1 = 101;
+ private static final int FIELD_2 = 2;
+ private static final int VALUE_HASH2 = 202;
+ private static final int FIELD_3 = 3;
+ private static final int VALUE_HASH3 = 301;
+ private static final int FIELD_4 = 4;
+ private static final int VALUE_HASH4 = 401;
@Test
public void addChildNode() {
@@ -59,4 +63,33 @@
assertThat(child.selfTypeValue()).isEqualTo(SELF_TYPE_VALUE);
assertThat(child.selfPropsValue()).isEqualTo(31 * FIELD_1 + VALUE_HASH1);
}
+
+ @Test
+ public void childNodeOrderMatters() {
+ Fingerprint root1 = new Fingerprint(SELF_TYPE_VALUE);
+ Fingerprint root2 = new Fingerprint(SELF_TYPE_VALUE);
+ Fingerprint parent12 = createParentFor(FIELD_1, VALUE_HASH1, FIELD_2, VALUE_HASH2);
+ Fingerprint parent34 = createParentFor(FIELD_3, VALUE_HASH3, FIELD_4, VALUE_HASH4);
+ Fingerprint parent13 = createParentFor(FIELD_1, VALUE_HASH1, FIELD_3, VALUE_HASH3);
+ Fingerprint parent24 = createParentFor(FIELD_2, VALUE_HASH2, FIELD_4, VALUE_HASH4);
+
+ root1.addChildNode(parent12);
+ root1.addChildNode(parent34);
+ root2.addChildNode(parent13);
+ root2.addChildNode(parent24);
+
+ assertThat(root1.childNodesValue()).isNotEqualTo(root2.childNodesValue());
+ }
+
+ private Fingerprint createParentFor(
+ int fieldId1, int fieldValue1, int fieldId2, int fieldValue2) {
+ Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+ Fingerprint child1FingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+ child1FingerPrint.recordPropertyUpdate(fieldId1, fieldValue1);
+ parentFingerPrint.addChildNode(child1FingerPrint);
+ Fingerprint child2FingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+ child2FingerPrint.recordPropertyUpdate(fieldId2, fieldValue2);
+ parentFingerPrint.addChildNode(child2FingerPrint);
+ return parentFingerPrint;
+ }
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
index f0a0f04..0b7b008 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
@@ -634,7 +634,7 @@
string zone_id = 2;
}
-// The datetime part to retrieve using ZonedDateTimePartOp.
+// The date-time part to retrieve using ZonedDateTimePartOp.
enum ZonedDateTimePartType {
// Undefined date-time part type.
ZONED_DATE_TIME_PART_UNDEFINED = 0;
@@ -654,12 +654,12 @@
ZONED_DATE_TIME_PART_YEAR = 7;
}
-// Retrieve the specified datetime part of a DynamicZonedDateTime instance as a
+// Retrieve the specified date-time part of a DynamicZonedDateTime instance as a
// DynamicInt32.
message GetZonedDateTimePartOp {
- // The zoned datetime input.
+ // The zoned date-time input.
DynamicZonedDateTime input = 1;
- // The datetime part to retrieve.
+ // The date-time part to retrieve.
ZonedDateTimePartType part_type = 2;
}
@@ -755,7 +755,6 @@
DurationPartType duration_part = 2;
}
-
// A dynamic Instant which sources its data from the a state entry.
message StateInstantSource {
// The key in the state to bind to.
@@ -772,4 +771,4 @@
// The namespace for the state key.
string source_namespace = 2;
-}
\ No newline at end of file
+}