Merge "Add new shapes to the shapes's library demo." into androidx-main
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
index b4cb0a6..8871d69 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
@@ -29,6 +29,8 @@
import org.jetbrains.kotlin.library.abi.AbiModality
import org.jetbrains.kotlin.library.abi.AbiProperty
import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiSignatureVersion
+import org.jetbrains.kotlin.library.abi.AbiSignatures
import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
import org.jetbrains.kotlin.library.abi.LibraryAbi
import org.jetbrains.kotlin.library.abi.LibraryManifest
@@ -37,13 +39,13 @@
import org.jetbrains.kotlin.library.abi.impl.AbiEnumEntryImpl
import org.jetbrains.kotlin.library.abi.impl.AbiFunctionImpl
import org.jetbrains.kotlin.library.abi.impl.AbiPropertyImpl
-import org.jetbrains.kotlin.library.abi.impl.AbiSignaturesImpl
import org.jetbrains.kotlin.library.abi.impl.AbiTopLevelDeclarationsImpl
import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl
class MutableAbiInfo(
val declarations: MutableList<AbiDeclaration> = mutableListOf(),
- var uniqueName: String = ""
+ var uniqueName: String = "",
+ var signatureVersions: MutableSet<AbiSignatureVersion> = mutableSetOf()
)
@OptIn(ExperimentalLibraryAbiReader::class)
@@ -82,7 +84,7 @@
target to
LibraryAbi(
uniqueName = abiInfo.uniqueName,
- signatureVersions = emptySet(),
+ signatureVersions = abiInfo.signatureVersions,
topLevelDeclarations = AbiTopLevelDeclarationsImpl(abiInfo.declarations),
manifest =
LibraryManifest(
@@ -104,20 +106,7 @@
private fun parseDeclaration(parentQualifiedName: AbiQualifiedName?): AbiDeclaration? {
// if the line begins with a comment, we may need to parse the current target list
if (cursor.parseCommentMarker() != null) {
- if (cursor.hasTargets()) {
- // There are never targets within targets, so when we encounter a new directive we
- // always reset our current targets
- val targets = cursor.parseTargets()
- targets.forEach { abiInfoByTarget.putIfAbsent(it, MutableAbiInfo()) }
- currentTargetNames.clear()
- currentTargetNames.addAll(targets)
- } else if (cursor.hasUniqueName()) {
- val uniqueName =
- cursor.parseUniqueName()
- ?: throw parseException("Failed to parse library unique name")
- currentTargets.forEach { it.uniqueName = uniqueName }
- }
- cursor.nextLine()
+ parseCommentLine()
} else if (cursor.hasClassKind()) {
return parseClass(parentQualifiedName)
} else if (cursor.hasFunctionKind()) {
@@ -134,6 +123,28 @@
return null
}
+ private fun parseCommentLine() {
+ if (cursor.hasTargets()) {
+ // There are never targets within targets, so when we encounter a new directive we
+ // always reset our current targets
+ val targets = cursor.parseTargets()
+ targets.forEach { abiInfoByTarget.putIfAbsent(it, MutableAbiInfo()) }
+ currentTargetNames.clear()
+ currentTargetNames.addAll(targets)
+ } else if (cursor.hasUniqueName()) {
+ val uniqueName =
+ cursor.parseUniqueName()
+ ?: throw parseException("Failed to parse library unique name")
+ currentTargets.forEach { it.uniqueName = uniqueName }
+ } else if (cursor.hasSignatureVersion()) {
+ val signatureVersion =
+ cursor.parseSignatureVersion()
+ ?: throw parseException("Failed to parse signature version")
+ currentTargets.forEach { it.signatureVersions.add(signatureVersion) }
+ }
+ cursor.nextLine()
+ }
+
internal fun parseClass(parentQualifiedName: AbiQualifiedName? = null): AbiClass {
val modality =
cursor.parseAbiModality() ?: throw parseException("Failed to parse class modality")
@@ -157,7 +168,7 @@
}
return AbiClassImpl(
qualifiedName = abiQualifiedName,
- signatures = fakeSignatures,
+ signatures = signaturesStub,
annotations = emptySet(), // annotations aren't part of klib dumps
modality = modality,
kind = kind,
@@ -210,7 +221,7 @@
}
return AbiPropertyImpl(
qualifiedName = qualifiedName,
- signatures = fakeSignatures,
+ signatures = signaturesStub,
annotations = emptySet(), // annotations aren't part of klib dumps
modality = modality,
kind = kind,
@@ -230,7 +241,7 @@
cursor.nextLine()
return AbiEnumEntryImpl(
qualifiedName = qualifiedName,
- signatures = fakeSignatures,
+ signatures = signaturesStub,
annotations = emptySet()
)
}
@@ -286,7 +297,7 @@
cursor.nextLine()
return AbiFunctionImpl(
qualifiedName = abiQualifiedName,
- signatures = fakeSignatures,
+ signatures = signaturesStub,
annotations = emptySet(), // annotations aren't part of klib dumps
modality = modality,
isInline = isInline,
@@ -314,7 +325,7 @@
cursor.nextLine()
return AbiConstructorImpl(
qualifiedName = abiQualifiedName,
- signatures = fakeSignatures,
+ signatures = signaturesStub,
annotations = emptySet(), // annotations aren't part of klib dumps
isInline = false, // TODO
contextReceiverParametersCount = 0, // TODO
@@ -356,13 +367,13 @@
val location = "$maybeFileName${cursor.rowIndex}:${cursor.columnIndex}"
return "$message at $location: '${cursor.currentLine}'"
}
-
- companion object {
- // placeholder signatures, currently not considered during parsing / compatibility checking
- // https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/ReadMe.md
- private val fakeSignatures = AbiSignaturesImpl(signatureV1 = null, signatureV2 = null)
- }
}
/** Exception which uses the cursor to include the location of the failure */
class ParseException(message: String) : RuntimeException(message)
+
+/** Signature implementation relies on internal rendering which we can't access and don't need */
+val signaturesStub =
+ object : AbiSignatures {
+ override operator fun get(signatureVersion: AbiSignatureVersion): String? = null
+ }
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
index cba280d..63f316f 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
@@ -25,6 +25,7 @@
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.AbiSignatureVersion
import org.jetbrains.kotlin.library.abi.AbiType
import org.jetbrains.kotlin.library.abi.AbiTypeArgument
import org.jetbrains.kotlin.library.abi.AbiTypeNullability
@@ -337,6 +338,16 @@
return parseSymbol(uniqueNameRegex)
}
+internal fun Cursor.hasSignatureVersion(): Boolean =
+ parseSymbol(signatureMarkerRegex, peek = true) != null
+
+internal fun Cursor.parseSignatureVersion(): AbiSignatureVersion? {
+ parseSymbol(signatureMarkerRegex)
+ val versionString = parseSymbol(digitRegex) ?: return null
+ val versionNumber = versionString.toInt()
+ return AbiSignatureVersion.resolveByVersionNumber(versionNumber)
+}
+
internal fun Cursor.parseEnumEntryKind(peek: Boolean = false) =
parseSymbol(enumEntryKindRegex, peek)
@@ -454,3 +465,5 @@
private val getterOrSetterSignalRegex = Regex("^<(get|set)\\-")
private val enumNameRegex = Regex("^[A-Z_]+")
private val enumEntryKindRegex = Regex("^enum\\sentry")
+private val signatureMarkerRegex = Regex("-\\sSignature\\sversion:")
+private val digitRegex = Regex("\\d+")
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
index a1cae5c..d131417 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
@@ -1297,7 +1297,6 @@
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
-
// Library unique name: <androidx:library>
$content
"""
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
index 83e8ef5..0178f86 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
@@ -22,7 +22,9 @@
import org.jetbrains.kotlin.library.abi.AbiCompoundName
import org.jetbrains.kotlin.library.abi.AbiModality
import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiSignatureVersion
import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.LibraryAbi
import org.junit.Test
@OptIn(ExperimentalLibraryAbiReader::class)
@@ -230,6 +232,16 @@
}
@Test
+ fun parsesSignatureVersion() {
+ val parsed = KlibDumpParser(exampleMetadata).parse()
+ assertThat(parsed).isNotNull()
+ assertThat(parsed.keys).hasSize(1)
+ val abi: LibraryAbi = parsed.values.single()
+ assertThat(abi.signatureVersions)
+ .containsExactly(AbiSignatureVersion.resolveByVersionNumber(2))
+ }
+
+ @Test
fun parseFullCollectionKlibDumpSucceeds() {
val parsed = KlibDumpParser(collectionDump).parse()
assertThat(parsed).isNotNull()
@@ -264,4 +276,18 @@
assertThat(iosQNames).containsExactly("my.lib/myIosFun", "my.lib/commonFun")
assertThat(linuxQNames).containsExactly("my.lib/myLinuxFun", "my.lib/commonFun")
}
+
+ companion object {
+ private val exampleMetadata =
+ """
+ // KLib ABI Dump
+ // Targets: [linuxX64]
+ // Rendering settings:
+ // - Signature version: 2
+ // - Show manifest properties: true
+ // - Show declarations: true
+ // Library unique name: <androidx:library>
+ """
+ .trimIndent()
+ }
}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
index 51c36da..bf0111b 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
@@ -538,6 +538,37 @@
}
@Test
+ fun hasSignatureVersion() {
+ val input = "// - Signature version: 2"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasSignatureVersion()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test
+ fun hasSignatureVersionFalsePositive() {
+ val input = "// - Show manifest properties: true"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasSignatureVersion()).isFalse()
+ }
+
+ @Test
+ fun parseSignatureVersion() {
+ val input = "// - Signature version: 2"
+ val cursor = Cursor(input)
+ val signatureVersion = cursor.parseSignatureVersion()
+ assertThat(signatureVersion.toString()).isEqualTo("V2")
+ }
+
+ @Test
+ fun parseSignatureVersionFromTheFuture() {
+ val input = "// - Signature version: 101"
+ val cursor = Cursor(input)
+ val signatureVersion = cursor.parseSignatureVersion()
+ assertThat(signatureVersion.toString()).isEqualTo("Unsupported(versionNumber=101)")
+ }
+
+ @Test
fun parseEnumEntryName() {
val input = "SOME_ENUM something else"
val cursor = Cursor(input)
diff --git a/browser/browser/src/main/res/values-uk/strings.xml b/browser/browser/src/main/res/values-uk/strings.xml
index 63a0419..f7af12a 100644
--- a/browser/browser/src/main/res/values-uk/strings.xml
+++ b/browser/browser/src/main/res/values-uk/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="fallback_menu_item_open_in_browser" msgid="3413186855122069269">"Відкрити у веб-переглядачі"</string>
+ <string name="fallback_menu_item_open_in_browser" msgid="3413186855122069269">"Відкрити у вебпереглядачі"</string>
<string name="fallback_menu_item_copy_link" msgid="4566929209979330987">"Копіювати посилання"</string>
<string name="fallback_menu_item_share_link" msgid="7145444925855055364">"Надіслати посилання"</string>
<string name="copy_toast_msg" msgid="3260749812566568062">"Посилання скопійовано в буфер обміну"</string>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 1465e39..10cb49a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -1381,7 +1381,6 @@
companion object {
const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
- const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
const val FINALIZE_TEST_CONFIGS_WITH_APKS_TASK = "finalizeTestConfigsWithApks"
const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
index 549bacc..cc9782a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
@@ -159,7 +159,7 @@
KotlinKlibExtractSupportedTargetsAbiTask::class.java
) {
it.strictValidation = true
- it.supportedTargets = project.provider { supportedTargets() }
+ it.supportedTargets = project.provider { supportedNativeTargetNames() }
it.inputAbiFile = requiredCompatFile
it.outputAbiFile = klibExtractDir.resolve(requiredCompatFile.name)
(it as DefaultTask).group = ABI_GROUP_NAME
@@ -195,6 +195,7 @@
it.version.set(projectVersion)
it.shouldWriteVersionedApiFile.set(project.shouldWriteVersionedApiFile())
it.group = ABI_GROUP_NAME
+ it.unsupportedNativeTargetNames.set(unsupportedNativeTargetNames())
}
/**
@@ -205,7 +206,7 @@
private fun Project.extractKlibAbiTask(klibApiDir: File, extractDir: File) =
project.tasks.register(EXTRACT_NAME, KotlinKlibExtractSupportedTargetsAbiTask::class.java) {
it.strictValidation = true
- it.supportedTargets = project.provider { supportedTargets() }
+ it.supportedTargets = project.provider { supportedNativeTargetNames() }
it.inputAbiFile = klibApiDir.resolve(CURRENT_API_FILE_NAME)
it.outputAbiFile = extractDir.resolve(CURRENT_API_FILE_NAME)
(it as DefaultTask).group = ABI_GROUP_NAME
@@ -247,19 +248,21 @@
}
}
- private fun supportedTargets(): Set<String> {
+ private fun supportedNativeTargetNames(): Set<String> {
val hostManager = HostManager()
- return kotlinMultiplatformExtension.targets
- .matching { it.platformType == KotlinPlatformType.native }
- .asSequence()
- .filterIsInstance<KotlinNativeTarget>()
+ return kotlinMultiplatformExtension
+ .nativeTargets()
.filter { hostManager.isEnabled(it.konanTarget) }
- .map {
- KlibTarget(it.targetName, konanTargetNameMapping[it.konanTarget.name]!!).toString()
- }
+ .map { it.klibTargetName() }
.toSet()
}
+ private fun allNativeTargetNames(): Set<String> =
+ kotlinMultiplatformExtension.nativeTargets().map { it.klibTargetName() }.toSet()
+
+ private fun unsupportedNativeTargetNames(): Set<String> =
+ allNativeTargetNames() - supportedNativeTargetNames()
+
private fun Project.configureKlibCompilation(
compilation: KotlinCompilation<KotlinCommonOptions>,
targetName: String,
@@ -289,4 +292,9 @@
)
private fun KotlinMultiplatformExtension.nativeTargets() =
- targets.matching { it.platformType == KotlinPlatformType.native }
+ targets.withType(KotlinNativeTarget::class.java).matching {
+ it.platformType == KotlinPlatformType.native
+ }
+
+private fun KotlinNativeTarget.klibTargetName(): String =
+ KlibTarget(targetName, konanTargetNameMapping[konanTarget.name]!!).toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
index 753a5b3..c28899f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
@@ -19,7 +19,9 @@
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.CacheableTask
@@ -39,6 +41,8 @@
@get:Input abstract val shouldWriteVersionedApiFile: Property<Boolean>
+ @get:Input abstract val unsupportedNativeTargetNames: ListProperty<String>
+
/** Text file from which API signatures will be read. */
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFile
@@ -49,6 +53,14 @@
@TaskAction
fun execute() {
+ unsupportedNativeTargetNames.get().let { targets ->
+ if (targets.isNotEmpty()) {
+ throw GradleException(
+ "Cannot update API files because the current host doesn't support the " +
+ "following targets: ${targets.joinToString(", ")}"
+ )
+ }
+ }
fileSystemOperations.copy {
it.from(inputApiLocation)
it.into(outputDir)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
new file mode 100644
index 0000000..ffa92365
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.build.testConfiguration
+
+import com.android.build.api.variant.BuiltArtifactsLoader
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/** Copy APKs needed for building androidTest.zip */
+@DisableCachingByDefault(because = "Only filesystem operations")
+abstract class CopyTestApksTask @Inject constructor(private val objects: ObjectFactory) :
+ DefaultTask() {
+
+ /** File existence check to determine whether to run this task. */
+ @get:InputFiles
+ @get:SkipWhenEmpty
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val androidTestSourceCode: ConfigurableFileCollection
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val testFolder: DirectoryProperty
+
+ @get:InputFiles
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val appFolder: DirectoryProperty
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val appFileCollection: ConfigurableFileCollection
+
+ /**
+ * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:InputFiles
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.RELATIVE) // We use parent folder for file name generation
+ abstract val privacySandboxSdkApks: ConfigurableFileCollection
+
+ /**
+ * Extracted split with manifest containing <uses-sdk-library> tag. Produced by AGP.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:InputFiles
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
+ abstract val privacySandboxUsesSdkSplit: ConfigurableFileCollection
+
+ /**
+ * Extracted compat splits for PrivacySandbox SDKs dependencies. Produced by AGP.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:InputFiles
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
+ abstract val privacySandboxSdkCompatSplits: ConfigurableFileCollection
+
+ /**
+ * Filename prefix for all PrivacySandbox related output files. Required for producing unique
+ * filenames over all projects.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:Input @get:Optional abstract val filenamePrefixForPrivacySandboxFiles: Property<String>
+
+ @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
+
+ @get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
+
+ @get:OutputFile abstract val outputApplicationId: RegularFileProperty
+
+ @get:OutputFile abstract val outputTestApk: RegularFileProperty
+
+ @get:[OutputFile Optional]
+ abstract val outputAppApk: RegularFileProperty
+
+ /**
+ * Output directory for PrivacySandbox SDKs APKs.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:[OutputDirectory Optional]
+ abstract val outputPrivacySandboxSdkApks: DirectoryProperty
+
+ /**
+ * Output directory for App splits required for devices with PrivacySandbox support.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:[OutputDirectory Optional]
+ abstract val outputPrivacySandboxAppSplits: DirectoryProperty
+
+ /**
+ * Output directory for App splits required for devices without PrivacySandbox support.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
+ @get:[OutputDirectory Optional]
+ abstract val outputPrivacySandboxCompatAppSplits: DirectoryProperty
+
+ @TaskAction
+ fun createApks() {
+ if (appLoader.isPresent) {
+ // Decides where to load the app apk from, depending on whether appFolder or
+ // appFileCollection has been set.
+ val appDir =
+ if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
+ appFolder.get()
+ } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
+ objects
+ .directoryProperty()
+ .also { it.set(appFileCollection.files.first()) }
+ .get()
+ } else {
+ throw IllegalStateException(
+ """
+ App apk not specified or both appFileCollection and appFolder specified.
+ """
+ .trimIndent()
+ )
+ }
+
+ val appApk =
+ appLoader.get().load(appDir)
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ // We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
+ val appApkBuiltArtifact = appApk.elements.single()
+ val destinationApk = outputAppApk.get().asFile
+ File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
+
+ createPrivacySandboxFiles()
+ }
+
+ val testApk =
+ testLoader.get().load(testFolder.get())
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ val testApkBuiltArtifact = testApk.elements.single()
+ val destinationApk = outputTestApk.get().asFile
+ File(testApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
+
+ val outputApplicationIdFile = outputApplicationId.get().asFile
+ outputApplicationIdFile.bufferedWriter().use { out -> out.write(testApk.applicationId) }
+ }
+
+ /**
+ * Creates APKs required for running App with PrivacySandbox SDKs. Do nothing if project doesn't
+ * have dependencies on PrivacySandbox SDKs.
+ */
+ private fun createPrivacySandboxFiles() {
+ if (privacySandboxSdkApks.isEmpty) {
+ return
+ }
+
+ val prefix = filenamePrefixForPrivacySandboxFiles.get()
+
+ privacySandboxSdkApks.asFileTree.map { sdkApk ->
+ // TODO (b/309610890): Remove after supporting unique filenames on bundletool side.
+ val sdkProjectName = sdkApk.parentFile?.name
+ val outputFileName = "$prefix-$sdkProjectName-${sdkApk.name}"
+ val outputFile = outputPrivacySandboxSdkApks.get().file(outputFileName)
+ sdkApk.copyTo(outputFile.asFile, overwrite = true)
+ }
+
+ val usesSdkSplitArtifact =
+ appLoader.get().load(privacySandboxUsesSdkSplit)?.elements?.single()
+ if (usesSdkSplitArtifact != null) {
+ val splitApk = File(usesSdkSplitArtifact.outputFile)
+ val outputFileName = "$prefix-${splitApk.name}"
+ val outputFile = outputPrivacySandboxAppSplits.get().file(outputFileName)
+ splitApk.copyTo(outputFile.asFile, overwrite = true)
+ }
+
+ appLoader.get().load(privacySandboxSdkCompatSplits)?.elements?.forEach { splitArtifact ->
+ val splitApk = File(splitArtifact.outputFile)
+ val outputFileName = "$prefix-${splitApk.name}"
+ val outputFile = outputPrivacySandboxCompatAppSplits.get().file(outputFileName)
+ splitApk.copyTo(outputFile.asFile, overwrite = true)
+ }
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index b26463e..5b57b81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -16,23 +16,18 @@
package androidx.build.testConfiguration
-import com.android.build.api.variant.BuiltArtifactsLoader
import java.io.File
-import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
@@ -43,47 +38,49 @@
/**
* Writes a configuration file in <a
* href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested. This config gets ingested by Tradefed.
+ * format that gets zipped alongside the APKs to be tested.
+ *
+ * Generates XML for Tradefed test infrastructure and JSON for FTL test infrastructure.
*/
@DisableCachingByDefault(because = "Doesn't benefit from caching")
-abstract class GenerateTestConfigurationTask
-@Inject
-constructor(private val objects: ObjectFactory) : DefaultTask() {
+abstract class GenerateTestConfigurationTask : DefaultTask() {
- @get:InputFiles
+ @get:InputFile
@get:Optional
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val appFolder: DirectoryProperty
-
- @get:InputFiles
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val appFileCollection: ConfigurableFileCollection
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
+ abstract val appApk: RegularFileProperty
/** File existence check to determine whether to run this task. */
@get:InputFiles
@get:SkipWhenEmpty
- @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:PathSensitive(PathSensitivity.NONE)
abstract val androidTestSourceCodeCollection: ConfigurableFileCollection
- @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
-
- /** Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP. */
+ /**
+ * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
@get:InputFiles
@get:Optional
- @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val privacySandboxSdkApks: ConfigurableFileCollection
- /** Extracted split with manifest containing <uses-sdk-library> tag. Produced by AGP. */
+ /**
+ * Extracted splits required for running app with PrivacySandbox SDKs. Produced by AGP.
+ *
+ * Should be set only for applications with PrivacySandbox SDKs dependencies.
+ */
@get:InputFiles
@get:Optional
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val privacySandboxUsesSdkSplit: ConfigurableFileCollection
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
+ abstract val privacySandboxAppSplits: ConfigurableFileCollection
- @get:InputFiles
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val testFolder: DirectoryProperty
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.NAME_ONLY)
+ abstract val testApk: RegularFileProperty
- @get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
+ @get:Input abstract val applicationId: Property<String>
@get:Input abstract val minSdk: Property<Int>
@@ -103,22 +100,13 @@
@get:OutputFile abstract val outputXml: RegularFileProperty
- @get:OutputFile abstract val outputJson: RegularFileProperty
-
- @get:OutputFile abstract val outputTestApk: RegularFileProperty
-
- @get:[OutputFile Optional]
- abstract val outputAppApk: RegularFileProperty
-
/**
- * Filename prefix for all PrivacySandbox related output files. Required for producing unique
- * filenames over all projects,
+ * Optional as privacy sandbox not yet supported in JSON configs.
+ *
+ * TODO (b/347315428): Support privacy sandbox on FTL.
*/
- @get:Input @get:Optional abstract val outputPrivacySandboxFilenamesPrefix: Property<String>
-
- /** Output directory for PrivacySandbox files (SDKs APKs, splits, etc). */
- @get:[OutputDirectory Optional]
- abstract val outputPrivacySandboxFiles: DirectoryProperty
+ @get:[OutputFile Optional]
+ abstract val outputJson: RegularFileProperty
@TaskAction
fun generateAndroidTestZip() {
@@ -130,39 +118,18 @@
*/
val configBuilder = ConfigBuilder()
configBuilder.configName = outputXml.asFile.get().name
- if (appLoader.isPresent) {
-
- // Decides where to load the app apk from, depending on whether appFolder or
- // appFileCollection has been set.
- val appDir =
- if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
- appFolder.get()
- } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
- objects
- .directoryProperty()
- .also { it.set(appFileCollection.files.first()) }
- .get()
- } else {
- throw IllegalStateException(
- """
- App apk not specified or both appFileCollection and appFolder specified.
- """
- .trimIndent()
- )
- }
-
- val appApk =
- appLoader.get().load(appDir)
- ?: throw RuntimeException("Cannot load required APK for task: $name")
- // We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
- val appApkBuiltArtifact = appApk.elements.single()
- val destinationApk = outputAppApk.get().asFile
- File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
- configBuilder
- .appApkName(destinationApk.name)
- .appApkSha256(sha256(File(appApkBuiltArtifact.outputFile)))
- configurePrivacySandbox(configBuilder)
+ if (appApk.isPresent) {
+ val appApkFile = appApk.get().asFile
+ configBuilder.appApkName(appApkFile.name).appApkSha256(sha256(appApkFile))
}
+
+ val privacySandboxSdkApksFileNames =
+ privacySandboxSdkApks.asFileTree.map { f -> f.name }.sorted()
+ configBuilder.initialSetupApks(privacySandboxSdkApksFileNames)
+ val privacySandboxSplitsFileNames =
+ privacySandboxAppSplits.asFileTree.map { f -> f.name }.sorted()
+ configBuilder.appSplits(privacySandboxSplitsFileNames)
+
configBuilder.additionalApkKeys(additionalApkKeys.get())
val isPresubmit = presubmit.get()
configBuilder.isPostsubmit(!isPresubmit)
@@ -193,79 +160,29 @@
configBuilder.tag("androidx_unit_tests")
}
additionalTags.get().forEach { configBuilder.tag(it) }
- val testApk =
- testLoader.get().load(testFolder.get())
- ?: throw RuntimeException("Cannot load required APK for task: $name")
- val testApkBuiltArtifact = testApk.elements.single()
- val destinationApk = outputTestApk.get().asFile
- File(testApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
instrumentationArgs.get().forEach { (key, value) ->
configBuilder.instrumentationArgsMap[key] = value
}
+ val testApkFile = testApk.get().asFile
configBuilder
- .testApkName(destinationApk.name)
- .applicationId(testApk.applicationId)
+ .testApkName(testApkFile.name)
+ .applicationId(applicationId.get())
.minSdk(minSdk.get().toString())
.testRunner(testRunner.get())
- .testApkSha256(sha256(File(testApkBuiltArtifact.outputFile)))
+ .testApkSha256(sha256(testApkFile))
createOrFail(outputXml).writeText(configBuilder.buildXml())
- if (!outputJson.asFile.get().name.startsWith("_")) {
- // Prefixing json file names with _ allows us to collocate these files
- // inside of the androidTest.zip to make fetching them less expensive.
- throw GradleException(
- "json output file names are expected to use _ prefix to, " +
- "currently set to ${outputJson.asFile.get().name}"
- )
- }
- if (privacySandboxSdkApks.isEmpty) {
- // Privacy sandbox not yet supported in JSON configs
+ if (outputJson.isPresent) {
+ if (!outputJson.asFile.get().name.startsWith("_")) {
+ // Prefixing json file names with _ allows us to collocate these files
+ // inside of the androidTest.zip to make fetching them less expensive.
+ throw GradleException(
+ "json output file names are expected to use _ prefix to, " +
+ "currently set to ${outputJson.asFile.get().name}"
+ )
+ }
createOrFail(outputJson).writeText(configBuilder.buildJson())
}
}
-
- /**
- * Configure installation of PrivacySandbox SDKs before main and test APKs. Do nothing if
- * project doesn't have dependencies on PrivacySandbox SDKs.
- */
- private fun configurePrivacySandbox(configBuilder: ConfigBuilder) {
- if (privacySandboxSdkApks.isEmpty) {
- return
- }
-
- val prefix = outputPrivacySandboxFilenamesPrefix.get()
- val sdkApkFileNames =
- privacySandboxSdkApks.asFileTree.map { sdkApk ->
- // TODO (b/309610890): Remove after supporting unique filenames on bundletool side.
- val sdkProjectName = sdkApk.parentFile?.name
- val outputFileName = "$prefix-$sdkProjectName-${sdkApk.name}"
- val outputFile = outputPrivacySandboxFiles.get().file(outputFileName)
- sdkApk.copyTo(outputFile.asFile, overwrite = true)
- outputFileName
- }
- configBuilder.initialSetupApks(sdkApkFileNames)
-
- val usesSdkSplitArtifact =
- appLoader.get().load(privacySandboxUsesSdkSplit)?.elements?.single()
- if (usesSdkSplitArtifact != null) {
- val splitApk = File(usesSdkSplitArtifact.outputFile)
- val outputFileName = "$prefix-${splitApk.name}"
- val outputFile = outputPrivacySandboxFiles.get().file(outputFileName)
- splitApk.copyTo(outputFile.asFile, overwrite = true)
- configBuilder.appSplits(listOf(outputFileName))
- }
-
- if (minSdk.get() < PRIVACY_SANDBOX_MIN_API_LEVEL) {
- /*
- Privacy Sandbox SDKs could be installed starting from PRIVACY_SANDBOX_MIN_API_LEVEL.
- Separate compat config will be generated for lower api levels.
- */
- configBuilder.minSdk(PRIVACY_SANDBOX_MIN_API_LEVEL.toString())
- }
- }
-
- companion object {
- private const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
- }
}
internal fun createOrFail(fileProperty: RegularFileProperty): File {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 95477657..5787c86 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -17,7 +17,6 @@
package androidx.build.testConfiguration
import androidx.build.AndroidXExtension
-import androidx.build.AndroidXImplPlugin
import androidx.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK
import androidx.build.asFilenamePrefix
import androidx.build.dependencyTracker.AffectedModuleDetector
@@ -44,9 +43,11 @@
import com.android.build.api.variant.TestAndroidComponentsExtension
import com.android.build.api.variant.TestVariant
import com.android.build.api.variant.Variant
+import kotlin.math.max
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
import org.gradle.api.attributes.Usage
+import org.gradle.api.file.Directory
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
@@ -69,62 +70,112 @@
instrumentationRunnerArgs: Provider<Map<String, String>>,
variant: Variant?
) {
- val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
- val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
- rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
- it.testModules.add(
- TestModule(
- name = xmlName,
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
+ val copyTestApksTask = registerCopyTestApksTask(variantName, artifacts, variant)
+
+ if (isPrivacySandboxEnabled()) {
+ /*
+ Privacy Sandbox SDKs could be installed starting from PRIVACY_SANDBOX_MIN_API_LEVEL.
+ Separate compat config generated for lower api levels.
+ */
+ registerGenerateTestConfigurationTask(
+ "${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}$variantName",
+ xmlName = "${path.asFilenamePrefix()}$variantName.xml",
+ jsonName = null, // Privacy sandbox not yet supported in JSON configs
+ copyTestApksTask.flatMap { it.outputApplicationId },
+ copyTestApksTask.flatMap { it.outputAppApk },
+ copyTestApksTask.flatMap { it.outputTestApk },
+ copyTestApksTask.flatMap { it.outputPrivacySandboxSdkApks },
+ copyTestApksTask.flatMap { it.outputPrivacySandboxAppSplits },
+ minSdk = max(minSdk, PRIVACY_SANDBOX_MIN_API_LEVEL),
+ testRunner,
+ instrumentationRunnerArgs,
+ variant
+ )
+
+ registerGenerateTestConfigurationTask(
+ "${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${variantName}",
+ xmlName = "${path.asFilenamePrefix()}${variantName}Compat.xml",
+ jsonName = null, // Privacy sandbox not yet supported in JSON configs
+ copyTestApksTask.flatMap { it.outputApplicationId },
+ copyTestApksTask.flatMap { it.outputAppApk },
+ copyTestApksTask.flatMap { it.outputTestApk },
+ privacySandboxApks = null,
+ copyTestApksTask.flatMap { it.outputPrivacySandboxCompatAppSplits },
+ minSdk,
+ testRunner,
+ instrumentationRunnerArgs,
+ variant
+ )
+ } else {
+ registerGenerateTestConfigurationTask(
+ "${GENERATE_TEST_CONFIGURATION_TASK}$variantName",
+ xmlName = "${path.asFilenamePrefix()}$variantName.xml",
+ jsonName = "_${path.asFilenamePrefix()}$variantName.json",
+ copyTestApksTask.flatMap { it.outputApplicationId },
+ copyTestApksTask.flatMap { it.outputAppApk },
+ copyTestApksTask.flatMap { it.outputTestApk },
+ privacySandboxApks = null,
+ privacySandboxSplits = null,
+ minSdk,
+ testRunner,
+ instrumentationRunnerArgs,
+ variant
)
}
- val generateTestConfigurationTask =
- tasks.register(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName",
- GenerateTestConfigurationTask::class.java
- ) { task ->
- val androidXExtension = extensions.getByType<AndroidXExtension>()
- if (isPrivacySandboxEnabled()) {
- // TODO (b/309610890): Replace for dependency on AGP artifact.
- val extractedPrivacySandboxSdkApksDir =
- layout.buildDirectory.dir(
- "intermediates/extracted_apks_from_privacy_sandbox_sdks"
- )
- task.privacySandboxSdkApks.from(
- files(extractedPrivacySandboxSdkApksDir) {
- it.builtBy("buildPrivacySandboxSdkApksForDebug")
- }
- )
- // TODO (b/309610890): Replace for dependency on AGP artifact.
- val usesSdkSplitDir =
- layout.buildDirectory.dir(
- "intermediates/uses_sdk_library_split_for_local_deployment"
- )
- task.privacySandboxUsesSdkSplit.from(
- files(usesSdkSplitDir) {
- it.builtBy("generateDebugAdditionalSplitForPrivacySandboxDeployment")
- }
- )
- task.outputPrivacySandboxFilenamesPrefix.set(
- "${path.asFilenamePrefix()}-$variantName"
- )
- task.outputPrivacySandboxFiles.set(
- getPrivacySandboxFilesDirectory().map {
- it.dir("${path.asFilenamePrefix()}-$variantName")
- }
- )
- }
+}
- task.testFolder.set(artifacts.get(SingleArtifact.APK))
- task.testLoader.set(artifacts.getBuiltArtifactsLoader())
- task.outputTestApk.set(
- getFileInTestConfigDirectory("${path.asFilenamePrefix()}-$variantName.apk")
- )
+private fun Project.registerCopyTestApksTask(
+ variantName: String,
+ artifacts: Artifacts,
+ variant: Variant?
+): TaskProvider<CopyTestApksTask> {
+ return tasks.register("${COPY_TEST_APKS_TASK}$variantName", CopyTestApksTask::class.java) { task
+ ->
+ task.testFolder.set(artifacts.get(SingleArtifact.APK))
+ task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+
+ task.outputApplicationId.set(layout.buildDirectory.file("$variantName-appId.txt"))
+ task.outputTestApk.set(
+ getFileInTestConfigDirectory("${path.asFilenamePrefix()}-$variantName.apk")
+ )
+
+ // Skip task if getTestSourceSetsForAndroid is empty, even if
+ // androidXExtension.deviceTests.enabled is set to true
+ task.androidTestSourceCode.from(getTestSourceSetsForAndroid(variant))
+ val androidXExtension = extensions.getByType<AndroidXExtension>()
+ task.enabled = androidXExtension.deviceTests.enabled
+ AffectedModuleDetector.configureTaskGuard(task)
+ }
+}
+
+private fun Project.registerGenerateTestConfigurationTask(
+ taskName: String,
+ xmlName: String,
+ jsonName: String?,
+ applicationIdFile: Provider<RegularFile>,
+ appApk: Provider<RegularFile>,
+ testApk: Provider<RegularFile>,
+ privacySandboxApks: Provider<Directory>?,
+ privacySandboxSplits: Provider<Directory>?,
+ minSdk: Int,
+ testRunner: Provider<String>,
+ instrumentationRunnerArgs: Provider<Map<String, String>>,
+ variant: Variant?
+) {
+ val generateTestConfigurationTask =
+ tasks.register(taskName, GenerateTestConfigurationTask::class.java) { task ->
+ task.applicationId.set(project.providers.fileContents(applicationIdFile).asText)
+ task.appApk.set(appApk)
+ task.testApk.set(testApk)
+
+ privacySandboxApks?.let { task.privacySandboxSdkApks.from(it) }
+ privacySandboxSplits?.let { task.privacySandboxAppSplits.from(it) }
+
+ val androidXExtension = extensions.getByType<AndroidXExtension>()
task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
task.outputXml.set(getFileInTestConfigDirectory(xmlName))
- task.outputJson.set(getFileInTestConfigDirectory(jsonName))
+ jsonName?.let { task.outputJson.set(getFileInTestConfigDirectory(it)) }
task.presubmit.set(isPresubmitBuild())
task.instrumentationArgs.putAll(instrumentationRunnerArgs)
task.minSdk.set(minSdk)
@@ -140,6 +191,14 @@
rootProject.tasks
.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(generateTestConfigurationTask)
+ rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
+ it.testModules.add(
+ TestModule(
+ name = xmlName,
+ path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+ )
+ )
+ }
}
/**
@@ -162,18 +221,65 @@
return getFileInTestConfigDirectory(filename)
}
+ fun addPrivacySandboxApksFor(variant: Variant, task: CopyTestApksTask) {
+ // TODO (b/309610890): Replace for dependency on AGP artifact.
+ val extractedPrivacySandboxSdkApksDir =
+ layout.buildDirectory.dir("intermediates/extracted_apks_from_privacy_sandbox_sdks")
+ task.privacySandboxSdkApks.from(
+ files(extractedPrivacySandboxSdkApksDir) {
+ it.builtBy("buildPrivacySandboxSdkApksForDebug")
+ }
+ )
+ // TODO (b/309610890): Replace for dependency on AGP artifact.
+ val usesSdkSplitDir =
+ layout.buildDirectory.dir("intermediates/uses_sdk_library_split_for_local_deployment")
+ task.privacySandboxUsesSdkSplit.from(
+ files(usesSdkSplitDir) {
+ it.builtBy("generateDebugAdditionalSplitForPrivacySandboxDeployment")
+ }
+ )
+ // TODO (b/309610890): Replace for dependency on AGP artifact.
+ val extractedPrivacySandboxCompatSplitsDir =
+ layout.buildDirectory.dir("intermediates/extracted_sdk_apks")
+ task.privacySandboxSdkCompatSplits.from(
+ files(extractedPrivacySandboxCompatSplitsDir) {
+ it.builtBy("extractApksFromSdkSplitsForDebug")
+ }
+ )
+ task.filenamePrefixForPrivacySandboxFiles.set("${path.asFilenamePrefix()}-${variant.name}")
+ task.outputPrivacySandboxSdkApks.set(
+ getPrivacySandboxFilesDirectory().map {
+ it.dir("${path.asFilenamePrefix()}-${variant.name}-sdks")
+ }
+ )
+ task.outputPrivacySandboxAppSplits.set(
+ getPrivacySandboxFilesDirectory().map {
+ it.dir("${path.asFilenamePrefix()}-${variant.name}-app-splits")
+ }
+ )
+ task.outputPrivacySandboxCompatAppSplits.set(
+ getPrivacySandboxFilesDirectory().map {
+ it.dir("${path.asFilenamePrefix()}-${variant.name}-compat-app-splits")
+ }
+ )
+ }
+
// For application modules, the instrumentation apk is generated in the module itself
extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
onVariants(selector().withBuildType("debug")) { variant ->
tasks.named(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
- GenerateTestConfigurationTask::class.java
+ "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
+ CopyTestApksTask::class.java
) { task ->
task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
// The target project is the same being evaluated
task.outputAppApk.set(outputAppApkFile(variant, path, null))
+
+ if (isPrivacySandboxEnabled()) {
+ addPrivacySandboxApksFor(variant, task)
+ }
}
}
}
@@ -184,10 +290,8 @@
// from the application one.
extensions.findByType(TestAndroidComponentsExtension::class.java)?.apply {
onVariants(selector().all()) { variant ->
- tasks.named(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}",
- GenerateTestConfigurationTask::class.java
- ) { task ->
+ tasks.named("${COPY_TEST_APKS_TASK}${variant.name}", CopyTestApksTask::class.java) {
+ task ->
task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
// The target app path is defined in the targetProjectPath field in the android
@@ -245,8 +349,8 @@
}
tasks.named(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
- GenerateTestConfigurationTask::class.java
+ "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
+ CopyTestApksTask::class.java
) { task ->
task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
@@ -273,11 +377,11 @@
!parentProject.tasks
.withType(GenerateMediaTestConfigurationTask::class.java)
.names
- .contains("support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}")
+ .contains("support-media-test${GENERATE_TEST_CONFIGURATION_TASK}")
) {
val task =
parentProject.tasks.register(
- "support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}",
+ "support-media-test${GENERATE_TEST_CONFIGURATION_TASK}",
GenerateMediaTestConfigurationTask::class.java
) { task ->
AffectedModuleDetector.configureTaskGuard(task)
@@ -287,7 +391,7 @@
} else {
return parentProject.tasks
.withType(GenerateMediaTestConfigurationTask::class.java)
- .named("support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}")
+ .named("support-media-test${GENERATE_TEST_CONFIGURATION_TASK}")
}
}
@@ -519,3 +623,11 @@
private fun Project.isPrivacySandboxEnabled(): Boolean =
extensions.findByType(ApplicationExtension::class.java)?.privacySandbox?.enable ?: false
+
+private const val COPY_TEST_APKS_TASK = "CopyTestApks"
+private const val GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK =
+ "GeneratePrivacySandboxMainTestConfiguration"
+private const val GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK =
+ "GeneratePrivacySandboxCompatTestConfiguration"
+private const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
+private const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
index edefeed..e50341f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
@@ -175,7 +175,7 @@
<string name="notification_template_demo_title" msgid="5076051497316030274">"الگوی اعلان نمونه"</string>
<string name="nav_map_template_demo_title" msgid="344985380763975398">"نمونه الگوی ناوبری فقط با نقشه"</string>
<string name="nav_demos_title" msgid="72781206086461004">"نمونههای ناوبری"</string>
- <string name="navigation_alert_title" msgid="8306554249264200848">"باز هم دام سرعت وجود دارد؟"</string>
+ <string name="navigation_alert_title" msgid="8306554249264200848">"باز هم تله سرعت وجود دارد؟"</string>
<string name="navigation_alert_subtitle" msgid="3331130131492672264">"۱۰ دقیقه پیش گزارش شده است"</string>
<string name="no_toll_card_permission" msgid="6789073114449712090">"اجازه کارت عوارض اعطا نشده است."</string>
<string name="no_energy_level_permission" msgid="1684773185095107825">"اجازه میزان سوخت اعطا نشده است."</string>
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index c5a1983..c7fc05d 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -388,11 +388,11 @@
package androidx.compose.foundation.gestures {
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface AnchoredDragScope {
+ public interface AnchoredDragScope {
method public void dragTo(float newOffset, optional float lastKnownVelocity);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class AnchoredDraggableDefaults {
+ public final class AnchoredDraggableDefaults {
method @androidx.compose.runtime.Composable public <T> androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
@@ -408,14 +408,16 @@
method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
+ method public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
- ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+ @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
+ ctor public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
ctor public AnchoredDraggableState(T initialValue, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method public suspend Object? anchoredDrag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? anchoredDrag(T targetValue, optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function4<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super T,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -452,7 +454,7 @@
public static final class AnchoredDraggableState.Companion {
method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+ method public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index d9c60c2..88f35f2 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -390,11 +390,11 @@
package androidx.compose.foundation.gestures {
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface AnchoredDragScope {
+ public interface AnchoredDragScope {
method public void dragTo(float newOffset, optional float lastKnownVelocity);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class AnchoredDraggableDefaults {
+ public final class AnchoredDraggableDefaults {
method @androidx.compose.runtime.Composable public <T> androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
@@ -410,14 +410,16 @@
method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
+ method public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
}
- @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
- ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+ @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
+ ctor public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
ctor public AnchoredDraggableState(T initialValue, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method public suspend Object? anchoredDrag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? anchoredDrag(T targetValue, optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function4<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super T,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -454,7 +456,7 @@
public static final class AnchoredDraggableState.Companion {
method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+ method public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index c13241a..60cf8e8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -1447,6 +1447,21 @@
}
}
+ @Test
+ fun longText_doesNotCrash() {
+ var textLayoutProvider: (() -> TextLayoutResult?)? = null
+ inputMethodInterceptor.setTextFieldTestContent {
+ BasicTextField(
+ rememberTextFieldState("A".repeat(100_000)),
+ onTextLayout = { textLayoutProvider = it }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(textLayoutProvider?.invoke()?.layoutInput?.text?.length).isEqualTo(100_000)
+ }
+ }
+
private fun requestFocus(tag: String) = rule.onNodeWithTag(tag).requestFocus()
private fun assertTextSelection(expected: TextRange) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
index 2fc1d32..d18722d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -341,4 +341,17 @@
)
assertThat(actual.height).isEqualTo(expected.height)
}
+
+ @Test
+ fun hugeString_doesntCrash() {
+ val text = "A".repeat(100_000)
+ val subject =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontSize = 100.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ )
+ .also { it.density = density }
+ subject.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
index 18acdfe..6f75020 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
@@ -328,6 +328,15 @@
assertThat(subject.slowCreateTextLayoutResultOrNull(style = style)).isNotNull()
}
+ @Test
+ fun hugeString_doesntCrash() {
+ val text = "A".repeat(100_000)
+ val style = createTextStyle(fontSize = 100.sp)
+ val subject =
+ ParagraphLayoutCache(text, style, fontFamilyResolver).also { it.density = density }
+ subject.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
+ }
+
private fun createTextStyle(
fontSize: TextUnit,
letterSpacing: TextUnit = TextUnit.Unspecified
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index b21b1cb..4708dcc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.relocation.findBringIntoViewParent
import androidx.compose.foundation.relocation.scrollIntoView
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
@@ -41,6 +42,7 @@
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.node.invalidateSemantics
import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireLayoutCoordinates
import androidx.compose.ui.platform.InspectableModifier
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
@@ -198,7 +200,11 @@
if (isFocused == wasFocused) return
if (isFocused) {
onFocus?.invoke()
- coroutineScope.launch { scrollIntoView() }
+ val parent = findBringIntoViewParent()
+ if (parent != null) {
+ val layoutCoordinates = requireLayoutCoordinates()
+ coroutineScope.launch { parent.scrollIntoView(layoutCoordinates) }
+ }
val pinnableContainer = retrievePinnableContainer()
pinnedHandle = pinnableContainer?.pin()
notifyObserverWhenAttached()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index 3a8a68a..0ebc2d1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -90,7 +90,102 @@
* @param state The associated [AnchoredDraggableState].
* @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom drag
* will behave like bottom to top, and a left to right drag will behave like right to left. If not
- * specified, this will be determined based on [orientation] and [LocalLayoutDirection].
+ * specified, this will be determined based on [orientation] and [LocalLayoutDirection] through
+ * the other [anchoredDraggable] overload.
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ * [Modifier.draggable].
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
+ * default (if passing in null), this will snap to the closest anchor considering the velocity
+ * thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+fun <T> Modifier.anchoredDraggable(
+ state: AnchoredDraggableState<T>,
+ reverseDirection: Boolean,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource? = null,
+ startDragImmediately: Boolean = state.isAnimationRunning,
+ flingBehavior: FlingBehavior? = null
+): Modifier =
+ this then
+ AnchoredDraggableElement(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ overscrollEffect = null,
+ startDragImmediately = startDragImmediately,
+ flingBehavior = flingBehavior
+ )
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. If the [orientation] is set to [Orientation.Horizontal] and [LocalLayoutDirection]'s value
+ * is [LayoutDirection.Rtl], the drag deltas will be reversed. You should use this offset to move
+ * your content accordingly (see [Modifier.offset]). When the drag ends, the offset will be animated
+ * to one of the anchors and when that anchor is reached, the value of the [AnchoredDraggableState]
+ * will also be updated to the value corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ * [Modifier.draggable].
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
+ * default (if passing in null), this will snap to the closest anchor considering the velocity
+ * thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+fun <T> Modifier.anchoredDraggable(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource? = null,
+ startDragImmediately: Boolean = state.isAnimationRunning,
+ flingBehavior: FlingBehavior? = null
+): Modifier =
+ this then
+ AnchoredDraggableElement(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = null,
+ interactionSource = interactionSource,
+ overscrollEffect = null,
+ startDragImmediately = startDragImmediately,
+ flingBehavior = flingBehavior
+ )
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. You should use this offset to move your content accordingly (see [Modifier.offset]). When
+ * the drag ends, the offset will be animated to one of the anchors and when that anchor is reached,
+ * the value of the [AnchoredDraggableState] will also be updated to the value corresponding to the
+ * new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom drag
+ * will behave like bottom to top, and a left to right drag will behave like right to left. If not
+ * specified, this will be determined based on [orientation] and [LocalLayoutDirection] through
+ * the other [anchoredDraggable] overload.
* @param orientation The orientation in which the [anchoredDraggable] can be dragged.
* @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
@@ -105,6 +200,8 @@
* @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
* default (if passing in null), this will snap to the closest anchor considering the velocity
* thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ *
+ * This API is experimental as it uses [OverscrollEffect], which is experimental.
*/
@ExperimentalFoundationApi
fun <T> Modifier.anchoredDraggable(
@@ -156,6 +253,8 @@
* @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
* default (if passing in null), this will snap to the closest anchor considering the velocity
* thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ *
+ * This API is experimental as it uses [OverscrollEffect], which is experimental.
*/
@ExperimentalFoundationApi
fun <T> Modifier.anchoredDraggable(
@@ -543,7 +642,6 @@
* @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
* access to this scope.
*/
-@ExperimentalFoundationApi
interface AnchoredDragScope {
/**
* Assign a new value for an offset value for [AnchoredDraggableState].
@@ -646,8 +744,8 @@
* @param initialValue The initial value of the state.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
*/
+@OptIn(ExperimentalFoundationApi::class) // DraggableAnchors is still experimental
@Stable
-@ExperimentalFoundationApi
class AnchoredDraggableState<T>(
initialValue: T,
internal val confirmValueChange: (newValue: T) -> Boolean = { true }
@@ -661,7 +759,6 @@
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state
* change.
*/
- @ExperimentalFoundationApi
constructor(
initialValue: T,
anchors: DraggableAnchors<T>,
@@ -1086,7 +1183,6 @@
companion object {
/** The default [Saver] implementation for [AnchoredDraggableState]. */
- @ExperimentalFoundationApi
fun <T : Any> Saver(confirmValueChange: (T) -> Boolean = { true }) =
Saver<AnchoredDraggableState<T>, T>(
save = { it.currentValue },
@@ -1134,7 +1230,7 @@
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*/
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
suspend fun <T> AnchoredDraggableState<T>.snapTo(targetValue: T) {
anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
val targetOffset = anchors.positionOf(latestTarget)
@@ -1177,7 +1273,7 @@
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*/
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
suspend fun <T> AnchoredDraggableState<T>.animateTo(
targetValue: T,
animationSpec: AnimationSpec<Float> =
@@ -1209,7 +1305,7 @@
* @throws CancellationException if the interaction interrupted bt another interaction like a
* gesture interaction or another programmatic interaction like [animateTo] or [snapTo] call.
*/
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
suspend fun <T> AnchoredDraggableState<T>.animateToWithDecay(
targetValue: T,
velocity: Float,
@@ -1326,7 +1422,6 @@
/**
* Contains useful defaults for use with [AnchoredDraggableState] and [Modifier.anchoredDraggable]
*/
-@ExperimentalFoundationApi
object AnchoredDraggableDefaults {
/** The default spec for snapping, a tween spec */
@@ -1511,7 +1606,6 @@
* is invoked with.
* @param snapAnimationSpec The animation spec that will be used to snap to a new state.
*/
-@ExperimentalFoundationApi
internal fun <T> anchoredDraggableFlingBehavior(
state: AnchoredDraggableState<T>,
density: Density,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
index 14efa18..dd43101 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
@@ -21,6 +21,7 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.requireLayoutCoordinates
import androidx.compose.ui.unit.toSize
@@ -44,7 +45,14 @@
if (!node.isAttached) return
val layoutCoordinates = requireLayoutCoordinates()
val parent = findBringIntoViewParent() ?: return
- parent.bringChildIntoView(layoutCoordinates) {
+ parent.scrollIntoView(layoutCoordinates, rect)
+}
+
+internal suspend fun BringIntoViewParent.scrollIntoView(
+ layoutCoordinates: LayoutCoordinates,
+ rect: Rect? = null
+) {
+ bringChildIntoView(layoutCoordinates) {
// If the rect is not specified, use a rectangle representing the entire composable.
// If the coordinates are detached when this call is made, we don't bother even
// submitting the request, but if the coordinates become detached while the request
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
index df3c97a..8a649d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
@@ -145,7 +145,15 @@
constraints = constraints,
)
- val placeable = measurable.measure(Constraints.fixed(result.size.width, result.size.height))
+ val placeable =
+ measurable.measure(
+ Constraints.fitPrioritizingWidth(
+ minWidth = result.size.width,
+ maxWidth = result.size.width,
+ minHeight = result.size.height,
+ maxHeight = result.size.height
+ )
+ )
// calculate the min height for single line text to prevent text cuts.
// for single line text maxLines puts in max height constraint based on
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index 1873ed3..3f475b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -27,7 +27,7 @@
overflow: TextOverflow,
maxIntrinsicWidth: Float
): Constraints =
- Constraints(
+ Constraints.fitPrioritizingWidth(
minWidth = 0,
maxWidth = finalMaxWidth(constraints, softWrap, overflow, maxIntrinsicWidth),
minHeight = 0,
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
index 9f707e7..da2078f 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -18,7 +18,7 @@
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
@@ -77,7 +77,7 @@
LayeredComposeTestCase(), ToggleableTestCase {
private lateinit var state: MutableFloatState
- @OptIn(ExperimentalMaterial3Api::class)
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
state = remember { mutableFloatStateOf(0f) }
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index efb3d49..2bc212b 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1188,7 +1188,7 @@
property public final long unselectedTextColor;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
field public static final androidx.compose.material3.NavigationItemIconPosition.Companion Companion;
}
@@ -1576,7 +1576,7 @@
enum_constant public static final androidx.compose.material3.SheetValue PartiallyExpanded;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarDefaults {
method public int getArrangement();
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
@@ -1588,14 +1588,14 @@
field public static final androidx.compose.material3.ShortNavigationBarDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarItemDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
}
public final class ShortNavigationBarKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2313,41 +2313,47 @@
property public final androidx.compose.ui.text.TextStyle titleSmall;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class WavyProgressIndicatorDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
method public float getCircularContainerSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularIndicatorStroke();
+ method public float getCircularIndicatorTrackGapSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularTrackStroke();
method public float getCircularWavelength();
method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getIndicatorAmplitude();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getIndicatorStroke();
- method public float getIndicatorTrackGapSize();
method public float getLinearContainerHeight();
method public float getLinearContainerWidth();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearIndicatorStroke();
+ method public float getLinearIndicatorTrackGapSize();
method public float getLinearTrackStopIndicatorSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearTrackStroke();
method public float getLinearWavelength();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getProgressAnimationSpec();
method @androidx.compose.runtime.Composable public long getTrackColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getTrackStroke();
property public final float CircularContainerSize;
+ property public final float CircularIndicatorTrackGapSize;
property public final float CircularWavelength;
- property public final float IndicatorTrackGapSize;
property public final float LinearContainerHeight;
property public final float LinearContainerWidth;
+ property public final float LinearIndicatorTrackGapSize;
property public final float LinearTrackStopIndicatorSize;
property public final float LinearWavelength;
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> ProgressAnimationSpec;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularIndicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularTrackStroke;
property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> indicatorAmplitude;
property @androidx.compose.runtime.Composable public final long indicatorColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke indicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearIndicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearTrackStroke;
property @androidx.compose.runtime.Composable public final long trackColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke trackStroke;
field public static final androidx.compose.material3.WavyProgressIndicatorDefaults INSTANCE;
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
}
@@ -2413,6 +2419,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
method @androidx.compose.runtime.Composable public void Indicator(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long color, optional float threshold);
+ method @androidx.compose.runtime.Composable public void IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getElevation();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
@@ -2430,7 +2437,6 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PullToRefreshBox(boolean isRefreshing, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.pulltorefresh.PullToRefreshState PullToRefreshState();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefresh(androidx.compose.ui.Modifier, boolean isRefreshing, androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional boolean enabled, optional float threshold, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefreshIndicator(androidx.compose.ui.Modifier, androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.pulltorefresh.PullToRefreshState rememberPullToRefreshState();
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index efb3d49..2bc212b 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1188,7 +1188,7 @@
property public final long unselectedTextColor;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
field public static final androidx.compose.material3.NavigationItemIconPosition.Companion Companion;
}
@@ -1576,7 +1576,7 @@
enum_constant public static final androidx.compose.material3.SheetValue PartiallyExpanded;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarDefaults {
method public int getArrangement();
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
@@ -1588,14 +1588,14 @@
field public static final androidx.compose.material3.ShortNavigationBarDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarItemDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
}
public final class ShortNavigationBarKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2313,41 +2313,47 @@
property public final androidx.compose.ui.text.TextStyle titleSmall;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class WavyProgressIndicatorDefaults {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
method public float getCircularContainerSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularIndicatorStroke();
+ method public float getCircularIndicatorTrackGapSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularTrackStroke();
method public float getCircularWavelength();
method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getIndicatorAmplitude();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getIndicatorStroke();
- method public float getIndicatorTrackGapSize();
method public float getLinearContainerHeight();
method public float getLinearContainerWidth();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearIndicatorStroke();
+ method public float getLinearIndicatorTrackGapSize();
method public float getLinearTrackStopIndicatorSize();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearTrackStroke();
method public float getLinearWavelength();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getProgressAnimationSpec();
method @androidx.compose.runtime.Composable public long getTrackColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getTrackStroke();
property public final float CircularContainerSize;
+ property public final float CircularIndicatorTrackGapSize;
property public final float CircularWavelength;
- property public final float IndicatorTrackGapSize;
property public final float LinearContainerHeight;
property public final float LinearContainerWidth;
+ property public final float LinearIndicatorTrackGapSize;
property public final float LinearTrackStopIndicatorSize;
property public final float LinearWavelength;
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> ProgressAnimationSpec;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularIndicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularTrackStroke;
property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> indicatorAmplitude;
property @androidx.compose.runtime.Composable public final long indicatorColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke indicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearIndicatorStroke;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearTrackStroke;
property @androidx.compose.runtime.Composable public final long trackColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke trackStroke;
field public static final androidx.compose.material3.WavyProgressIndicatorDefaults INSTANCE;
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
}
@@ -2413,6 +2419,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
method @androidx.compose.runtime.Composable public void Indicator(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long color, optional float threshold);
+ method @androidx.compose.runtime.Composable public void IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getElevation();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
@@ -2430,7 +2437,6 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PullToRefreshBox(boolean isRefreshing, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.pulltorefresh.PullToRefreshState PullToRefreshState();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefresh(androidx.compose.ui.Modifier, boolean isRefreshing, androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional boolean enabled, optional float threshold, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefreshIndicator(androidx.compose.ui.Modifier, androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.pulltorefresh.PullToRefreshState rememberPullToRefreshState();
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 008272d..6741b9e 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -124,6 +124,7 @@
import androidx.compose.material3.samples.PlainTooltipWithManualInvocationSample
import androidx.compose.material3.samples.PrimaryIconTabs
import androidx.compose.material3.samples.PrimaryTextTabs
+import androidx.compose.material3.samples.PullToRefreshCustomIndicatorWithDefaultTransform
import androidx.compose.material3.samples.PullToRefreshSample
import androidx.compose.material3.samples.PullToRefreshSampleCustomState
import androidx.compose.material3.samples.PullToRefreshScalingSample
@@ -149,6 +150,8 @@
import androidx.compose.material3.samples.SecondaryTextTabs
import androidx.compose.material3.samples.SegmentedButtonMultiSelectSample
import androidx.compose.material3.samples.SegmentedButtonSingleSelectSample
+import androidx.compose.material3.samples.ShortNavigationBarSample
+import androidx.compose.material3.samples.ShortNavigationBarWithHorizontalItemsSample
import androidx.compose.material3.samples.SimpleBottomAppBar
import androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample
import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
@@ -859,6 +862,20 @@
val NavigationBarExamples =
listOf(
Example(
+ name = ::ShortNavigationBarSample.name,
+ description = NavigationBarExampleDescription,
+ sourceUrl = NavigationBarExampleSourceUrl,
+ ) {
+ ShortNavigationBarSample()
+ },
+ Example(
+ name = ::ShortNavigationBarWithHorizontalItemsSample.name,
+ description = NavigationBarExampleDescription,
+ sourceUrl = NavigationBarExampleSourceUrl,
+ ) {
+ ShortNavigationBarWithHorizontalItemsSample()
+ },
+ Example(
name = ::NavigationBarSample.name,
description = NavigationBarExampleDescription,
sourceUrl = NavigationBarExampleSourceUrl,
@@ -1051,6 +1068,13 @@
) {
PullToRefreshViewModelSample()
},
+ Example(
+ name = ::PullToRefreshViewModelSample.name,
+ description = PullToRefreshExampleDescription,
+ sourceUrl = PullToRefreshExampleSourceUrl
+ ) {
+ PullToRefreshCustomIndicatorWithDefaultTransform()
+ },
)
private const val RadioButtonsExampleDescription = "Radio buttons examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
index 36502a5..2c76d18 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
@@ -17,18 +17,79 @@
package androidx.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarArrangement
import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationItemIconPosition
+import androidx.compose.material3.ShortNavigationBar
+import androidx.compose.material3.ShortNavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ShortNavigationBarSample() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ val items = listOf("Songs", "Artists", "Playlists")
+
+ ShortNavigationBar {
+ items.forEachIndexed { index, item ->
+ ShortNavigationBarItem(
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = item) },
+ label = { Text(item) },
+ selected = selectedItem == index,
+ onClick = { selectedItem = index }
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ShortNavigationBarWithHorizontalItemsSample() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ val items = listOf("Songs", "Artists", "Playlists")
+
+ Column {
+ Text(
+ "Note: this is configuration is better displayed in medium screen sizes.",
+ Modifier.padding(16.dp)
+ )
+
+ Spacer(Modifier.height(32.dp))
+
+ ShortNavigationBar(arrangement = NavigationBarArrangement.Centered) {
+ items.forEachIndexed { index, item ->
+ ShortNavigationBarItem(
+ iconPosition = NavigationItemIconPosition.Start,
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = item) },
+ label = { Text(item) },
+ selected = selectedItem == index,
+ onClick = { selectedItem = index }
+ )
+ }
+ }
+ }
+}
@Preview
@Sampled
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
index a1ae0e2..1891882 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
@@ -26,7 +26,7 @@
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
@@ -73,7 +73,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -100,7 +100,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -143,7 +143,7 @@
Column(horizontalAlignment = Alignment.CenterHorizontally) { LinearProgressIndicator() }
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -175,7 +175,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -200,7 +200,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -241,7 +241,7 @@
Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator() }
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
index 7e7f9dc..ea0ea7c 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -352,3 +353,59 @@
}
}
}
+
+@Sampled
+@Composable
+@Preview
+@OptIn(ExperimentalMaterial3Api::class)
+fun PullToRefreshCustomIndicatorWithDefaultTransform() {
+ var itemCount by remember { mutableIntStateOf(15) }
+ var isRefreshing by remember { mutableStateOf(false) }
+ val state = rememberPullToRefreshState()
+ val coroutineScope = rememberCoroutineScope()
+ val onRefresh: () -> Unit = {
+ isRefreshing = true
+ coroutineScope.launch {
+ delay(1500)
+ itemCount += 5
+ isRefreshing = false
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Title") },
+ // Provide an accessible alternative to trigger refresh.
+ actions = {
+ IconButton(onClick = onRefresh) {
+ Icon(Icons.Filled.Refresh, "Trigger Refresh")
+ }
+ }
+ )
+ }
+ ) {
+ PullToRefreshBox(
+ modifier = Modifier.padding(it),
+ state = state,
+ isRefreshing = isRefreshing,
+ onRefresh = onRefresh,
+ indicator = {
+ PullToRefreshDefaults.IndicatorBox(state = state, isRefreshing = isRefreshing) {
+ if (isRefreshing) {
+ CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
+ } else {
+ CircularProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ progress = { state.distanceFraction }
+ )
+ }
+ }
+ }
+ ) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }
+ }
+ }
+ }
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
index 9c1d2d1..bf33073 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
@@ -53,7 +53,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -433,7 +433,7 @@
* @param modifier the [Modifier] applied to the navigation bar
* @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun DefaultShortNavigationBar(
interactionSource: MutableInteractionSource,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
index 48fc29d..e02d719 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
@@ -66,7 +66,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@LargeTest
@RunWith(AndroidJUnit4::class)
class ShortNavigationBarTest {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
index 9d444f0..5852853 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
@@ -44,7 +44,7 @@
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@LargeTest
@RunWith(Parameterized::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
index 865bc65..a8760b3 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
@@ -48,7 +48,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@LargeTest
@RunWith(AndroidJUnit4::class)
class WavyProgressIndicatorTest {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 26ce3b8..8933e20 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -74,7 +74,7 @@
/** Class that describes the different supported icon positions of the navigation item. */
@JvmInline
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
value class NavigationItemIconPosition private constructor(private val value: Int) {
companion object {
/* The icon is positioned on top of the label. */
@@ -226,7 +226,7 @@
* for this item. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this item in different states
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun NavigationItem(
selected: Boolean,
@@ -335,7 +335,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun NavigationItemLayout(
interactionSource: InteractionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index abe68c7..7fead98 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -33,6 +33,8 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.material3.ProgressIndicatorDefaults.drawStopIndicator
+import androidx.compose.material3.tokens.CircularProgressIndicatorTokens
+import androidx.compose.material3.tokens.LinearProgressIndicatorTokens
import androidx.compose.material3.tokens.ProgressIndicatorTokens
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -863,7 +865,7 @@
@Composable get() = Color.Transparent
/** Default stroke width for a circular progress indicator. */
- val CircularStrokeWidth: Dp = ProgressIndicatorTokens.TrackThickness
+ val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.TrackThickness
/** Default stroke cap for a linear progress indicator. */
val LinearStrokeCap: StrokeCap = StrokeCap.Round
@@ -878,19 +880,19 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val LinearTrackStopIndicatorSize: Dp = ProgressIndicatorTokens.StopSize
+ val LinearTrackStopIndicatorSize: Dp = LinearProgressIndicatorTokens.StopSize
/** Default indicator track gap size for a linear progress indicator. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val LinearIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
+ val LinearIndicatorTrackGapSize: Dp = LinearProgressIndicatorTokens.TrackActiveSpace
/** Default indicator track gap size for a circular progress indicator. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
- val CircularIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
+ val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
/**
* The default [AnimationSpec] that should be used when animating between progress in a
@@ -955,13 +957,12 @@
internal val LinearIndicatorWidth = 240.dp
/*@VisibleForTesting*/
-internal val LinearIndicatorHeight = ProgressIndicatorTokens.TrackThickness
+internal val LinearIndicatorHeight = LinearProgressIndicatorTokens.Height
// CircularProgressIndicator Material specs
// Diameter of the indicator circle
/*@VisibleForTesting*/
-internal val CircularIndicatorDiameter =
- ProgressIndicatorTokens.Size - ProgressIndicatorTokens.TrackThickness * 2
+internal val CircularIndicatorDiameter = CircularProgressIndicatorTokens.Size
// Indeterminate linear indicator transition specs
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
index 2503b0c..e0fd8eb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
@@ -63,6 +63,14 @@
* [NavigationBarArrangement.Centered], so that the navigation items are distributed grouped on
* the center of the bar.
*
+ * A simple example of the first configuration looks like this:
+ *
+ * @sample androidx.compose.material3.samples.ShortNavigationBarSample
+ *
+ * And of the second configuration:
+ *
+ * @sample androidx.compose.material3.samples.ShortNavigationBarWithHorizontalItemsSample
+ *
* See [ShortNavigationBarItem] for configurations specific to each item, and not the overall
* [ShortNavigationBar] component.
*
@@ -74,7 +82,7 @@
* @param arrangement the [NavigationBarArrangement] of this navigation bar
* @param content the content of this navigation bar, typically [ShortNavigationBarItem]s
*/
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun ShortNavigationBar(
modifier: Modifier = Modifier,
@@ -173,7 +181,7 @@
* preview the item in different states. Note that if `null` is provided, interactions will still
* happen internally.
*/
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun ShortNavigationBarItem(
selected: Boolean,
@@ -227,7 +235,7 @@
}
/** Defaults used in [ShortNavigationBar]. */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
object ShortNavigationBarDefaults {
/** Default container color for a short navigation bar. */
// TODO: Replace with token.
@@ -253,7 +261,7 @@
}
/** Defaults used in [ShortNavigationBarItem]. */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
object ShortNavigationBarItemDefaults {
/**
* Creates a [NavigationItemColors] with the provided colors according to the Material
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
index 73b8f6c..1f35152 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
@@ -29,9 +29,12 @@
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.material3.internal.toPath
+import androidx.compose.material3.tokens.CircularProgressIndicatorTokens
+import androidx.compose.material3.tokens.LinearProgressIndicatorTokens
import androidx.compose.material3.tokens.MotionTokens
import androidx.compose.material3.tokens.ProgressIndicatorTokens
import androidx.compose.runtime.Composable
@@ -123,17 +126,16 @@
*
* @sample androidx.compose.material3.samples.LinearThickWavyProgressIndicatorSample
*/
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun LinearWavyProgressIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
color: Color = WavyProgressIndicatorDefaults.indicatorColor,
trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
- stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
- trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
- gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+ stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
+ trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
+ gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
stopSize: Dp = WavyProgressIndicatorDefaults.LinearTrackStopIndicatorSize,
amplitude: (progress: Float) -> Float = WavyProgressIndicatorDefaults.indicatorAmplitude,
wavelength: Dp = WavyProgressIndicatorDefaults.LinearWavelength,
@@ -182,6 +184,7 @@
.semantics(mergeDescendants = true) {
progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f)
}
+ .requiredSizeIn(minWidth = LinearContainerMinWidth)
.size(
width = WavyProgressIndicatorDefaults.LinearContainerWidth,
height = WavyProgressIndicatorDefaults.LinearContainerHeight
@@ -282,16 +285,15 @@
* @param wavelength the length of a wave
* @sample androidx.compose.material3.samples.IndeterminateLinearWavyProgressIndicatorSample
*/
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun LinearWavyProgressIndicator(
modifier: Modifier = Modifier,
color: Color = WavyProgressIndicatorDefaults.indicatorColor,
trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
- stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
- trackStroke: Stroke = stroke,
- gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+ stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
+ trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
+ gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
wavelength: Dp = WavyProgressIndicatorDefaults.LinearWavelength
) {
val infiniteTransition = rememberInfiniteTransition()
@@ -371,6 +373,7 @@
modifier
.then(IncreaseSemanticsBounds)
.progressSemantics()
+ .requiredSizeIn(minWidth = LinearContainerMinWidth)
.size(
WavyProgressIndicatorDefaults.LinearContainerWidth,
WavyProgressIndicatorDefaults.LinearContainerHeight
@@ -450,17 +453,16 @@
*
* @sample androidx.compose.material3.samples.CircularThickWavyProgressIndicatorSample
*/
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun CircularWavyProgressIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
color: Color = WavyProgressIndicatorDefaults.indicatorColor,
trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
- stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
- trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
- gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+ stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
+ trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
+ gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
amplitude: (progress: Float) -> Float = WavyProgressIndicatorDefaults.indicatorAmplitude,
wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength,
waveSpeed: Dp = wavelength // Match to 1 wavelength per second
@@ -567,16 +569,15 @@
* wavelength may be different to ensure a continuous wave shape.
* @sample androidx.compose.material3.samples.IndeterminateCircularWavyProgressIndicatorSample
*/
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
@Composable
fun CircularWavyProgressIndicator(
modifier: Modifier = Modifier,
color: Color = WavyProgressIndicatorDefaults.indicatorColor,
trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
- stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
- trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
- gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+ stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
+ trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
+ gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength
) {
val circularShapes = remember { CircularShapes() }
@@ -920,8 +921,7 @@
}
/** Contains the default values used for wavy progress indicators */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
object WavyProgressIndicatorDefaults {
/**
@@ -942,50 +942,83 @@
val trackColor: Color
@Composable get() = ProgressIndicatorTokens.TrackColor.value
- /** A default active indicator [Stroke]. */
- val indicatorStroke: Stroke
+ /** A default linear progress indicator active indicator [Stroke]. */
+ val linearIndicatorStroke: Stroke
@Composable
get() =
Stroke(
width =
with(LocalDensity.current) {
- 4.dp.toPx() // TODO: Link to a token value for a thin PI
+ LinearProgressIndicatorTokens.ActiveThickness.toPx()
},
cap = StrokeCap.Round
)
- /** A default track [Stroke]. */
- val trackStroke: Stroke
+ /** A default circular progress indicator active indicator [Stroke]. */
+ val circularIndicatorStroke: Stroke
@Composable
get() =
Stroke(
width =
with(LocalDensity.current) {
- 4.dp.toPx() // TODO: Link to a token value for a thin PI
+ CircularProgressIndicatorTokens.ActiveThickness.toPx()
+ },
+ cap = StrokeCap.Round
+ )
+
+ /** A default linear progress indicator track [Stroke]. */
+ val linearTrackStroke: Stroke
+ @Composable
+ get() =
+ Stroke(
+ width =
+ with(LocalDensity.current) {
+ LinearProgressIndicatorTokens.TrackThickness.toPx()
+ },
+ cap = StrokeCap.Round
+ )
+
+ /** A default circular progress indicator track [Stroke]. */
+ val circularTrackStroke: Stroke
+ @Composable
+ get() =
+ Stroke(
+ width =
+ with(LocalDensity.current) {
+ CircularProgressIndicatorTokens.TrackThickness.toPx()
},
cap = StrokeCap.Round
)
/** A default wavelength of a linear progress indicator when it's in a wavy form. */
- val LinearWavelength: Dp = 40.dp // TODO: Link to a token value
+ val LinearWavelength: Dp = LinearProgressIndicatorTokens.ActiveWaveWavelength
/** A default linear progress indicator container height. */
- val LinearContainerHeight: Dp = 10.dp // TODO: Link to a token value
+ val LinearContainerHeight: Dp = LinearProgressIndicatorTokens.WaveHeight
/** A default linear progress indicator container width. */
- val LinearContainerWidth: Dp = 240.dp // TODO: Link to a token value
+ val LinearContainerWidth: Dp = 240.dp
/** A default linear stop indicator size. */
- val LinearTrackStopIndicatorSize: Dp = 4.dp // TODO: Link to a token value
+ val LinearTrackStopIndicatorSize: Dp = LinearProgressIndicatorTokens.StopSize
/** A default circular progress indicator container size. */
- val CircularContainerSize: Dp = 48.dp // TODO: Link to a token value
+ val CircularContainerSize: Dp = CircularProgressIndicatorTokens.WaveSize
/** A default wavelength of a circular progress indicator when it's in a wavy form. */
- val CircularWavelength: Dp = 15.dp // TODO: Link to a token value
+ val CircularWavelength: Dp = CircularProgressIndicatorTokens.ActiveWaveWavelength
- /** A default gap size that appears in between the active indicator and the track. */
- val IndicatorTrackGapSize: Dp = 4.dp // TODO: Link to a token value
+ /**
+ * A default gap size that appears in between the active indicator and the track at the linear
+ * progress indicator.
+ */
+ val LinearIndicatorTrackGapSize: Dp = LinearProgressIndicatorTokens.TrackActiveSpace
+
+ /**
+ * A default gap size that appears in between the active indicator and the track at the circular
+ * progress indicator.
+ */
+ val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
/** A function that returns the indicator's amplitude for a given progress */
val indicatorAmplitude: (progress: Float) -> Float = { progress ->
@@ -1844,6 +1877,10 @@
}
}
+// Set the linear indicator min width to the smallest circular indicator value at the tokens. Small
+// linear indicators should be substituted with circular ones.
+private val LinearContainerMinWidth = CircularProgressIndicatorTokens.Size
+
// Total duration for one linear cycle
private const val LinearAnimationDuration = 1750
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index 1ec27e6..65d22ea 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -104,6 +104,10 @@
* Scaling behavior can be implemented like this
*
* @sample androidx.compose.material3.samples.PullToRefreshScalingSample
+ *
+ * Custom indicators with default transforms can be seen in
+ *
+ * @sample androidx.compose.material3.samples.PullToRefreshCustomIndicatorWithDefaultTransform
* @param isRefreshing whether a refresh is occurring
* @param onRefresh callback invoked when the user gesture crosses the threshold, thereby requesting
* a refresh.
@@ -142,49 +146,6 @@
}
/**
- * A Modifier that handles the size, offset, clipping, shadow, and background drawing of a
- * pull-to-refresh indicator, useful when implementing custom indicators.
- * [PullToRefreshDefaults.Indicator] applies this automatically.
- *
- * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
- * calculate the offset
- * @param isRefreshing whether a refresh is occurring
- * @param threshold how much the indicator can be pulled down before a refresh is triggered on
- * release
- * @param shape the [Shape] of this indicator
- * @param containerColor the container color of this indicator
- * @param elevation the elevation for the indicator
- */
-@ExperimentalMaterial3Api
-fun Modifier.pullToRefreshIndicator(
- state: PullToRefreshState,
- isRefreshing: Boolean,
- threshold: Dp = PullToRefreshDefaults.PositionalThreshold,
- shape: Shape = PullToRefreshDefaults.shape,
- containerColor: Color = Color.Unspecified,
- elevation: Dp = PullToRefreshDefaults.Elevation,
-): Modifier =
- this.size(SpinnerContainerSize)
- .drawWithContent {
- clipRect(
- top = 0f,
- left = -Float.MAX_VALUE,
- right = Float.MAX_VALUE,
- bottom = Float.MAX_VALUE
- ) {
- [email protected]()
- }
- }
- .graphicsLayer {
- val showElevation = state.distanceFraction > 0f || isRefreshing
- translationY = state.distanceFraction * threshold.roundToPx() - size.height
- shadowElevation = if (showElevation) elevation.toPx() else 0f
- this.shape = shape
- clip = true
- }
- .background(color = containerColor, shape = shape)
-
-/**
* A Modifier that adds nested scroll to a container to support a pull-to-refresh gesture. When the
* user pulls a distance greater than [threshold] and releases the gesture, [onRefresh] is invoked.
* [PullToRefreshBox] applies this automatically.
@@ -281,13 +242,7 @@
override fun onAttach() {
delegate(nestedScrollNode)
- coroutineScope.launch {
- if (isRefreshing) {
- state.snapTo(1f)
- } else {
- state.snapTo(0f)
- }
- }
+ coroutineScope.launch { state.snapTo(if (isRefreshing) 1f else 0f) }
}
override fun onPreScroll(
@@ -425,28 +380,90 @@
/** The default refresh threshold for [rememberPullToRefreshState] */
val PositionalThreshold = 80.dp
- /** The default elevation for [pullToRefreshIndicator] */
+ /** The default elevation for [IndicatorBox] */
val Elevation = ElevationTokens.Level2
- /** The default indicator for [PullToRefreshBox]. */
+ /**
+ * A Wrapper that handles the size, offset, clipping, shadow, and background drawing for a
+ * pull-to-refresh indicator, useful when implementing custom indicators.
+ * [PullToRefreshDefaults.Indicator] uses this as the container.
+ *
+ * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
+ * calculate the offset
+ * @param isRefreshing whether a refresh is occurring
+ * @param modifier the modifier applied to this layout
+ * @param threshold how much the indicator can be pulled down before a refresh is triggered on
+ * release
+ * @param shape the [Shape] of this indicator
+ * @param containerColor the container color of this indicator
+ * @param elevation the elevation for the indicator
+ * @param content content for this [IndicatorBox]
+ */
+ @Composable
+ fun IndicatorBox(
+ state: PullToRefreshState,
+ isRefreshing: Boolean,
+ modifier: Modifier = Modifier,
+ threshold: Dp = PositionalThreshold,
+ shape: Shape = PullToRefreshDefaults.shape,
+ containerColor: Color = Color.Unspecified,
+ elevation: Dp = Elevation,
+ content: @Composable BoxScope.() -> Unit
+ ) {
+ Box(
+ modifier =
+ modifier
+ .size(SpinnerContainerSize)
+ .drawWithContent {
+ clipRect(
+ top = 0f,
+ left = -Float.MAX_VALUE,
+ right = Float.MAX_VALUE,
+ bottom = Float.MAX_VALUE
+ ) {
+ [email protected]()
+ }
+ }
+ .graphicsLayer {
+ val showElevation = state.distanceFraction > 0f || isRefreshing
+ translationY = state.distanceFraction * threshold.roundToPx() - size.height
+ shadowElevation = if (showElevation) elevation.toPx() else 0f
+ this.shape = shape
+ clip = true
+ }
+ .background(color = containerColor, shape = shape),
+ contentAlignment = Alignment.Center,
+ content = content
+ )
+ }
+
+ /**
+ * The default indicator for [PullToRefreshBox].
+ *
+ * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
+ * calculate the offset
+ * @param isRefreshing whether a refresh is occurring
+ * @param modifier the modifier applied to this layout
+ * @param containerColor the container color of this indicator
+ * @param color the color of this indicator
+ * @param threshold how much the indicator can be pulled down before a refresh is triggered on
+ * release
+ */
@Composable
fun Indicator(
state: PullToRefreshState,
isRefreshing: Boolean,
modifier: Modifier = Modifier,
- containerColor: Color = PullToRefreshDefaults.containerColor,
- color: Color = PullToRefreshDefaults.indicatorColor,
+ containerColor: Color = this.containerColor,
+ color: Color = this.indicatorColor,
threshold: Dp = PositionalThreshold,
) {
- Box(
- modifier =
- modifier.pullToRefreshIndicator(
- state = state,
- isRefreshing = isRefreshing,
- containerColor = containerColor,
- threshold = threshold,
- ),
- contentAlignment = Alignment.Center
+ IndicatorBox(
+ modifier = modifier,
+ state = state,
+ isRefreshing = isRefreshing,
+ containerColor = containerColor,
+ threshold = threshold,
) {
Crossfade(
targetState = isRefreshing,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
new file mode 100644
index 0000000..9ed1149
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_4_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object CircularProgressIndicatorTokens {
+ val ActiveThickness = 4.0.dp
+ val ActiveWaveAmplitude = 2.0.dp
+ val ActiveWaveWavelength = 15.0.dp
+ val Size = 40.0.dp
+ val TrackActiveSpace = 4.0.dp
+ val TrackThickness = 4.0.dp
+ val WaveSize = 48.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
new file mode 100644
index 0000000..d0838d3
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+// VERSION: v0_4_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object LinearProgressIndicatorTokens {
+ val ActiveThickness = 4.0.dp
+ val ActiveWaveAmplitude = 3.0.dp
+ val ActiveWaveWavelength = 40.0.dp
+ val Height = 4.0.dp
+ val StopSize = 4.0.dp
+ val StopTrailingSpace = 0.0.dp
+ val TrackActiveSpace = 4.0.dp
+ val TrackThickness = 4.0.dp
+ val WaveHeight = 10.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
index ade192d..eea7701 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
@@ -13,23 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// Version: v2_3_5
+// VERSION: v0_4_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
-import androidx.compose.ui.unit.dp
-
internal object ProgressIndicatorTokens {
val ActiveIndicatorColor = ColorSchemeKeyTokens.Primary
val ActiveShape = ShapeKeyTokens.CornerFull
- val ActiveThickness = 4.0.dp
- val ActiveTrackSpace = 4.0.dp
val StopColor = ColorSchemeKeyTokens.Primary
- val StopShape = 4.0.dp
- val StopSize = 4.0.dp
+ val StopShape = ShapeKeyTokens.CornerFull
val TrackColor = ColorSchemeKeyTokens.SecondaryContainer
val TrackShape = ShapeKeyTokens.CornerFull
- val TrackThickness = 4.0.dp
- val Size = 48.0.dp
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index aa33218..180831d 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -367,6 +367,12 @@
.isEqualTo(TextDecoration.LineThrough)
}
+ @Test
+ fun emptyConstraints_hugeString_dontCrash() {
+ val subject = textMeasurer()
+ subject.measure("A".repeat(100_000), TextStyle.Default)
+ }
+
private fun textLayoutInput(
text: AnnotatedString = AnnotatedString("Hello"),
style: TextStyle = TextStyle.Default,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index b9e30ed..47d2651 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -325,7 +325,12 @@
MultiParagraph(
intrinsics = nonNullIntrinsics,
constraints =
- Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
+ Constraints.fitPrioritizingWidth(
+ minWidth = 0,
+ maxWidth = width,
+ minHeight = 0,
+ maxHeight = constraints.maxHeight
+ ),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
ellipsis = overflow == TextOverflow.Ellipsis
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index b2fe7f6..9529deb 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1958,7 +1958,7 @@
method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
- method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
+ method public static void writeBoolean(android.os.Parcel, boolean);
}
@Deprecated public final class ParcelableCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 6d8746a..c280485 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2348,7 +2348,7 @@
method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
- method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
+ method public static void writeBoolean(android.os.Parcel, boolean);
}
@Deprecated public final class ParcelableCompat {
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index fb67665..52de196 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -50,15 +50,13 @@
/**
* Write a boolean value into the parcel at the current f{@link Parcel#dataPosition()},
* growing {@link Parcel#dataCapacity()} if needed.
- *
- * <p>Note: This method currently delegates to {@link Parcel#writeInt} with a value of 1 or 0
- * for true or false, respectively, but may change in the future.
- * @deprecated Call {@link Parcel#writeInt()} directly.
*/
- @Deprecated
- @androidx.annotation.ReplaceWith(expression = "out.writeInt(value ? 1 : 0)")
public static void writeBoolean(@NonNull Parcel out, boolean value) {
- out.writeInt(value ? 1 : 0);
+ if (Build.VERSION.SDK_INT >= 29) {
+ Api29Impl.writeBoolean(out, value);
+ } else {
+ out.writeInt(value ? 1 : 0);
+ }
}
/**
@@ -406,6 +404,11 @@
@NonNull List<T> list, @Nullable ClassLoader cl) {
return in.readParcelableList(list, cl);
}
+
+ @DoNotInline
+ static void writeBoolean(@NonNull Parcel parcel, boolean val) {
+ parcel.writeBoolean(val);
+ }
}
@RequiresApi(30)
diff --git a/core/core/src/main/res/values-pa/strings.xml b/core/core/src/main/res/values-pa/strings.xml
index c24092c..6099d51 100644
--- a/core/core/src/main/res/values-pa/strings.xml
+++ b/core/core/src/main/res/values-pa/strings.xml
@@ -20,7 +20,7 @@
<string name="status_bar_notification_info_overflow" msgid="6277540029070332960">"999+"</string>
<string name="call_notification_answer_action" msgid="881409763997275156">"ਜਵਾਬ ਦਿਓ"</string>
<string name="call_notification_answer_video_action" msgid="8793775615905189152">"ਵੀਡੀਓ"</string>
- <string name="call_notification_decline_action" msgid="3229508546291798546">"ਅਸਵੀਕਾਰ ਕਰੋ"</string>
+ <string name="call_notification_decline_action" msgid="3229508546291798546">"ਕਾਲ ਕੱਟੋ"</string>
<string name="call_notification_hang_up_action" msgid="2659457946726154263">"ਸਮਾਪਤ ਕਰੋ"</string>
<string name="call_notification_incoming_text" msgid="6107532579223922871">"ਇਨਕਮਿੰਗ ਕਾਲ"</string>
<string name="call_notification_ongoing_text" msgid="8623827134497363134">"ਜਾਰੀ ਕਾਲ"</string>
diff --git a/libraryversions.toml b/libraryversions.toml
index d781128..a084055 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -162,10 +162,10 @@
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha02"
WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
-WEAR_PROTOLAYOUT = "1.2.0-alpha04"
+WEAR_PROTOLAYOUT = "1.2.0-alpha05"
WEAR_PROTOLAYOUT_MATERIAL3 = "1.0.0-alpha01"
WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha02"
-WEAR_TILES = "1.4.0-alpha04"
+WEAR_TILES = "1.4.0-alpha05"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
WEAR_WATCHFACE = "1.3.0-alpha03"
WEBKIT = "1.12.0-alpha02"
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
index 32055f6..ffa3358 100644
--- a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
@@ -25,16 +25,8 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
-import org.jetbrains.kotlin.analysis.api.analyze
-import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
-import org.jetbrains.kotlin.idea.references.mainReference
-import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
-import org.jetbrains.kotlin.psi.KtExpression
-import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getParameterForArgument
abstract class BaseWrongStartDestinationTypeDetector(
@@ -54,42 +46,7 @@
parameterNames.contains(node.getParameterForArgument(it)?.name)
} ?: return
- /**
- * True if:
- * 1. reference to object (i.e. val myStart = TestStart(), startDest = myStart)
- * 2. object declaration (i.e. object MyStart, startDest = MyStart)
- * 3. class reference (i.e. class MyStart, startDest = MyStart)
- *
- * We only want to catch case 3., so we need more filters to eliminate case 1 & 2.
- */
- val isSimpleRefExpression = startNode is USimpleNameReferenceExpression
-
- /** True if nested class i.e. OuterClass.InnerClass */
- val isQualifiedRefExpression = startNode is UQualifiedReferenceExpression
-
- if (!(isSimpleRefExpression || isQualifiedRefExpression)) return
-
- val sourcePsi = startNode.sourcePsi as? KtExpression ?: return
- val (isClassType, name) =
- analyze(sourcePsi) {
- val symbol =
- when (sourcePsi) {
- is KtDotQualifiedExpression -> {
- val lastChild = sourcePsi.lastChild
- if (lastChild is KtReferenceExpression) {
- lastChild.mainReference.resolveToSymbol()
- } else {
- null
- }
- }
- is KtReferenceExpression -> sourcePsi.mainReference.resolveToSymbol()
- else -> null
- }
- as? KtClassOrObjectSymbol ?: return
-
- symbol.classKind.isClass to symbol.name
- }
-
+ val (isClassType, name) = startNode.isClassReference()
if (isClassType) {
context.report(
getIssue(),
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt
new file mode 100644
index 0000000..d393265
--- /dev/null
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.common.lint
+
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
+import org.jetbrains.kotlin.idea.references.mainReference
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.kotlin.psi.KtReferenceExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
+
+/** Catches simple class/interface name reference */
+fun UExpression.isClassReference(): Pair<Boolean, String?> {
+ /**
+ * True if:
+ * 1. reference to object (i.e. val myStart = TestStart(), startDest = myStart)
+ * 2. object declaration (i.e. object MyStart, startDest = MyStart)
+ * 3. class reference (i.e. class MyStart, startDest = MyStart)
+ *
+ * We only want to catch case 3., so we need more filters to eliminate case 1 & 2.
+ */
+ val isSimpleRefExpression = this is USimpleNameReferenceExpression
+
+ /** True if nested class i.e. OuterClass.InnerClass */
+ val isQualifiedRefExpression = this is UQualifiedReferenceExpression
+
+ if (!(isSimpleRefExpression || isQualifiedRefExpression)) return false to null
+
+ val sourcePsi = sourcePsi as? KtExpression ?: return false to null
+ return analyze(sourcePsi) {
+ val symbol =
+ when (sourcePsi) {
+ is KtDotQualifiedExpression -> {
+ val lastChild = sourcePsi.lastChild
+ if (lastChild is KtReferenceExpression) {
+ lastChild.mainReference.resolveToSymbol()
+ } else {
+ null
+ }
+ }
+ is KtReferenceExpression -> sourcePsi.mainReference.resolveToSymbol()
+ else -> null
+ }
+ as? KtClassOrObjectSymbol ?: return false to null
+
+ (symbol.classKind.isClass || symbol.classKind.name == "INTERFACE") to
+ symbol.name?.asString()
+ }
+}
diff --git a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
index 606e2b3..647068d 100644
--- a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
+++ b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
@@ -132,6 +132,8 @@
builder.navigation<TestGraph>(startDestination = Outer.InnerClass)
builder.navigation<TestGraph>(startDestination = InterfaceChildClass)
builder.navigation<TestGraph>(startDestination = AbstractChildClass)
+ builder.navigation<TestGraph>(startDestination = TestInterface)
+ builder.navigation<TestGraph>(startDestination = TestAbstract)
}
"""
)
@@ -171,7 +173,19 @@
you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
builder.navigation<TestGraph>(startDestination = AbstractChildClass)
~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+ builder.navigation<TestGraph>(startDestination = TestInterface)
+ ~~~~~~~~~~~~~
+src/com/example/test.kt:14: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestAbstract(...)?
+If the class TestAbstract does not contain arguments,
+you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
+ builder.navigation<TestGraph>(startDestination = TestAbstract)
+ ~~~~~~~~~~~~
+7 errors, 0 warnings
"""
)
}
@@ -252,6 +266,8 @@
provider.navigation(startDestination = Outer.InnerClass) {}
provider.navigation(startDestination = InterfaceChildClass) {}
provider.navigation(startDestination = AbstractChildClass) {}
+ provider.navigation(startDestination = TestInterface)
+ provider.navigation(startDestination = TestAbstract)
}
"""
)
@@ -291,7 +307,19 @@
you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
provider.navigation(startDestination = AbstractChildClass) {}
~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+ provider.navigation(startDestination = TestInterface)
+ ~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestAbstract(...)?
+If the class TestAbstract does not contain arguments,
+you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
+ provider.navigation(startDestination = TestAbstract)
+ ~~~~~~~~~~~~
+7 errors, 0 warnings
"""
)
}
@@ -372,6 +400,8 @@
navGraph.setStartDestination(startDestRoute = Outer.InnerClass)
navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
navGraph.setStartDestination(startDestRoute = AbstractChildClass)
+ navGraph.setStartDestination(startDestRoute = TestInterface)
+ navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
}
"""
)
@@ -411,7 +441,19 @@
you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
navGraph.setStartDestination(startDestRoute = AbstractChildClass)
~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+ navGraph.setStartDestination(startDestRoute = TestInterface)
+ ~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor InterfaceChildClass(...)?
+If the class InterfaceChildClass does not contain arguments,
+you can also pass in its KClass reference InterfaceChildClass::class [WrongStartDestinationType]
+ navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
+ ~~~~~~~~~~~~~~~~~~~
+7 errors, 0 warnings
"""
)
}
diff --git a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
index 80e2a36..bc11285 100644
--- a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
+++ b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
@@ -33,6 +33,7 @@
listOf(
DeepLinkInActivityDestinationDetector.DeepLinkInActivityDestination,
WrongStartDestinationTypeDetector.WrongStartDestinationType,
+ WrongNavigateRouteDetector.WrongNavigateRouteType,
)
override val vendor =
diff --git a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt
new file mode 100644
index 0000000..ee38a50
--- /dev/null
+++ b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.runtime.lint
+
+import androidx.navigation.common.lint.isClassReference
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClassLiteralExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.getParameterForArgument
+
+class WrongNavigateRouteDetector() : Detector(), SourceCodeScanner {
+
+ companion object {
+ val WrongNavigateRouteType =
+ Issue.create(
+ id = "WrongNavigateRouteType",
+ briefDescription =
+ "Navigation route should be an object literal or a destination class instance " +
+ "with arguments.",
+ explanation =
+ "If the destination class contains arguments, the route is " +
+ "expected to be class instance with the arguments filled in.",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation =
+ Implementation(WrongNavigateRouteDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+
+ final override fun getApplicableMethodNames(): List<String> = listOf("navigate")
+
+ final override fun visitMethodCall(
+ context: JavaContext,
+ node: UCallExpression,
+ method: PsiMethod
+ ) {
+ val startNode =
+ node.valueArguments.find { node.getParameterForArgument(it)?.name == "route" } ?: return
+
+ val isClassLiteral = startNode is UClassLiteralExpression
+ val (isClassType, _) = startNode.isClassReference()
+ if (isClassType || isClassLiteral) {
+ context.report(
+ WrongNavigateRouteType,
+ startNode,
+ context.getNameLocation(startNode as UElement),
+ """
+ The route should be a destination class instance or destination object.
+ """
+ .trimIndent()
+ )
+ }
+ }
+}
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
new file mode 100644
index 0000000..3931fb0
--- /dev/null
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
@@ -0,0 +1,451 @@
+/*
+ * 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.navigation.runtime.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class WrongNavigateRouteDetectorTest : LintDetectorTest() {
+
+ override fun getDetector(): Detector = WrongNavigateRouteDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(WrongNavigateRouteDetector.WrongNavigateRouteType)
+
+ @Test
+ fun testEmptyConstructorNoError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ fun createGraph() {
+ val navController = NavController()
+ navController.navigate(route = TestClass())
+ }
+ """
+ )
+ .indented(),
+ byteCode,
+ )
+ .skipTestModes(TestMode.FULLY_QUALIFIED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testNoError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ fun createGraph() {
+ val navController = NavController()
+ navController.navigate(route = TestClassWithArg(10))
+ navController.navigate(route = TestObject)
+ navController.navigate(route = classInstanceRef)
+ navController.navigate(route = classInstanceWithArgRef)
+ navController.navigate(route = Outer)
+ navController.navigate(route = Outer.InnerObject)
+ navController.navigate(route = Outer.InnerClass(123))
+ navController.navigate(route = 123)
+ navController.navigate(route = "www.test.com/{arg}")
+ navController.navigate(route = InterfaceChildClass(true))
+ navController.navigate(route = InterfaceChildObject)
+ navController.navigate(route = AbstractChildClass(true))
+ navController.navigate(route = AbstractChildObject)
+ }
+ """
+ )
+ .indented(),
+ byteCode,
+ )
+ .skipTestModes(TestMode.FULLY_QUALIFIED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testHasError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ fun createGraph() {
+ val navController = NavController()
+ navController.navigate(route = TestClass)
+ navController.navigate(route = TestClass::class)
+ navController.navigate(route = TestClassWithArg)
+ navController.navigate(route = TestClassWithArg::class)
+ navController.navigate(route = TestInterface)
+ navController.navigate(route = TestInterface::class)
+ navController.navigate(route = InterfaceChildClass)
+ navController.navigate(route = InterfaceChildClass::class)
+ navController.navigate(route = TestAbstract)
+ navController.navigate(route = TestAbstract::class)
+ navController.navigate(route = AbstractChildClass)
+ navController.navigate(route = AbstractChildClass::class)
+ navController.navigate(route = InterfaceChildClass::class)
+ navController.navigate(route = Outer.InnerClass)
+ navController.navigate(route = Outer.InnerClass::class)
+ }
+ """
+ )
+ .indented(),
+ byteCode,
+ )
+ .skipTestModes(TestMode.FULLY_QUALIFIED)
+ .run()
+ .expect(
+ """
+src/com/example/test.kt:7: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestClass)
+ ~~~~~~~~~
+src/com/example/test.kt:8: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestClass::class)
+ ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:9: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestClassWithArg)
+ ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:10: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestClassWithArg::class)
+ ~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:11: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestInterface)
+ ~~~~~~~~~~~~~
+src/com/example/test.kt:12: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestInterface::class)
+ ~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = InterfaceChildClass)
+ ~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:14: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = InterfaceChildClass::class)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:15: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestAbstract)
+ ~~~~~~~~~~~~
+src/com/example/test.kt:16: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = TestAbstract::class)
+ ~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:17: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = AbstractChildClass)
+ ~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:18: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = AbstractChildClass::class)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:19: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = InterfaceChildClass::class)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:20: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = Outer.InnerClass)
+ ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:21: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+ navController.navigate(route = Outer.InnerClass::class)
+ ~~~~~~~~~~~~~~~~~~~~~~~
+15 errors, 0 warnings
+ """
+ )
+ }
+
+ private val sourceCode =
+ """
+
+package androidx.navigation
+
+public open class NavController {
+
+ public fun navigate(resId: Int) {}
+
+ public fun navigate(route: String) {}
+
+ public fun <T : Any> navigate(route: T) {}
+}
+
+object TestObject
+
+class TestClass
+
+class TestClassWithArg(val arg: Int)
+
+val classInstanceRef = TestClass()
+
+val classInstanceWithArgRef = TestClassWithArg(15)
+
+object Outer {
+ data object InnerObject
+
+ data class InnerClass (
+ val innerArg: Int,
+ )
+}
+
+interface TestInterface
+class InterfaceChildClass(val arg: Boolean): TestInterface
+object InterfaceChildObject: TestInterface
+
+abstract class TestAbstract
+class AbstractChildClass(val arg: Boolean): TestAbstract()
+object AbstractChildObject: TestAbstract()
+"""
+
+ private val byteCode =
+ compiled(
+ "libs/StartDestinationLint.jar",
+ kotlin(sourceCode).indented(),
+ 0xa3960069,
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUuMSTsxLKcrPTKnQy0ssy0xPLMnM
+ zxPi90ssc87PKynKz8lJLfIu4RLl4k7Oz9VLrUjMLchJFWILSS0u8S5RYtBi
+ AADcysPxVwAAAA==
+ """,
+ """
+ androidx/navigation/AbstractChildClass.class:
+ H4sIAAAAAAAA/41QTW9SQRQ9M+8DeAV54Belamv9CGUhtHGnaaQkJiTYRW1Y
+ wGrgvdAJj/eSNwPpkt/i2o2JxsSFIS79UcY7j2pcsHAxZ+65c3LunfPz17fv
+ AF7iGcNzEQdpIoPrViyWciq0TOJWZ6x0Kia6eyWjoBsJpXJgDAfbtJeh0n/0
+ OVgM7msZS33KYDeGRwMGq3E0KMJBzoONPHGRThnYsAgPOwVwFEmqr6RiaPT/
+ b5tXNGUa6o4xIvshQ6U/S3Qk49a7UItAaEESPl9a9E1moGAANHZG/WtpWJuq
+ 4JjhbL2qerzGs7Needzf8Xjeqq1XJ7zNzkpV1+d13rZ+fHC5b19U/rI8qet2
+ 3vFd43TCcLh1/X8Doq1oCf9cLLtJrNMkisL0xUxTAN0kCBnKfRmH54v5OEwv
+ xTiiTrWfTEQ0EKk0/KbpvU8W6SR8Kw3ZvVjEWs7DgVSSXjtxnOhsssIxpWtn
+ /66asKniVDtwCQ+InRLndHvNryg0976g9CnTPCY0GuApDgnvbVS4hbKJkSrj
+ RqnDp7Pxapl06Xaan1H6uNWmuBHc2HA8yXCfXoE32ZIObo9g9XCnh7s9Gnuf
+ StR62EV9BKawhwcj5BTKCg8VPIVHCq6Cr1D5DV3dCrfVAgAA
+ """,
+ """
+ androidx/navigation/AbstractChildObject.class:
+ H4sIAAAAAAAA/41STW/TQBB9u0kcxw00lI8mFGhpQVAOuK24USGFCCRLwUg0
+ ioR6WturdhtnV7I3UY858UP4BxWHSiChCG78KMTYhA+JHrDlmX2zb99o3vrb
+ 94+fATzBPYYHQieZUcmpr8VUHQmrjPa7UW4zEdvesUqT19GJjG0djGHjIvJA
+ 5vbXgToqDM6+0so+Y6g83B42UYPjoYo6Q9Ueq5xhu/+fPZ8yuPtxWqp54IWE
+ G4QHg27Ye9HEJXgNKl5m2Oqb7Mg/kTbKhNK5L7Q2tlTN/dDYcJKmJHWlPzKW
+ xPxX0opEWEE1Pp5WyAlWhEYRwMBGVD9VBdqhVbJLDeYzz+NtXn7zmfv1HW/P
+ Z3t8hz2vu/zLe4e3eEHdY9i8cLi/PaK2rVBMe0bbzKSpzB6PLMPam4m2aiwD
+ PVW5ilLZ/TMEOdcziWRY7istw8k4ktlAEIdhpW9ikQ5Fpgq8KHoHZpLF8qUq
+ QGchPPxHFrtkX7WcuVO4SfkOIYdyizKnt1aidUJ+4Qzl2qNzuGfl9saCDNzH
+ XYrNnwQ0SApwsfT78Cqxi2fpE/jbczQ/YPmsLHBslvE2tsofkm6JBFYOUQlw
+ NcC1ANdxg5ZYDdBG5xAsx02s0X4OL8etHM4PGz/wlc0CAAA=
+ """,
+ """
+ androidx/navigation/InterfaceChildClass.class:
+ H4sIAAAAAAAA/41Qz28SQRT+ZhZY2FJZqFZK/dFatYWD0MabprElMSHBmtSG
+ A5wGdqVTltlkZyA98rd49mKiMfFgiEf/KOMbSpqYcPAw771v3jffe/P9/vPj
+ J4CX2GfYFypIYhlc15WYyqEwMlb1ljJh8lEMwualjIJmJLR2wRj8KzEV9Uio
+ Yf19/yocGBcOw+4qiYtQm1sZF2mGzGuppDlmSB10qx0G56DaycNFzkMKHmGR
+ DBlYN4881nPguENUcyk1Q7X9n1u+ojHD0JxYJdLvMhTbo9hEUtXfhUYEwgii
+ 8PHUof8zG3I2gOaO6P5aWtSgKjhkOJ3PSh4v88WZzzzur3k865TnsyPeYKfr
+ pYzPK7zh/PqU4X7qvHiLssSupLJpP2OVjhj2Vu7/j0W0Fm3hn4lpM1YmiaMo
+ TF6MDFnQjIOQodCWKjybjPthciH6Ed2U2vFARB2RSIuXl96HeJIMwrfSgq3z
+ iTJyHHakltQ9USo2i9Eah+RvigZm6JSs4fRvTrWLLMUnhI4Jc8pe7TvWatvf
+ UPiy4OxRtK+AHTyluHnDgo+idZIqq0bGk+7GUqtuDaacrn1F4fNKmfwNYSnD
+ 8WwRd/Gc8hvq3aXevR6cFjZbuN9CGVtUotLCNh70wDQe4lEPrkZR47FGXmNH
+ I6tR0tj4C6G2ycbxAgAA
+ """,
+ """
+ androidx/navigation/InterfaceChildObject.class:
+ H4sIAAAAAAAA/41SXWsTQRQ9M0mTzTbatH4l1q9aldoHty2+WYQaFBbiCjYE
+ pE+T3TGdZDMDu5PQxzz5Q/wHxYeCggR980eJd9ZYHxR0l7kfZ849M/fufvv+
+ 8TOAx3jAsCV0khmVnARaTNVAWGV0EGors7cilu1jlSav+kMZ2yoYQ2MopiJI
+ hR4Ev9ASw8bfNLoyt+c6VSwxVPaVVvYpQ2nrYa+OKjwfZdQYyvZY5Qzbnf+9
+ yxMGbz9OCzkf3Gl4YXTYPYjaz+tYQb1GYINhs2OyQTCUtp8JpfNAaG1sIZsH
+ kbHRJE1JarUzMpbEgpfSikRYQRgfT0s0IuZMzRkwsBHhJ8plOxQlu3TAfOb7
+ vMmLNZ95X9/x5ny2x3fYs6rHv7yv8AZ31D13l39Oic5tRGLaNtpmJk1l9mhk
+ GdZfT7RVYxnqqcpVP5UHv7ug2bVNIhlWOkrLaDLuy6wriMOw1jGxSHsiUy5f
+ gP6hmWSxfKFc0loI9/6QxS7Nr0wtV2i13EDJ36G+Xb5GntNL34+yDcoCNxzy
+ S9tn8E+L7bsLMoiwSbb+k4BlikCFF86LrxHbPcufwN+c4eIHrJ4WAMe9wt7G
+ /eJnZbhEApePUApxJcTVkEqbFKIV4jrWj8By3MBN2s9Rz3Erh/cDKimwMukC
+ AAA=
+ """,
+ """
+ androidx/navigation/NavController.class:
+ H4sIAAAAAAAA/41SW08TQRT+Zttut8utrYJQQeWiFFC2EH2xhERJjEtqNdL0
+ hadpuynTbmeT3WnDY3+L/8AnjQ+G+OiPMp7ZNlBAo5vsuX/fnDNzfv769h3A
+ c+wzrHLZCgPROnckH4g2VyKQTpUPjgKpwsD3vTANxpDt8AF3fC7bzvtGx2uq
+ NBIM5oGQQh0yJIpb9WmkYNpIIs2QVGciYliv/JO9zGCNcx7hiu5WnSEVepHb
+ YmAuw3yxcnX2iQqFbJd1zXolCNtOx1ONkAsZOVzKQMUHRE41UNW+75c1U9BX
+ noUcw4NuoHwhnc6g5wipvFBy33GlZoxEM0rjDh3WPPOa3TH8Aw95z6NChs3J
+ JkYXUP5TW9OYx4KNu7jHkL9dcGOaMZGeZvmg9vJ25rBYq8Xp/O0cQ64ynuid
+ p3iLK04xozdI0NMyLTJagG6xS/Fzob0SWa09hvBiuGwbi4ZtZC+GtmFpg34r
+ Sdqif9ZaWLwY7hsl9jr145NJyeOVbKJglJJrlnUxzKa2KbVvZs2C8XZUkD6e
+ HRVQ1CKdmfCpqmTrk2nfdD812qdrS7DbVfT2R0GLVmCuIqRX7fcaXljjDd/T
+ wwdN7td5KLQ/Dm587Eslep4rByISFLp8rVdXi8CQORFtyVU/JIh9EvTDpvdG
+ aPzSGF8foSdAWIVBW6y/JHVLS01yhzxH9046tf0F1mcyDDwlacbBJJ6RnB4V
+ IAObdA5TcUSDX8T1NP5NoBkDF0bJMVBbM5glqSnmKKcpyqR1VXonn/+KxetE
+ JqwJovQlUZoolii/q23df3bcWAGJ/2G1/8q6THknrr5/nd1AKZbb2CNdoegK
+ XcmDUyRcPHTxyKUbXiMT6y428PgULMITbJ5iKoIdoRjBjLRNxlaEXIRChJnY
+ Lf4G70EG2roEAAA=
+ """,
+ """
+ androidx/navigation/NavControllerKt.class:
+ H4sIAAAAAAAA/41SXU8TQRQ9s1va7VLoFkRoAZEPsfWDBfVNYkKamGysNUGC
+ ITxN27EuLLPJzrThkd/iL1B5IJHEEB/9UcY7ayMBVNhs5s69c865987cHz+/
+ fgPwDKsMi1x2kjjsHPqS98Mu12Es/Sbv12OpkziKRPJK58AYvD3e537EZdd/
+ 09oTbYraDONdoesRVyqQSnPZFpviPcN8tdb4m+6WUL/RzylxI066/p7QrYSH
+ UvlcylinMOU3Y93sRRGhvPYV8blrpAtwkM/DgstQuVzeu1B/2Ei6qVD1uioH
+ YCpjsv0vkeWbSRQwiqIpymNw1ttRKEP9gsGu1rYZZv8rkcMthux6yijgNsZd
+ TGCSYekmiXMoM2SqQW3bUKddVDDDUGrsx5pq8F8LzTtcc2rROujbNBTMLHmz
+ gIHtm41Fh4eh2dG8WJ01ho2zozH37Mi1pizXcuyBtRZK3tlRxVpl3z9mHTqv
+ ZBzLsymaoeDQeTDr5YzQE3reC5O2sq8Zpjd7UocHIpD9UIWtSGycDwa1Uo87
+ gqHYCKVo9g5aItnihGFw38a9pC1ehsYpDzS2ryhgjd4gY5qim6A5oSYfkpcl
+ myNbMc90KWajjCHyLDwib4ai5st8wfCn9JIeD7DA+AVeGQWMXGWVLrMmLrAc
+ jJEOS1lPkb4DJk8xsXOCqWMMn6Ky4xVPMHuM0uc/Qi6lMenNZFhYSdt7AJ9s
+ nRB3qPy5XdgB7gaYD7CAxQBLuBdgGfd3wRSqqO3CUebPKwwpZBVGFYoKBYWR
+ X85LYc0vBAAA
+ """,
+ """
+ androidx/navigation/Outer$InnerClass.class:
+ H4sIAAAAAAAA/41U31MbVRT+7m5+bJYAG6AtkNiqRExC2wVstRZaBRRZDKGC
+ w1jx5ZKsYWHZxd0NU18cnvondEZfnHEcn3ioMxocO+Ng++bf5Diem90mNVSG
+ meSec8+e853vnnPu/euf3/8AcAPrDHnu1DzXqj3QHX5g1XlguY6+2ghML284
+ jukt2Nz3k2AM2g4/4LrNnbq+urVjVoMkZIbErOVYwV2GWMEobjDIheJGGnEk
+ VcSgMCiWQJnz6gzMSENFTwoS0uQfbFs+w3j5PARmGHrqZmC0sSiNwaBW3b19
+ 1zGdYIoAq+7+1wxF4nFezLGy69X1HTPY8rjl+Dp3HDdoeft6xQ0qDdueEYdJ
+ qMT5IkNapMjXzC95ww4YtgrnS2QY5e7azZyTYxqDGBLZR6mUgbseeJZDxx8q
+ FF+ADK10nkvdtvmGZddML4nLKq6Idgx1sAvPO3NHwWvUSL6/bzo1hmuF09Cn
+ s0XIRHAMeQH+BkNOlP4sxzeFY0E4LpztWBKOE2nk8IrQrtHht7m/veDWTIZM
+ J9JwArMuzjcZDiBNmI5pFVN4i05kftXgNs3YhcJL6v85zf5Z7afe8y3bpKrG
+ 3WDb9BgGTqMQmfKuG9iWo6+YAa/xgJNN2juQ6X4xsaTEAhr+XbI/sMSOuEo1
+ GtgfTw4vq9KwpErayaFKP0lTVElJkOwhKZPsU54+VIZPDqelSTbfO5DQpFFp
+ Un76Q0LSYsspLSl2S88eysuDmkI6OSqKFDqRmZE5Rbo6rWg9o7FhNsmWnj2S
+ KTAdejxipPeS3if0tUwbXiE6ozElriUE12kmTjDyvwObxCLdxc5k0VtR4QcL
+ rhN4rm2b3vVduiyxsHn9ZcsxK429LdP7VNRXlNWtcnuDe5bYR8bsWsMJrD3T
+ cA4s3yLTXKc3DL3rAa/urvD9yDvf7X2Pe3zPJGr/CUt3KJq0Vdfdhlc1Fy0B
+ MRJBbJxKR8Mk0VsmSjAg3i/SFNLpVaB1mXaL9F0iqZaOkSplf0Xvz7ST8DGt
+ fRAdH6X4LFIky7S7GHrTt34xG6QJVBolaPQPMXUxMiTjpV/Qe9SGS7SM2RZM
+ OnSIYDJE7nnwWHcwe2kAvSwEKwKmiKXglHoC6X72GJcet4NCsqk22VREdiVi
+ cwHQUhjGSJR7PCpWJhf75lsogsFsKdtENoSs0CqDCQS621H62yQFtdwTXLl/
+ jFcHXm9iXEQ2UdSKTVxt4vrjrmPkIkYv8KBVb9dgPKpBi8FvuNFdBiWKZ7iJ
+ tyMeX5AU7cqXJn5CPHY08Sek7xCXjyZOIK0IoKv0/15YYmFPKq32yUnlb2SS
+ tO9ULN+uWB638C7lWSU9KUi906rBvVYoXS98hCUq3yctQANrJD8j+23q1Mwm
+ ZAOzBu4YuIv3SMX7BuYwvwnmYwEfbKLfF78PfaitNeFD85HxMeBj0MfNlvGW
+ D91HjvR/AXBODlL6BwAA
+ """,
+ """
+ androidx/navigation/Outer$InnerObject.class:
+ H4sIAAAAAAAA/41US08UQRD+umcfs7M8loc8FR8s8lJmQTxBTJBoHLIsxiUY
+ 5dTsjjAwzOhM74YjJ29ePXj04ImDxAOJJgYlXvxRxOrZISAEY7Lb/VV1VX3V
+ X/Xu7+Ov3wFM4z7DkPCqge9Ud0xP1J11IR3fM5dq0g7ylufZwdLapl2RaTCG
+ 3KaoC9MV3rp54tUYUrOO58gHDNrI6EoTkkgZSCDNkJAbTsgwXPwvhhkGXfpl
+ GTjeOkPnyGjxlK3hpYjBoh+sm5u2XAuE44Wm8DxfRgVDs+TLUs11KSp7pqyO
+ Fiq8IcKNeb9qR01a2q/p43fUuP2mJlzq8MpI8fzNZkZfMuT/xUZUYs21iS7p
+ yw07YGi/WIWoZytupI8BrkTRrVJ5ea40/6gJfTAy5OxnaCtu+ZLCzEVbiqqQ
+ ghL5dl2jGTG1ZNQCBrZF/h1HWQVC1UmGV4e7Awbv4QbPHe4aXFcgG++6oVy5
+ Fv3ordFzuDvFC+xhWuc/P6Z4ji905LQ+XkhM6blkX6KHFdiTo/faQiaXIm+a
+ MCOsE84orNimmOqh99JppjFKb6Qk6vO+JwPfde1gYksy9D+redLZti2v7oQO
+ aTZ3qiO9ksZcWouOZ5dq22t2sKx0VXL6FeGuiMBRduxsLktR2VoUr2M7f772
+ UxGIbZu6+YukKXoR864IQ5tMo+zXgor92FEleuMSKxeawySNJxEp36umRfsd
+ slK0N9OepNNkZN0ly1TzUd6xA+j7BDgm4mCgh46BpkYAMlRKFc2Sh0fJN+Nk
+ rb31c3R0Gq7F4WeZSWa0xbynqe17l6QydKAzZrJo57R3j41/QjKxN/4D/AOS
+ 2t74IfjzxF7UeIHWBHhaj4p1NRLiYgp10ZeROlAvmn4/BHS644kU3VECkP0G
+ /uIAvV9wdT9yaJiiVenIMYYWUvVexDdOf0WqNYZrJM/AKjQL1y3csOh2twhi
+ 0EIeQ6tgIW5jeBVGqD4jIVIhOiLQFSIXgSytfwCP7WED4AQAAA==
+ """,
+ """
+ androidx/navigation/Outer.class:
+ H4sIAAAAAAAA/4VRW2sTQRg9M5vLZhNtGi9JrK2XptpUcNviUy1CDQoLMYW2
+ BCRPk2SIk2xmYXcS+pgnf4j/oPhQUJCgb/4o8dttNA9S3GG/M9/tfDNnfv76
+ 8g3ACzxjqArdDwPVP3e1mKqBMCrQ7vHEyDALxlAciqlwfaEH7nF3KHsmC4sh
+ c6i0Mq8YrO16u4A0Mg5SyDKkzAcVMaw1r2V9yWAf9vyk3wGPm2yvdXp21Gq8
+ KeAGnBwFbzJsNoNw4A6l6YZC6cgVWgcm4YncVmBaE98nqtXmKDBE5r6TRvSF
+ ERTj46lFt2OxycUGDGxE8XMVe7u06+8x1OezgsMr3OHF+czhtmX/+Mgr89k+
+ 32UH3Eq9ztr8+6cML/K4YZ/FNI6ntQwbvojokvnEuVKFoXbtjWvLpiweMGz9
+ p/KPzo9I/ZaYNgJtwsD3Zfh8RHPWTibaqLH09FRFquvLo6UwpH8j6EuGlabS
+ sjUZd2V4JqiGodQMesJvi1DF/iJYWJ5MUrNzGkzCnnyr4lx1Maf9zxTs0Qul
+ Elmr8YMR1sjLEBYJOa104m2R58biE6Z3LmFfJOkni2KgjKdkC1cFyBEVYCP/
+ t7lM1fGX/wr+/hKFz1i5SAIWtsmWKP2Q/nU6x2PCDcJ6MmITO4QHRLNKxKUO
+ LA+3PNz2cAd3aYuyhwqqHbAI97DWQTqCE+F+hEyE9QgbvwFBj+0jIgMAAA==
+ """,
+ """
+ androidx/navigation/TestAbstract.class:
+ H4sIAAAAAAAA/4VRy0oDMRQ9SduxjlWnPusLfICoC0fFnSKoIBSqgko3rtJO
+ 0NhpApO0uOy3+AeuBBdSXPpR4s3o3s3hPG7Cyc3X9/sHgEOsMKwKnWRGJc+x
+ Fn31IJwyOr6T1p22rMtE242AMURPoi/iVOiH+Lr1JL1bYAiOlVbuhKGwtd2s
+ oIQgRBEjDEX3qCzDeuO/y48Yqo2OcanS8aV0IhFOkMe7/QIVZB5GPYCBdch/
+ Vl7tEUv2qftwEIa8xkMeERsOypu14eCA77Gz0udLwCPu5w6YPx1dif650S4z
+ aSqz3Y6jkucmkQyTDaXlVa/bktmdaKXkTDVMW6RNkSmv/8zw1vSytrxQXizc
+ 9LRTXdlUVlF6qrVx+eNscQ2cdvBX2a+EsEYqzjVQ2nlD+ZUIxwJhkJsbWCSs
+ /A5gFGGeL+U4j+X8rxjGKKvco1DHeB0TdUwiIopqHVOYvgezmMEs5RahxZxF
+ 8AMTgVUI6AEAAA==
+ """,
+ """
+ androidx/navigation/TestClass.class:
+ H4sIAAAAAAAA/31RO0sDQRD+ZmMuekY93/HdqoWnYqcIGhACUUEljdUmt+ia
+ yy7cboJlfov/wEqwkGDpjxLnTmubj+8xM8zsfn2/fwA4wgZhQ5okszp5jo0c
+ 6AfptTXxnXK+nkrnKiBC9CQHMk6leYiv20+q4ysoEYITbbQ/JZS2d1pVlBGE
+ GEOFMOYftSNsNf+dfEyYbXatT7WJL5WXifSSPdEblHg1ymEiBxCoy/6zztU+
+ s+SAsDkahqGoiVBEzEbD2mh4KPbpvPz5EohI5FWHlPdGV3JQt8ZnNk1Vttf1
+ vF/dJoow09RGXfV7bZXdyXbKzlzTdmTakpnO9Z8Z3tp+1lEXOhcrN33jdU+1
+ tNOcnhljfXGXwwEEn/+3cP4ajDVWcaGB8u4bxl+ZCKwwBoU5g1XG6m8BJhAW
+ +VqBy1gv/ogwyVn1HqUGphqYbnBXxBSzDcxh/h7ksIBFzh1ChyWH4AcFuY/X
+ 4AEAAA==
+ """,
+ """
+ androidx/navigation/TestClassWithArg.class:
+ H4sIAAAAAAAA/41QTYvTUBQ97yVN09jatH51On47yEwXpjO4UwZrQQjUEcah
+ Lrp6bUPnTdMXyHsts+xvce1GUAQXUlz6o8T70sGVCyE5956bw7m559fv7z8A
+ PMcew55Q0zyT08tIiZWcCSMzFZ0l2vRTofUHac57+awMxhBeiJWIUqFm0bvx
+ RTIxZTgM3kuppDlmcPfjgyGDs38wrKKEcgAXPnGRzxhYXEWAaxVwVElqzqVm
+ eDr4n90vaMcsMT1rQ+YxQ2Mwz0wqVfQ2MWIqjCAJX6wcOolZqFgALZ3T/FJa
+ 1qVuesjQ36ybAW/xgIebdUAPD/2A+05rsz7iXfa61vRC3uZd5+dHj4fuaeMv
+ 80nddv1S6FmrI2YXhCdi1c+UybM0TfJnc0On9bNpwlAfSJWcLBfjJD8T45Qm
+ zUE2EelQ5NLyq2HwPlvmk+SNtGTndKmMXCRDqSV97SmVmSISjUPKzS1uatoY
+ qePUl+ARPiB2TJxTDTrfUOnsfkXtc6F5SGg1QAOPCG9vVbiOuo2IOutGiSKk
+ d+sV2eSoljpfUPv0T5vqVnBlw/G4wPt4QvVV8ZMl3BjBiXEzxq2Y1t6hFq0Y
+ O2iPwDR2cXeEskZd455GUKCnEWo0/gB/P3a9nQIAAA==
+ """,
+ """
+ androidx/navigation/TestInterface.class:
+ H4sIAAAAAAAA/4WOz0rDQBDGv9lo08Z/qVqoR/Fu2tKbJykIgaqg4iWnbbIt
+ 22x3IbsNPfa5PEjPPpR0Ux/AGfjmmxn4zfz8fn0DGKNHuOW6qIwsNonmtVxw
+ J41OPoR1qXaimvNchCBCvOQ1TxTXi+R1thS5CxEQutPSOCV18iwcL7jjDwS2
+ qgMPp0Y6jYBApZ9vZNMNvCuGhN5u245Yn0Us9m7e321HbEDNckS4m/77lb/k
+ wfELrydGu8ooJar70hGid7OucvEklSDcvK21kyvxKa2cKfGotXEHmG35UzjC
+ XzBcHfQS174OPfjYZytDkCJM0U7RQeQtTlKc4iwDWZzjIgOziC26e5qGvyhR
+ AQAA
+ """,
+ """
+ androidx/navigation/TestObject.class:
+ H4sIAAAAAAAA/31STW/TQBB9u0kcxw00lI8mFEqhPQAH3FbcqJBKBJKlYCQa
+ Rap62sSrsImzK9kbq8ec+CH8g4pDJZBQBDd+FGLWBDggYUsz896+edoZ+/uP
+ T18APMUew7bQSWZUch5qUaixsMrosC9z+2Y4kSNbB2NoTUQhwlTocfibrTB4
+ R0or+5yh8vDRoIkavABV1Bmq9p3KGXZ6/7d+xuAfjdLSJAB3nX4Un/SP4+7L
+ Jq4gaBB5lWG3Z7JxOJF2mAml81BobWxploexsfE8TcnqWm9qLJmFr6UVibCC
+ OD4rKjQnc6HhAhjYlPhz5dA+VckBw95yEQS8zQPeomq58L+95+3l4pDvsxd1
+ n3/94PEWd9pD5hxasSi6RtvMpKnMnkwtw9bbubZqJiNdqFwNU3n89460j65J
+ JMN6T2kZz2dDmfUFaRg2emYk0oHIlMMrMjgx82wkXykHOivjwT+2OKDtVMuR
+ Om5ZlLcJee6ClDm9tRLdIxS6wSnXHl/CvyiPd1ZiUPN9is1fAjTICvCx9qd5
+ k9TuWfsMfnqJ5kesX5QEx4My3sVu+TfRRyCDjTNUIlyPcCPCTdyiEpsR2uic
+ geW4jS06zxHkuJPD+wnyhROwigIAAA==
+ """
+ )
+}
diff --git a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
index edaa770..bbc74df 100644
--- a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
@@ -46,16 +46,10 @@
<string name="label_page_range" msgid="8290964180158460076">"<xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> / <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_page_range" msgid="5286496438609641577">"faqet nga <xliff:g id="FIRST">%1$d</xliff:g> deri në <xliff:g id="LAST">%2$d</xliff:g> nga <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imazhi: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
- <!-- no translation found for hint_find (5385388836603550565) -->
- <skip />
- <!-- no translation found for message_no_matches_found (6965828658999779258) -->
- <skip />
- <!-- no translation found for message_match_status (6288242289981639727) -->
- <skip />
- <!-- no translation found for action_edit (5882082700509010966) -->
- <skip />
- <!-- no translation found for password_not_entered (8875370870743585303) -->
- <skip />
- <!-- no translation found for retry_button_text (3443862378337999137) -->
- <skip />
+ <string name="hint_find" msgid="5385388836603550565">"Gjej te skedari"</string>
+ <string name="message_no_matches_found" msgid="6965828658999779258">"Nuk u gjetën përputhje."</string>
+ <string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
+ <string name="action_edit" msgid="5882082700509010966">"Modifiko skedarin"</string>
+ <string name="password_not_entered" msgid="8875370870743585303">"Fut fjalëkalimin për ta shkyçur"</string>
+ <string name="retry_button_text" msgid="3443862378337999137">"Riprovo"</string>
</resources>
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
index 4d8eeb9..c06a863 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import java.util.concurrent.Executor
public class MyUiInterfaceClientProxy(
@@ -31,4 +32,12 @@
sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
isZOrderOnTop, clientExecutor, client)
}
+
+ public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+ }
+
+ public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+ }
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
index d7f90e3..2a26042 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import java.util.concurrent.Executor
public class MySecondInterfaceClientProxy(
@@ -31,4 +32,12 @@
sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
isZOrderOnTop, clientExecutor, client)
}
+
+ public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+ }
+
+ public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+ }
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
index 86709a8..b16b160 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import java.util.concurrent.Executor
public class MyUiInterfaceClientProxy(
@@ -31,4 +32,12 @@
sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
isZOrderOnTop, clientExecutor, client)
}
+
+ public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+ }
+
+ public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+ }
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 54d5210..78c6c592 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -100,6 +100,8 @@
.build()
)
addFunction(generateOpenSession())
+ addFunction(generateAddObserverFactory())
+ addFunction(generateRemoveObserverFactory())
}
}
@@ -171,6 +173,38 @@
)
}
+ private fun generateAddObserverFactory() =
+ FunSpec.builder("addObserverFactory").build {
+ addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
+ addParameters(
+ listOf(
+ ParameterSpec(
+ "sessionObserverFactory",
+ ClassName("androidx.privacysandbox.ui.core", "SessionObserverFactory")
+ )
+ )
+ )
+ addStatement(
+ "$sandboxedUiAdapterPropertyName.addObserverFactory(" + "sessionObserverFactory)"
+ )
+ }
+
+ private fun generateRemoveObserverFactory() =
+ FunSpec.builder("removeObserverFactory").build {
+ addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
+ addParameters(
+ listOf(
+ ParameterSpec(
+ "sessionObserverFactory",
+ ClassName("androidx.privacysandbox.ui.core", "SessionObserverFactory")
+ )
+ )
+ )
+ addStatement(
+ "$sandboxedUiAdapterPropertyName.removeObserverFactory(" + "sessionObserverFactory)"
+ )
+ }
+
private fun generateTransactionCallbackObject(method: Method) =
CodeBlock.builder().build {
val transactionCallbackClassName =
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
index 3a3f304..71d47a9 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
@@ -77,7 +77,7 @@
)
}
- private inner class BannerAdSession(private val adView: View) : SandboxedUiAdapter.Session {
+ private inner class BannerAdSession(private val adView: View) : AbstractSession() {
override val view: View
get() = adView
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index b5f2651..3e536c5 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -19,10 +19,16 @@
import android.content.Context
import android.os.Bundle
import android.os.Process
+import android.util.Log
import android.view.View
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.MediationOption
import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
@@ -33,6 +39,7 @@
class SdkApi(private val sdkContext: Context) : ISdkApi.Stub() {
private val testAdapters = TestAdapters(sdkContext)
+ private val measurementManager = MeasurementManager()
override fun loadBannerAd(
@AdType adType: Int,
@@ -73,7 +80,9 @@
}
private fun loadWebViewBannerAdFromLocalAssets(): Bundle {
- return testAdapters.WebViewAdFromLocalAssets().toCoreLibInfo(sdkContext)
+ val ad = testAdapters.WebViewAdFromLocalAssets()
+ measurementManager.startObserving(ad)
+ return ad.toCoreLibInfo(sdkContext)
}
private fun loadNonWebViewBannerAd(text: String, waitInsideOnDraw: Boolean): Bundle {
@@ -139,8 +148,39 @@
return null
}
+ class MeasurementManager {
+ fun startObserving(adapter: SandboxedUiAdapter) {
+ adapter.addObserverFactory(SessionObserverFactoryImpl())
+ }
+
+ private inner class SessionObserverFactoryImpl : SessionObserverFactory {
+
+ override fun create(): SessionObserver {
+ return SessionObserverImpl()
+ }
+
+ private inner class SessionObserverImpl : SessionObserver {
+
+ override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+ Log.i(TAG, "onSessionOpened $sessionObserverContext")
+ }
+
+ override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+ // TODO(b/330515740): Reflect this event in the app UI.
+ val sandboxedSdkViewUiInfo = SandboxedSdkViewUiInfo.fromBundle(uiContainerInfo)
+ Log.i(TAG, "onUiContainerChanged $sandboxedSdkViewUiInfo")
+ }
+
+ override fun onSessionClosed() {
+ Log.i(TAG, "session closed")
+ }
+ }
+ }
+ }
+
companion object {
private const val MEDIATEE_SDK =
"androidx.privacysandbox.ui.integration.mediateesdkprovider"
+ private const val TAG = "SdkApi"
}
}
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index fbf7583..da5ff2f 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -54,6 +54,7 @@
androidTestImplementation(libs.multidex)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation project(path: ':appcompat:appcompat')
+ androidTestImplementation project(":privacysandbox:ui:ui-provider")
}
android {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index eec0931..7eb9e7a 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -21,7 +21,9 @@
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Rect
+import android.os.Bundle
import android.os.IBinder
+import android.os.SystemClock
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
@@ -33,7 +35,9 @@
import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
import androidx.privacysandbox.ui.client.view.SandboxedSdkView
import androidx.privacysandbox.ui.core.BackwardCompatUtil
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -46,9 +50,11 @@
import androidx.test.uiautomator.Until
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import java.lang.Long.min
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import kotlin.Long.Companion.MAX_VALUE
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
@@ -64,8 +70,11 @@
companion object {
const val TIMEOUT = 1000.toLong()
+
// Longer timeout used for expensive operations like device rotation.
const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
+
+ const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
}
private lateinit var context: Context
@@ -78,7 +87,7 @@
@get:Rule var activityScenarioRule = ActivityScenarioRule(UiLibActivity::class.java)
- class FailingTestSandboxedUiAdapter : SandboxedUiAdapter {
+ class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
override fun openSession(
context: Context,
windowInputToken: IBinder,
@@ -92,7 +101,8 @@
}
}
- class TestSandboxedUiAdapter : SandboxedUiAdapter {
+ class TestSandboxedUiAdapter(private val signalOptions: Set<String> = setOf("option")) :
+ AbstractSandboxedUiAdapter() {
var isSessionOpened = false
var internalClient: SandboxedUiAdapter.SessionClient? = null
@@ -118,7 +128,7 @@
client: SandboxedUiAdapter.SessionClient
) {
internalClient = client
- testSession = TestSession(context, initialWidth, initialHeight)
+ testSession = TestSession(context, initialWidth, initialHeight, signalOptions)
if (!delayOpenSessionCallback) {
client.onSessionOpened(testSession!!)
}
@@ -148,9 +158,15 @@
context: Context,
initialWidth: Int,
initialHeight: Int,
+ override val signalOptions: Set<String>
) : SandboxedUiAdapter.Session {
var zOrderChangedLatch: CountDownLatch = CountDownLatch(1)
+ var shortestGapBetweenUiChangeEvents = MAX_VALUE
+ private var notifyUiChangedLatch: CountDownLatch = CountDownLatch(1)
+ private var latestUiChange: Bundle = Bundle()
+ private var hasReceivedFirstUiChange = false
+ private var timeReceivedLastUiChange = SystemClock.elapsedRealtime()
override val view: View = View(context)
@@ -176,6 +192,37 @@
}
override fun close() {}
+
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {
+ if (hasReceivedFirstUiChange) {
+ shortestGapBetweenUiChangeEvents =
+ min(
+ shortestGapBetweenUiChangeEvents,
+ SystemClock.elapsedRealtime() - timeReceivedLastUiChange
+ )
+ }
+ hasReceivedFirstUiChange = true
+ timeReceivedLastUiChange = SystemClock.elapsedRealtime()
+ latestUiChange = uiContainerInfo
+ notifyUiChangedLatch.countDown()
+ }
+
+ fun assertNoSubsequentUiChanges() {
+ notifyUiChangedLatch = CountDownLatch(1)
+ assertThat(notifyUiChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ }
+
+ /**
+ * Performs the action specified in the Runnable, and waits for the next UI change.
+ *
+ * Throws an [AssertionError] if no UI change is reported.
+ */
+ fun runAndRetrieveNextUiChange(runnable: Runnable): SandboxedSdkViewUiInfo {
+ notifyUiChangedLatch = CountDownLatch(1)
+ runnable.run()
+ assertThat(notifyUiChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ return SandboxedSdkViewUiInfo.fromBundle(latestUiChange)
+ }
}
}
@@ -590,12 +637,156 @@
)
}
- private fun addViewToLayout(waitToBeActive: Boolean = false) {
+ @Test
+ fun signalsOnlyCollectedWhenSignalOptionsNonEmpty() {
+ addViewToLayoutAndWaitToBeActive()
+ assertThat(view.signalMeasurer).isNotNull()
+ val adapter = TestSandboxedUiAdapter(setOf())
+ val view2 = SandboxedSdkView(context)
+ activityScenarioRule.withActivity { view2.setAdapter(adapter) }
+ addViewToLayoutAndWaitToBeActive(view2)
+ assertThat(view2.signalMeasurer).isNull()
+ }
+
+ @Test
+ fun signalsNotSentWhenViewUnchanged() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ session.runAndRetrieveNextUiChange {}
+ session.assertNoSubsequentUiChanges()
+ }
+
+ @Test
+ fun signalsSentWhenSizeChanges() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ val newWidth = 500
+ val newHeight = 500
+ val sandboxedSdkViewUiInfo =
+ session.runAndRetrieveNextUiChange {
+ activityScenarioRule.withActivity {
+ view.layoutParams = LinearLayout.LayoutParams(newWidth, newHeight)
+ }
+ }
+ assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(newWidth)
+ assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(newHeight)
+ assertThat(session.shortestGapBetweenUiChangeEvents)
+ .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+ }
+
+ /**
+ * Shifts the view partially off screen and verifies that the reported onScreenGeometry is
+ * cropped accordingly.
+ */
+ @Test
+ fun correctSignalsSentForOnScreenGeometryWhenViewOffScreen() {
+ val clippedWidth = 400
+ val clippedHeight = 500
+ activityScenarioRule.withActivity {
+ val layoutParams = findViewById<LinearLayout>(R.id.mainlayout).layoutParams
+ layoutParams.width = clippedWidth
+ layoutParams.height = clippedHeight
+ }
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ val initialHeight = view.height
+ val initialWidth = view.width
+ val xShiftDistance = 200f
+ val yShiftDistance = 300f
+ val sandboxedSdkViewUiInfo =
+ session.runAndRetrieveNextUiChange {
+ activityScenarioRule.withActivity {
+ view.y -= yShiftDistance
+ view.x -= xShiftDistance
+ }
+ }
+ assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(clippedWidth)
+ assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(clippedHeight)
+ assertThat(sandboxedSdkViewUiInfo.onScreenGeometry.height().toFloat())
+ .isEqualTo(initialHeight - yShiftDistance)
+ assertThat(sandboxedSdkViewUiInfo.onScreenGeometry.width().toFloat())
+ .isEqualTo(initialWidth - xShiftDistance)
+ }
+
+ @Test
+ fun signalsSentWhenPositionChanges() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ val newXPosition = 100f
+ val sandboxedSdkViewUiInfo =
+ session.runAndRetrieveNextUiChange {
+ activityScenarioRule.withActivity { view.x = newXPosition }
+ }
+ val containerWidth = sandboxedSdkViewUiInfo.uiContainerWidth
+ val onScreenWidth = sandboxedSdkViewUiInfo.onScreenGeometry.width().toFloat()
+ assertThat(containerWidth - newXPosition).isEqualTo(onScreenWidth)
+ }
+
+ @Test
+ fun signalsSentWhenAlphaChanges() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ // Catch initial UI change so that the subsequent alpha change will be reflected in the
+ // next SandboxedSdkViewUiInfo
+ session.runAndRetrieveNextUiChange {}
+ val newAlpha = 0.5f
+ val sandboxedSdkViewUiInfo =
+ session.runAndRetrieveNextUiChange {
+ activityScenarioRule.withActivity { view.alpha = newAlpha }
+ }
+ assertThat(sandboxedSdkViewUiInfo.uiContainerOpacityHint).isEqualTo(newAlpha)
+ }
+
+ /**
+ * Changes the size of the view several times in quick succession, and verifies that the signals
+ * sent match the width of the final change.
+ */
+ @Test
+ fun signalsSentAreFresh() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ var currentWidth = view.width
+ var currentHeight = view.height
+ val sandboxedSdkViewUiInfo =
+ session.runAndRetrieveNextUiChange {
+ activityScenarioRule.withActivity {
+ for (i in 1..5) {
+ view.layoutParams =
+ LinearLayout.LayoutParams(currentWidth + 10, currentHeight + 10)
+ currentWidth += 10
+ currentHeight += 10
+ }
+ }
+ }
+ assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(currentWidth)
+ assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(currentHeight)
+ }
+
+ /**
+ * Creates many UI changes and ensures that these changes are not sent more frequently than
+ * expected.
+ */
+ @Test
+ @SuppressLint("BanThreadSleep") // Deliberate delay for testing
+ fun signalsNotSentMoreFrequentlyThanLimit() {
+ addViewToLayoutAndWaitToBeActive()
+ val session = testSandboxedUiAdapter.testSession!!
+ for (i in 1..10) {
+ activityScenarioRule.withActivity {
+ view.layoutParams = LinearLayout.LayoutParams(view.width + 10, view.height + 10)
+ }
+ Thread.sleep(100)
+ }
+ assertThat(session.shortestGapBetweenUiChangeEvents)
+ .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+ }
+
+ private fun addViewToLayout(waitToBeActive: Boolean = false, viewToAdd: View = view) {
activityScenarioRule.withActivity {
val mainLayout: LinearLayout = findViewById(R.id.mainlayout)
mainLayoutWidth = mainLayout.width
mainLayoutHeight = mainLayout.height
- mainLayout.addView(view)
+ mainLayout.addView(viewToAdd)
}
if (waitToBeActive) {
val latch = CountDownLatch(1)
@@ -608,8 +799,8 @@
}
}
- private fun addViewToLayoutAndWaitToBeActive() {
- addViewToLayout(true)
+ private fun addViewToLayoutAndWaitToBeActive(viewToAdd: View = view) {
+ addViewToLayout(true, viewToAdd)
}
private fun requestSizeAndVerifyLayout(
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index 0eb7423..7504162 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -35,6 +35,7 @@
import androidx.privacysandbox.ui.core.IRemoteSessionController
import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
@@ -81,6 +82,7 @@
* [LocalAdapter] fetches UI from a provider living on same process as the client but on a
* different class loader.
*/
+ @SuppressLint("BanUncheckedReflection") // using reflection on library classes
private class LocalAdapter(adapterInterface: ISandboxedUiAdapter) : SandboxedUiAdapter {
private val uiProviderBinder = adapterInterface.asBinder()
@@ -111,7 +113,6 @@
targetSessionClientClass
)
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
override fun openSession(
context: Context,
windowInputToken: IBinder,
@@ -145,11 +146,14 @@
}
}
+ override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+ override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
private class SessionClientProxyHandler(
private val origClient: SandboxedUiAdapter.SessionClient,
) : InvocationHandler {
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any {
return when (method.name) {
"onSessionOpened" -> {
@@ -198,33 +202,39 @@
private val getViewMethod = targetClass.getMethod("getView")
private val notifyResizedMethod =
targetClass.getMethod("notifyResized", Int::class.java, Int::class.java)
+ private val getSignalOptionsMethod = targetClass.getMethod("getSignalOptions")
private val notifyZOrderChangedMethod =
targetClass.getMethod("notifyZOrderChanged", Boolean::class.java)
private val notifyConfigurationChangedMethod =
targetClass.getMethod("notifyConfigurationChanged", Configuration::class.java)
+ private val notifyUiChangedMethod =
+ targetClass.getMethod("notifyUiChanged", Bundle::class.java)
private val closeMethod = targetClass.getMethod("close")
override val view: View
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
get() = getViewMethod.invoke(origSession) as View
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+ override val signalOptions: Set<String>
+ @Suppress("UNCHECKED_CAST") // using reflection on library classes
+ get() = getSignalOptionsMethod.invoke(origSession) as Set<String>
+
override fun notifyResized(width: Int, height: Int) {
view.layout(0, 0, width, height)
notifyResizedMethod.invoke(origSession, width, height)
}
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
notifyZOrderChangedMethod.invoke(origSession, isZOrderOnTop)
}
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
override fun notifyConfigurationChanged(configuration: Configuration) {
notifyConfigurationChangedMethod.invoke(origSession, configuration)
}
- @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {
+ notifyUiChangedMethod.invoke(origSession, uiContainerInfo)
+ }
+
override fun close() {
closeMethod.invoke(origSession)
}
@@ -261,6 +271,10 @@
}
}
+ override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+ override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
class RemoteSessionClient(
val context: Context,
val client: SandboxedUiAdapter.SessionClient,
@@ -272,7 +286,8 @@
override fun onRemoteSessionOpened(
surfacePackage: SurfaceControlViewHost.SurfacePackage,
remoteSessionController: IRemoteSessionController,
- isZOrderOnTop: Boolean
+ isZOrderOnTop: Boolean,
+ hasObservers: Boolean
) {
surfaceView = SurfaceView(context)
surfaceView.setChildSurfacePackage(surfacePackage)
@@ -298,7 +313,12 @@
clientExecutor.execute {
client.onSessionOpened(
- SessionImpl(surfaceView, remoteSessionController, surfacePackage)
+ SessionImpl(
+ surfaceView,
+ remoteSessionController,
+ surfacePackage,
+ hasObservers
+ )
)
}
tryToCallRemoteObject {
@@ -324,11 +344,22 @@
private class SessionImpl(
val surfaceView: SurfaceView,
val remoteSessionController: IRemoteSessionController,
- val surfacePackage: SurfaceControlViewHost.SurfacePackage
+ val surfacePackage: SurfaceControlViewHost.SurfacePackage,
+ hasObservers: Boolean
) : SandboxedUiAdapter.Session {
override val view: View = surfaceView
+ // While there are no more refined signal options, just use hasObservers as a signal
+ // for whether to start measurement.
+ // TODO(b/341895747): Add structured signal options.
+ override val signalOptions =
+ if (hasObservers) {
+ setOf("someOptions")
+ } else {
+ setOf()
+ }
+
override fun notifyConfigurationChanged(configuration: Configuration) {
tryToCallRemoteObject {
remoteSessionController.notifyConfigurationChanged(configuration)
@@ -363,6 +394,10 @@
tryToCallRemoteObject { remoteSessionController.notifyZOrderChanged(isZOrderOnTop) }
}
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {
+ tryToCallRemoteObject { remoteSessionController.notifyUiChanged(uiContainerInfo) }
+ }
+
override fun close() {
tryToCallRemoteObject { remoteSessionController.close() }
}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index d566994..061bf98 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -106,6 +106,7 @@
override fun surfaceCreated(p0: SurfaceHolder) {
updateAndSetClippingBounds(true)
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutChangeListener)
+ viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
}
override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}
@@ -117,6 +118,9 @@
private val globalLayoutChangeListener =
ViewTreeObserver.OnGlobalLayoutListener { updateAndSetClippingBounds() }
+ private val scrollChangedListener =
+ ViewTreeObserver.OnScrollChangedListener { signalMeasurer?.maybeSendSignals() }
+
private var adapter: SandboxedUiAdapter? = null
private var client: Client? = null
private var isZOrderOnTop = true
@@ -131,6 +135,7 @@
internal val stateListenerManager: StateListenerManager = StateListenerManager()
private var viewContainingPoolingContainerListener: View? = null
private var poolingContainerListener = PoolingContainerListener {}
+ internal var signalMeasurer: SandboxedSdkViewSignalMeasurer? = null
/** Adds a state change listener to the UI session and immediately reports the current state. */
fun addStateChangedListener(stateChangedListener: SandboxedSdkUiSessionStateChangedListener) {
@@ -148,6 +153,7 @@
if (this.adapter === sandboxedUiAdapter) return
client?.close()
client = null
+ signalMeasurer = null
this.adapter = sandboxedUiAdapter
checkClientOpenSession()
}
@@ -225,7 +231,6 @@
}
private fun removeContentView() {
- removeCallbacks()
if (childCount == 1) {
super.removeViewAt(0)
}
@@ -234,6 +239,7 @@
private fun removeCallbacks() {
(contentView as? SurfaceView)?.holder?.removeCallback(surfaceChangedCallback)
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutChangeListener)
+ viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
}
internal fun setContentView(contentView: View) {
@@ -251,6 +257,7 @@
}
// Wait for the next frame commit before sending an ACTIVE state change to listeners.
+ // TODO(b/338196636): Unregister this when necessary.
CompatImpl.registerFrameCommitCallback(viewTreeObserver) {
stateListenerManager.currentUiSessionState = Active
}
@@ -262,6 +269,8 @@
internal fun onClientClosedSession(error: Throwable? = null) {
removeContentView()
+ signalMeasurer?.dropPendingUpdates()
+ signalMeasurer = null
stateListenerManager.currentUiSessionState =
if (error != null) {
SandboxedSdkUiSessionState.Error(error)
@@ -339,6 +348,12 @@
previousHeight = height
previousWidth = width
checkClientOpenSession()
+ signalMeasurer?.maybeSendSignals()
+ }
+
+ override fun setAlpha(alpha: Float) {
+ super.setAlpha(alpha)
+ signalMeasurer?.maybeSendSignals()
}
private fun closeClient() {
@@ -388,6 +403,7 @@
}
override fun onDetachedFromWindow() {
+ removeCallbacks()
if (!this.isWithinPoolingContainer) {
closeClient()
}
@@ -501,7 +517,8 @@
session.close()
return
}
- sandboxedSdkView?.setContentView(session.view)
+ val view = checkNotNull(sandboxedSdkView) { "SandboxedSdkView should not be null" }
+ view.setContentView(session.view)
this.session = session
val width = pendingWidth
val height = pendingHeight
@@ -512,6 +529,9 @@
pendingConfiguration = null
pendingZOrderOnTop?.let { session.notifyZOrderChanged(it) }
pendingZOrderOnTop = null
+ if (session.signalOptions.isNotEmpty()) {
+ view.signalMeasurer = SandboxedSdkViewSignalMeasurer(view, session)
+ }
}
override fun onSessionError(throwable: Throwable) {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt
new file mode 100644
index 0000000..57d201a
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.client.view
+
+import android.graphics.Rect
+import android.os.SystemClock
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+/**
+ * Class for calculating signals related to the presentation of a [SandboxedSdkView].
+ *
+ * This class also schedules the collection of signals to ensure that signals are not sent too
+ * frequently.
+ */
+internal class SandboxedSdkViewSignalMeasurer(
+ val view: SandboxedSdkView,
+ private val session: SandboxedUiAdapter.Session,
+ private val clock: Clock = Clock { SystemClock.uptimeMillis() }
+) {
+ private companion object {
+ private const val MIN_SIGNAL_LATENCY_MS = 200
+ }
+
+ internal fun interface Clock {
+ fun uptimeMillis(): Long
+ }
+
+ private val windowLocation = IntArray(2)
+ private var onScreenGeometry = Rect()
+ private var containerWidthPx = 0
+ private var containerHeightPx = 0
+ private var opacityHint = 1.0f
+ private var lastTimeSentSignals: Long = clock.uptimeMillis()
+ private var scheduledTask: Runnable? = null
+
+ /**
+ * Updates the [SandboxedSdkViewUiInfo] that represents the view if there is no task already
+ * scheduled and the time since the last time signals were sent is at least the minimum
+ * acceptable latency.
+ *
+ * TODO(b/333853853): Use concurrency constructs instead.
+ */
+ fun maybeSendSignals() {
+ if (scheduledTask != null) {
+ return
+ }
+
+ if ((clock.uptimeMillis() - lastTimeSentSignals) < MIN_SIGNAL_LATENCY_MS) {
+ val delayToNextSend =
+ MIN_SIGNAL_LATENCY_MS - (clock.uptimeMillis() - lastTimeSentSignals)
+ scheduledTask = Runnable {
+ scheduledTask = null
+ maybeSendSignals()
+ }
+ view.postDelayed(scheduledTask, delayToNextSend)
+ } else {
+ updateUiContainerInfo()
+ session.notifyUiChanged(
+ SandboxedSdkViewUiInfo.toBundle(
+ SandboxedSdkViewUiInfo(
+ containerWidthPx,
+ containerHeightPx,
+ onScreenGeometry,
+ opacityHint
+ )
+ )
+ )
+ lastTimeSentSignals = clock.uptimeMillis()
+ }
+ }
+
+ /** Removes the pending UI update [Runnable] from the message queue, if one exists. */
+ fun dropPendingUpdates() {
+ scheduledTask?.let { view.removeCallbacks(it) }
+ scheduledTask = null
+ }
+
+ /** Updates the [SandboxedSdkViewUiInfo] that represents the state of the view. */
+ private fun updateUiContainerInfo() {
+ val isVisible = view.getGlobalVisibleRect(onScreenGeometry)
+ if (!isVisible) {
+ onScreenGeometry.set(-1, -1, -1, -1)
+ } else {
+ view.getLocationOnScreen(windowLocation)
+ onScreenGeometry.offset(-windowLocation[0], -windowLocation[1])
+ onScreenGeometry.intersect(0, 0, view.width, view.height)
+ }
+ containerHeightPx = view.height
+ containerWidthPx = view.width
+ opacityHint = view.alpha
+ }
+}
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
index 5ab678d..aa33944 100644
--- a/privacysandbox/ui/ui-core/api/current.txt
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -1,16 +1,41 @@
// Signature format: 4.0
package androidx.privacysandbox.ui.core {
+ public final class SandboxedSdkViewUiInfo {
+ ctor public SandboxedSdkViewUiInfo(int uiContainerWidth, int uiContainerHeight, android.graphics.Rect onScreenGeometry, float uiContainerOpacityHint);
+ method public static androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+ method public android.graphics.Rect getOnScreenGeometry();
+ method public int getUiContainerHeight();
+ method public float getUiContainerOpacityHint();
+ method public int getUiContainerWidth();
+ method public static android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+ property public final android.graphics.Rect onScreenGeometry;
+ property public final int uiContainerHeight;
+ property public final float uiContainerOpacityHint;
+ property public final int uiContainerWidth;
+ field public static final androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo.Companion Companion;
+ }
+
+ public static final class SandboxedSdkViewUiInfo.Companion {
+ method public androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+ }
+
public interface SandboxedUiAdapter {
+ method public void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
method public void openSession(android.content.Context context, android.os.IBinder windowInputToken, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+ method public void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
}
public static interface SandboxedUiAdapter.Session extends java.lang.AutoCloseable {
method public void close();
+ method public java.util.Set<java.lang.String> getSignalOptions();
method public android.view.View getView();
method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
method public void notifyResized(int width, int height);
+ method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
method public void notifyZOrderChanged(boolean isZOrderOnTop);
+ property public abstract java.util.Set<java.lang.String> signalOptions;
property public abstract android.view.View view;
}
@@ -31,5 +56,21 @@
field public static final int apiVersion = 1; // 0x1
}
+ public interface SessionObserver {
+ method public void onSessionClosed();
+ method public void onSessionOpened(androidx.privacysandbox.ui.core.SessionObserverContext sessionObserverContext);
+ method public void onUiContainerChanged(android.os.Bundle uiContainerInfo);
+ }
+
+ public final class SessionObserverContext {
+ ctor public SessionObserverContext(android.view.View? view);
+ method public android.view.View? getView();
+ property public final android.view.View? view;
+ }
+
+ public fun interface SessionObserverFactory {
+ method public androidx.privacysandbox.ui.core.SessionObserver create();
+ }
+
}
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
index 5ab678d..aa33944 100644
--- a/privacysandbox/ui/ui-core/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -1,16 +1,41 @@
// Signature format: 4.0
package androidx.privacysandbox.ui.core {
+ public final class SandboxedSdkViewUiInfo {
+ ctor public SandboxedSdkViewUiInfo(int uiContainerWidth, int uiContainerHeight, android.graphics.Rect onScreenGeometry, float uiContainerOpacityHint);
+ method public static androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+ method public android.graphics.Rect getOnScreenGeometry();
+ method public int getUiContainerHeight();
+ method public float getUiContainerOpacityHint();
+ method public int getUiContainerWidth();
+ method public static android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+ property public final android.graphics.Rect onScreenGeometry;
+ property public final int uiContainerHeight;
+ property public final float uiContainerOpacityHint;
+ property public final int uiContainerWidth;
+ field public static final androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo.Companion Companion;
+ }
+
+ public static final class SandboxedSdkViewUiInfo.Companion {
+ method public androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+ }
+
public interface SandboxedUiAdapter {
+ method public void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
method public void openSession(android.content.Context context, android.os.IBinder windowInputToken, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+ method public void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
}
public static interface SandboxedUiAdapter.Session extends java.lang.AutoCloseable {
method public void close();
+ method public java.util.Set<java.lang.String> getSignalOptions();
method public android.view.View getView();
method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
method public void notifyResized(int width, int height);
+ method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
method public void notifyZOrderChanged(boolean isZOrderOnTop);
+ property public abstract java.util.Set<java.lang.String> signalOptions;
property public abstract android.view.View view;
}
@@ -31,5 +56,21 @@
field public static final int apiVersion = 1; // 0x1
}
+ public interface SessionObserver {
+ method public void onSessionClosed();
+ method public void onSessionOpened(androidx.privacysandbox.ui.core.SessionObserverContext sessionObserverContext);
+ method public void onUiContainerChanged(android.os.Bundle uiContainerInfo);
+ }
+
+ public final class SessionObserverContext {
+ ctor public SessionObserverContext(android.view.View? view);
+ method public android.view.View? getView();
+ property public final android.view.View? view;
+ }
+
+ public fun interface SessionObserverFactory {
+ method public androidx.privacysandbox.ui.core.SessionObserver create();
+ }
+
}
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
index 9ea0d9d..e6df2ad 100644
--- a/privacysandbox/ui/ui-core/build.gradle
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -39,6 +39,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
+ implementation("androidx.core:core:1.13.0")
}
android {
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
index a28c0a7..bd085ca 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
@@ -22,7 +22,8 @@
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
oneway interface IRemoteSessionClient {
void onRemoteSessionOpened(in SurfacePackage surfacePackage,
- IRemoteSessionController remoteSessionController, boolean isZOrderOnTop);
+ IRemoteSessionController remoteSessionController,
+ boolean isZOrderOnTop, boolean hasObservers);
void onRemoteSessionError(String exception);
void onResizeRequested(int width, int height);
void onSessionUiFetched(in SurfacePackage surfacePackage);
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
index c685b2b..704c474 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -23,4 +23,5 @@
void notifyResized(int width, int height);
void notifyZOrderChanged(boolean isZOrderOnTop);
void notifyFetchUiForSession();
+ void notifyUiChanged(in Bundle uiContainerInfo);
}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt
new file mode 100644
index 0000000..8aa2449
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.graphics.Rect
+import android.os.Bundle
+import androidx.core.os.BundleCompat
+
+/** A class representing the UI state of a SandboxedSdkView. */
+class SandboxedSdkViewUiInfo(
+ /** Returns the width of the UI container in pixels. */
+ val uiContainerWidth: Int,
+ /** Returns the height of the UI container in pixels. */
+ val uiContainerHeight: Int,
+ /**
+ * Returns the portion of the UI container which is not clipped by parent views and is visible
+ * on screen. The coordinates of this [Rect] are relative to the UI container and measured in
+ * pixels.
+ *
+ * If none of the UI container is visible on screen, each coordinate in this [Rect] will be -1.
+ */
+ val onScreenGeometry: Rect,
+ /**
+ * Returns the opacity of the UI container, where available.
+ *
+ * When available, this is a value from 0 to 1, where 0 means the container is completely
+ * transparent and 1 means the container is completely opaque. This value doesn't necessarily
+ * reflect the user-visible opacity of the UI container, as shaders and other overlays can
+ * affect that.
+ *
+ * When the opacity is not available, the value will be -1.
+ */
+ val uiContainerOpacityHint: Float
+) {
+ companion object {
+ private const val UI_CONTAINER_WIDTH_KEY = "uiContainerWidth"
+ private const val UI_CONTAINER_HEIGHT_KEY = "uiContainerHeight"
+ private const val ONSCREEN_GEOMETRY_KEY = "onScreenGeometry"
+ private const val UI_CONTAINER_OPACITY_KEY = "uiContainerOpacity"
+
+ @JvmStatic
+ fun fromBundle(bundle: Bundle): SandboxedSdkViewUiInfo {
+ val uiContainerWidth = bundle.getInt(UI_CONTAINER_WIDTH_KEY)
+ val uiContainerHeight = bundle.getInt(UI_CONTAINER_HEIGHT_KEY)
+ val onScreenGeometry =
+ checkNotNull(
+ BundleCompat.getParcelable(bundle, ONSCREEN_GEOMETRY_KEY, Rect::class.java)
+ )
+ val uiContainerOpacityHint = bundle.getFloat(UI_CONTAINER_OPACITY_KEY)
+ return SandboxedSdkViewUiInfo(
+ uiContainerWidth,
+ uiContainerHeight,
+ onScreenGeometry,
+ uiContainerOpacityHint
+ )
+ }
+
+ @JvmStatic
+ fun toBundle(sandboxedSdkViewUiInfo: SandboxedSdkViewUiInfo): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(UI_CONTAINER_WIDTH_KEY, sandboxedSdkViewUiInfo.uiContainerWidth)
+ bundle.putInt(UI_CONTAINER_HEIGHT_KEY, sandboxedSdkViewUiInfo.uiContainerHeight)
+ bundle.putParcelable(ONSCREEN_GEOMETRY_KEY, sandboxedSdkViewUiInfo.onScreenGeometry)
+ bundle.putFloat(UI_CONTAINER_OPACITY_KEY, sandboxedSdkViewUiInfo.uiContainerOpacityHint)
+ return bundle
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SandboxedSdkViewUiInfo) return false
+
+ return onScreenGeometry == other.onScreenGeometry &&
+ uiContainerWidth == other.uiContainerWidth &&
+ uiContainerHeight == other.uiContainerHeight &&
+ uiContainerOpacityHint == other.uiContainerOpacityHint
+ }
+
+ override fun hashCode(): Int {
+ var result = uiContainerWidth
+ result = 31 * result + uiContainerHeight
+ result = 31 * result + onScreenGeometry.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SandboxedSdkViewUiInfo(" +
+ "uiContainerWidth=$uiContainerWidth, " +
+ "uiContainerHeight=$uiContainerHeight, " +
+ "onScreenGeometry=$onScreenGeometry," +
+ "uiContainerOpacityHint=$uiContainerOpacityHint"
+ }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
index d312c56..ea86d40 100644
--- a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.content.res.Configuration
+import android.os.Bundle
import android.os.IBinder
import android.view.View
import java.lang.AutoCloseable
@@ -45,6 +46,31 @@
client: SessionClient
)
+ /**
+ * Adds a [SessionObserverFactory] with a [SandboxedUiAdapter] for tracking UI presentation
+ * state across UI sessions. This has no effect on already open sessions.
+ *
+ * For each [SandboxedUiAdapter.Session] that is created for the adapter after registration is
+ * complete, [SessionObserverFactory.create] will be invoked to allow a new [SessionObserver]
+ * instance to be attached to the UI session. This [SessionObserver] will receive UI updates for
+ * the lifetime of the session. There may be one or more UI sessions created for a
+ * [SandboxedUiAdapter], and a separate [SessionObserverFactory.create] call will be made for
+ * each one.
+ */
+ fun addObserverFactory(sessionObserverFactory: SessionObserverFactory)
+
+ /**
+ * Removes a [SessionObserverFactory] from a [SandboxedUiAdapter], if it has been previously
+ * added with [addObserverFactory].
+ *
+ * If the [SessionObserverFactory] was not previously added, no action is performed. Any
+ * existing [SessionObserver] instances that have been created by the [SessionObserverFactory]
+ * will continue to receive updates until their corresponding [SandboxedUiAdapter.Session] has
+ * been closed. For any subsequent sessions created for the [SandboxedUiAdapter], no call to
+ * [SessionObserverFactory.create] will be made.
+ */
+ fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory)
+
/** A single session with the provider of remote content. */
interface Session : AutoCloseable {
@@ -56,6 +82,16 @@
val view: View
/**
+ * The set of options that will be used to determine what information is calculated and sent
+ * to [SessionObserver]s attached to this session.
+ *
+ * This value should not be directly set by UI providers. Instead, the registration of any
+ * [SessionObserverFactory] with [addObserverFactory] will indicate that information should
+ * be calculated for this session.
+ */
+ val signalOptions: Set<String>
+
+ /**
* Notify the provider that the size of the host presentation area has changed to a size of
* [width] x [height] pixels.
*/
@@ -71,6 +107,19 @@
fun notifyConfigurationChanged(configuration: Configuration)
/**
+ * Notify the session when the presentation state of its UI container has changed.
+ *
+ * [uiContainerInfo] contains a Bundle that represents the state of the container. The exact
+ * details of this Bundle depend on the container this Bundle is describing. This
+ * notification is not in real time and is throttled, so it should not be used to react to
+ * UI changes on the client side.
+ *
+ * UI providers should use [addObserverFactory] to observe UI changes rather than using this
+ * method.
+ */
+ fun notifyUiChanged(uiContainerInfo: Bundle)
+
+ /**
* Close this session, indicating that the remote provider of content should dispose of
* associated resources and that the [SessionClient] should not receive further callback
* events.
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt
new file mode 100644
index 0000000..6348aa2
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.os.Bundle
+
+/**
+ * An interface that can be used by the client of a
+ * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] to receive updates about its state.
+ * One session may be associated with multiple session observers.
+ *
+ * When a new UI session is started, [onSessionOpened] will be invoked for all registered observers.
+ * This callback will contain a [SessionObserverContext] object containing data which will be
+ * constant for the lifetime of the UI session. [onUiContainerChanged] will also be sent when a new
+ * session is opened. This callback will contain a [Bundle] representing the UI presentation of the
+ * session's view.
+ *
+ * During the entire lifetime of the UI session, [onUiContainerChanged] will be sent when the UI
+ * presentation has changed. These updates will be throttled.
+ *
+ * When the UI session has completed, [onSessionClosed] will be sent. After this point, no more
+ * callbacks will be sent and it is safe to free any resources associated with this session
+ * observer.
+ */
+@SuppressWarnings("CallbackName")
+interface SessionObserver {
+
+ /**
+ * Called exactly once per session, when the
+ * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] associated with the session
+ * observer is created. [sessionObserverContext] contains data which will be constant for the
+ * lifetime of the UI session. The resources associated with the [sessionObserverContext] will
+ * be released when [onSessionClosed] is called.
+ */
+ fun onSessionOpened(sessionObserverContext: SessionObserverContext)
+
+ /**
+ * Called when the UI container has changed its presentation state. Note that these updates will
+ * not be sent instantaneously, and will be throttled. This should not be used to react to UI
+ * changes on the client side as it is not sent in real time.
+ */
+ // TODO(b/326942993): Decide on the correct data type to send.
+ fun onUiContainerChanged(uiContainerInfo: android.os.Bundle)
+
+ /**
+ * Called exactly once per session when the
+ * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] associated with this session
+ * observer closes. No more callbacks will be made on this observer after this point, and any
+ * resources associated with it can be freed.
+ */
+ fun onSessionClosed()
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt
new file mode 100644
index 0000000..99389c7
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.privacysandbox.ui.core
+
+import android.view.View
+
+/**
+ * A class containing information that will be constant through the lifetime of a [SessionObserver].
+ *
+ * When [SessionObserver.onSessionClosed] is called for the associated session observers, the
+ * resources of the [SessionObserverContext] will be freed.
+ */
+class SessionObserverContext(
+ /**
+ * Returns the view that is presenting content for the associated [SandboxedUiAdapter.Session].
+ *
+ * This value will be non-null if the [SandboxedUiAdapter.Session] and the [SessionObserver] are
+ * created from the same process. Otherwise, it will be null.
+ */
+ val view: View?
+) {
+ override fun toString() = "SessionObserverContext(view=$view)"
+
+ override fun equals(other: Any?): Boolean {
+ return other is SessionObserverContext && view == other.view
+ }
+
+ override fun hashCode(): Int {
+ return view.hashCode()
+ }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt
new file mode 100644
index 0000000..6597769
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.privacysandbox.ui.core
+
+/**
+ * A factory that creates [SessionObserver] instances that can be attached to a
+ * [SandboxedUiAdapter.Session]. Many [SessionObserver]s may be created for the same
+ * [SandboxedUiAdapter.Session].
+ */
+fun interface SessionObserverFactory {
+ /**
+ * Called if a new [SandboxedUiAdapter.Session] has been opened by the [SandboxedUiAdapter] that
+ * this factory is registered to. This will not be called for sessions that are already open.
+ */
+ fun create(): SessionObserver
+}
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
index b1deb894..53933f1 100644
--- a/privacysandbox/ui/ui-provider/api/current.txt
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -3,13 +3,21 @@
public abstract class AbstractSandboxedUiAdapter implements androidx.privacysandbox.ui.core.SandboxedUiAdapter {
ctor public AbstractSandboxedUiAdapter();
+ method public final void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+ method public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> getSessionObserverFactories();
+ method public final void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+ property public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> sessionObserverFactories;
}
public abstract static class AbstractSandboxedUiAdapter.AbstractSession implements androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session {
ctor public AbstractSandboxedUiAdapter.AbstractSession();
+ method public void close();
+ method public final java.util.Set<java.lang.String> getSignalOptions();
method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
method public void notifyResized(int width, int height);
+ method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
method public void notifyZOrderChanged(boolean isZOrderOnTop);
+ property public final java.util.Set<java.lang.String> signalOptions;
}
public final class SandboxedUiAdapterProxy {
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
index b1deb894..53933f1 100644
--- a/privacysandbox/ui/ui-provider/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -3,13 +3,21 @@
public abstract class AbstractSandboxedUiAdapter implements androidx.privacysandbox.ui.core.SandboxedUiAdapter {
ctor public AbstractSandboxedUiAdapter();
+ method public final void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+ method public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> getSessionObserverFactories();
+ method public final void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+ property public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> sessionObserverFactories;
}
public abstract static class AbstractSandboxedUiAdapter.AbstractSession implements androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session {
ctor public AbstractSandboxedUiAdapter.AbstractSession();
+ method public void close();
+ method public final java.util.Set<java.lang.String> getSignalOptions();
method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
method public void notifyResized(int width, int height);
+ method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
method public void notifyZOrderChanged(boolean isZOrderOnTop);
+ property public final java.util.Set<java.lang.String> signalOptions;
}
public final class SandboxedUiAdapterProxy {
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
index 168541f..acf6d521 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
@@ -17,7 +17,9 @@
package androidx.privacysandbox.ui.provider
import android.content.res.Configuration
+import android.os.Bundle
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserverFactory
/**
* An abstract class that implements [SandboxedUiAdapter] while abstracting away methods that do not
@@ -27,6 +29,28 @@
*/
abstract class AbstractSandboxedUiAdapter : SandboxedUiAdapter {
+ /** The list of [SessionObserverFactory] instances that have been added to this adapter. */
+ val sessionObserverFactories: List<SessionObserverFactory>
+ get() {
+ synchronized(_sessionObserverFactories) {
+ return _sessionObserverFactories.toList()
+ }
+ }
+
+ private val _sessionObserverFactories: MutableList<SessionObserverFactory> = mutableListOf()
+
+ final override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ synchronized(_sessionObserverFactories) {
+ _sessionObserverFactories.add(sessionObserverFactory)
+ }
+ }
+
+ final override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+ synchronized(_sessionObserverFactories) {
+ _sessionObserverFactories.remove(sessionObserverFactory)
+ }
+ }
+
/**
* An abstract class that implements [SandboxedUiAdapter.Session] so that a UI provider does not
* need to implement the entire interface.
@@ -35,10 +59,17 @@
*/
abstract class AbstractSession : SandboxedUiAdapter.Session {
+ final override val signalOptions: Set<String>
+ get() = setOf()
+
override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {}
override fun notifyResized(width: Int, height: Int) {}
override fun notifyConfigurationChanged(configuration: Configuration) {}
+
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {}
+
+ override fun close() {}
}
}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index 91f074e..a993e74 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -28,6 +28,7 @@
import android.util.Log
import android.view.Display
import android.view.SurfaceControlViewHost
+import android.view.View
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
@@ -35,6 +36,9 @@
import androidx.privacysandbox.ui.core.IRemoteSessionController
import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import java.util.concurrent.Executor
/**
@@ -76,10 +80,14 @@
initialHeight,
isZOrderOnTop,
clientExecutor,
- client
+ SessionClientForObservers(client)
)
}
+ override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+ override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun openRemoteSession(
windowInputToken: IBinder,
@@ -183,7 +191,8 @@
remoteSessionClient.onRemoteSessionOpened(
surfacePackage,
remoteSessionController,
- isZOrderOnTop
+ isZOrderOnTop,
+ session.signalOptions.isNotEmpty()
)
}
@@ -196,11 +205,10 @@
@VisibleForTesting
private inner class RemoteSessionController(
val surfaceControlViewHost: SurfaceControlViewHost,
- val session: SandboxedUiAdapter.Session
+ val session: SandboxedUiAdapter.Session,
) : IRemoteSessionController.Stub() {
override fun notifyConfigurationChanged(configuration: Configuration) {
- surfaceControlViewHost.surfacePackage?.notifyConfigurationChanged(configuration)
session.notifyConfigurationChanged(configuration)
}
@@ -220,6 +228,10 @@
sendSurfacePackage()
}
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {
+ session.notifyUiChanged(uiContainerInfo)
+ }
+
override fun close() {
val mHandler = Handler(Looper.getMainLooper())
mHandler.post {
@@ -231,6 +243,59 @@
}
/**
+ * Wrapper class to handle the creation of [SessionObserver] instances when the session is
+ * opened.
+ */
+ private inner class SessionClientForObservers(val client: SandboxedUiAdapter.SessionClient) :
+ SandboxedUiAdapter.SessionClient by client {
+
+ override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+ val sessionObservers: MutableList<SessionObserver> = mutableListOf()
+ if (adapter is AbstractSandboxedUiAdapter) {
+ adapter.sessionObserverFactories.forEach { sessionObservers.add(it.create()) }
+ }
+ client.onSessionOpened(SessionForObservers(session, sessionObservers))
+ }
+ }
+
+ /**
+ * Wrapper class of a [SandboxedUiAdapter.Session] that handles the sending of events to any
+ * [SessionObserver]s attached to the session.
+ */
+ private class SessionForObservers(
+ val session: SandboxedUiAdapter.Session,
+ val sessionObservers: List<SessionObserver>
+ ) : SandboxedUiAdapter.Session by session {
+
+ init {
+ if (sessionObservers.isNotEmpty()) {
+ val sessionObserverContext = SessionObserverContext(view)
+ sessionObservers.forEach { it.onSessionOpened(sessionObserverContext) }
+ }
+ }
+
+ override val view: View
+ get() = session.view
+
+ override val signalOptions: Set<String>
+ get() =
+ if (sessionObservers.isEmpty()) {
+ setOf()
+ } else {
+ setOf("someOptions")
+ }
+
+ override fun notifyUiChanged(uiContainerInfo: Bundle) {
+ sessionObservers.forEach { it.onUiContainerChanged(uiContainerInfo) }
+ }
+
+ override fun close() {
+ session.close()
+ sessionObservers.forEach { it.onSessionClosed() }
+ }
+ }
+
+ /**
* Provides backward compat support for APIs.
*
* If the API is available, it's called from a version-specific static inner class gated with
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index b4cf11b..7e17123 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -322,7 +322,9 @@
// Verify toString, hashCode and equals have been implemented for dynamic proxy
val testSession = sdkAdapter.session as TestSandboxedUiAdapter.TestSession
val client = testSession.sessionClient
- assertThat(client.toString()).isEqualTo(testSessionClient.toString())
+
+ // TODO(b/329468679): We cannot assert this as we wrap the client on the provider side.
+ // assertThat(client.toString()).isEqualTo(testSessionClient.toString())
assertThat(client.equals(client)).isTrue()
assertThat(client).isNotEqualTo(testSessionClient)
@@ -349,6 +351,133 @@
session.assertViewWasLaidOut()
}
+ @Test
+ fun testAddSessionObserverFactory_ObserverIsCreated() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ }
+
+ @Test
+ fun testAddSessionObserverFactory_OnSessionOpenedIsSent() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ val sessionObserver = factory.sessionObservers[0]
+ sessionObserver.assertSessionOpened()
+ }
+
+ @Test
+ fun testAddSessionObserverFactory_NoObserverCreatedForAlreadyOpenSession() {
+ val adapter = sessionManager.createAdapterAndWaitToBeActive(viewForSession = view)
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ adapter.addObserverFactory(factory)
+ factory.assertNoSessionsAreCreated()
+ }
+
+ @Test
+ fun testAddSessionObserverFactory_MultipleFactories() {
+ val factory1 = TestSessionManager.SessionObserverFactoryImpl()
+ val factory2 = TestSessionManager.SessionObserverFactoryImpl()
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory1, factory2)
+ )
+ assertThat(factory1.sessionObservers.size).isEqualTo(1)
+ assertThat(factory2.sessionObservers.size).isEqualTo(1)
+ }
+
+ @Test
+ fun testAddSessionObserverFactory_SessionObserverContextIsCorrect() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ val adapter =
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ val sessionObserver = factory.sessionObservers[0]
+ sessionObserver.assertSessionOpened()
+ assertThat(sessionObserver.sessionObserverContext).isNotNull()
+ assertThat(sessionObserver.sessionObserverContext?.view).isEqualTo(adapter.session.view)
+ }
+
+ @Test
+ fun testRegisterSessionObserverFactory_OnUiContainerChangedSentWhenSessionOpened() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ val sessionObserver = factory.sessionObservers[0]
+ sessionObserver.assertOnUiContainerChangedSent()
+ }
+
+ @Test
+ fun testRemoveSessionObserverFactory_DoesNotImpactExistingObservers() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ val adapter =
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ adapter.removeObserverFactory(factory)
+ val sessionObserver = factory.sessionObservers[0]
+ // Setting a new adapter on the SandboxedSdKView will cause the current session to close.
+ activityScenarioRule.withActivity { view.setAdapter(TestSandboxedUiAdapter()) }
+ // onSessionClosed is still sent for the observer
+ sessionObserver.assertSessionClosed()
+ }
+
+ @Test
+ fun testRemoveSessionObserverFactory_DoesNotCreateObserverForNewSession() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ val adapter =
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ adapter.removeObserverFactory(factory)
+ val sandboxedSdkView2 = SandboxedSdkView(context)
+ activityScenarioRule.withActivity { linearLayout.addView(sandboxedSdkView2) }
+ // create a new session and wait to be active
+ sandboxedSdkView2.setAdapter(adapter)
+
+ val activeLatch = CountDownLatch(1)
+ sandboxedSdkView2.addStateChangedListener { state ->
+ if (state is SandboxedSdkUiSessionState.Active) {
+ activeLatch.countDown()
+ }
+ }
+ assertThat(activeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ // The session observers size should remain 1, showing that no new observers have been
+ // created for the new session.
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ }
+
+ @Test
+ fun testSessionObserver_OnClosedSentWhenSessionClosed() {
+ val factory = TestSessionManager.SessionObserverFactoryImpl()
+ sessionManager.createAdapterAndWaitToBeActive(
+ viewForSession = view,
+ sessionObserverFactories = listOf(factory)
+ )
+ assertThat(factory.sessionObservers.size).isEqualTo(1)
+ val sessionObserver = factory.sessionObservers[0]
+ // Setting a new adapter on the SandboxedSdKView will cause the current session to close.
+ activityScenarioRule.withActivity { view.setAdapter(TestSandboxedUiAdapter()) }
+ sessionObserver.assertSessionClosed()
+ }
+
private fun injectInputEventOnView() {
activityScenarioRule.withActivity {
val location = IntArray(2)
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
index 7f08d8c..85868f9 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
@@ -29,6 +29,9 @@
import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
import androidx.privacysandbox.ui.client.view.SandboxedSdkView
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
import androidx.privacysandbox.ui.provider.toCoreLibInfo
import androidx.privacysandbox.ui.tests.endtoend.IntegrationTests
@@ -60,10 +63,12 @@
hasFailingTestSession: Boolean = false,
placeViewInsideFrameLayout: Boolean = false,
viewForSession: SandboxedSdkView?,
- testSessionClient: TestSessionClient = TestSessionClient()
+ testSessionClient: TestSessionClient = TestSessionClient(),
+ sessionObserverFactories: List<SessionObserverFactory>? = null
): TestSandboxedUiAdapter {
val adapter = TestSandboxedUiAdapter(hasFailingTestSession, placeViewInsideFrameLayout)
+ sessionObserverFactories?.forEach { adapter.addObserverFactory(it) }
val adapterFromCoreLibInfo =
SandboxedUiAdapterFactory.createFromCoreLibInfo(getCoreLibInfoFromAdapter(adapter))
if (viewForSession != null) {
@@ -94,14 +99,16 @@
fun createAdapterAndWaitToBeActive(
initialZOrder: Boolean = true,
viewForSession: SandboxedSdkView,
- placeViewInsideFrameLayout: Boolean = false
+ placeViewInsideFrameLayout: Boolean = false,
+ sessionObserverFactories: List<SessionObserverFactory>? = null,
): TestSandboxedUiAdapter {
viewForSession.orderProviderUiAboveClientUi(initialZOrder)
val adapter =
createAdapterAndEstablishSession(
placeViewInsideFrameLayout = placeViewInsideFrameLayout,
- viewForSession = viewForSession
+ viewForSession = viewForSession,
+ sessionObserverFactories = sessionObserverFactories
)
val activeLatch = CountDownLatch(1)
@@ -166,8 +173,8 @@
*/
inner class FailingTestSession(
private val context: Context,
- private val sessionClient: SandboxedUiAdapter.SessionClient
- ) : SandboxedUiAdapter.Session {
+ sessionClient: SandboxedUiAdapter.SessionClient
+ ) : TestSession(context, sessionClient) {
override val view: View
get() {
sessionClient.onSessionError(Throwable("Test Session Exception"))
@@ -183,7 +190,7 @@
override fun close() {}
}
- inner class TestSession(
+ open inner class TestSession(
private val context: Context,
val sessionClient: SandboxedUiAdapter.SessionClient,
private val placeViewInsideFrameLayout: Boolean = false
@@ -246,14 +253,13 @@
}
}
- override val view: View
- get() {
- return if (placeViewInsideFrameLayout) {
- FrameLayout(context).also { it.addView(testView) }
- } else {
- testView
- }
+ override val view: View by lazy {
+ if (placeViewInsideFrameLayout) {
+ FrameLayout(context).also { it.addView(testView) }
+ } else {
+ testView
}
+ }
override fun notifyResized(width: Int, height: Int) {
resizedWidth = width
@@ -328,6 +334,54 @@
}
}
+ class SessionObserverFactoryImpl : SessionObserverFactory {
+ val sessionObservers: MutableList<SessionObserverImpl> = mutableListOf()
+ private val sessionObserverCreatedLatch = CountDownLatch(1)
+
+ override fun create(): SessionObserver {
+ sessionObserverCreatedLatch.countDown()
+ val sessionObserver = SessionObserverImpl()
+ sessionObservers.add(sessionObserver)
+ return sessionObserver
+ }
+
+ fun assertNoSessionsAreCreated() {
+ assertThat(sessionObserverCreatedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ }
+ }
+
+ class SessionObserverImpl : SessionObserver {
+ var sessionObserverContext: SessionObserverContext? = null
+ private val sessionOpenedLatch = CountDownLatch(1)
+ private val sessionClosedLatch = CountDownLatch(1)
+ private val uiContainerChangedLatch = CountDownLatch(1)
+
+ override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+ this.sessionObserverContext = sessionObserverContext
+ sessionOpenedLatch.countDown()
+ }
+
+ override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+ uiContainerChangedLatch.countDown()
+ }
+
+ override fun onSessionClosed() {
+ sessionClosedLatch.countDown()
+ }
+
+ fun assertSessionOpened() {
+ assertThat(sessionOpenedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ fun assertOnUiContainerChangedSent() {
+ assertThat(uiContainerChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ fun assertSessionClosed() {
+ assertThat(sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ }
+ }
+
fun getCoreLibInfoFromAdapter(sdkAdapter: SandboxedUiAdapter): Bundle {
val bundle = sdkAdapter.toCoreLibInfo(context)
bundle.putBoolean(TEST_ONLY_USE_REMOTE_ADAPTER, !invokeBackwardsCompatFlow)
diff --git a/security/security-state/src/androidTest/AndroidManifest.xml b/security/security-state/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..4ce03fb
--- /dev/null
+++ b/security/security-state/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+ 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
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ </application>
+
+</manifest>
diff --git a/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
new file mode 100644
index 0000000..35e8762
--- /dev/null
+++ b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.state
+
+import android.content.Context
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+class SecurityStateManagerTest {
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var securityStateManager: SecurityStateManager
+
+ @Before
+ fun setup() {
+ securityStateManager = SecurityStateManager(context)
+ }
+
+ @Test
+ fun testGetGlobalSecurityState() {
+ val bundle = securityStateManager.getGlobalSecurityState()
+
+ // Check if dates are in the format YYYY-MM-DD
+ val dateRegex = "^\\d{4}-\\d{2}-\\d{2}$"
+ assertTrue(bundle.getString("system_spl")!!.matches(dateRegex.toRegex()))
+ assertTrue(bundle.getString("vendor_spl")!!.matches(dateRegex.toRegex()))
+
+ // Check if kernel version is in the format X.X.XX
+ val versionRegex = "^\\d+\\.\\d+\\.\\d+$"
+ assertTrue(bundle.getString("kernel_version")!!.matches(versionRegex.toRegex()))
+ }
+}
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 341e497..4f8a68f 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -417,7 +417,7 @@
)
}
- val completeUrl = "$serverUrl/v1/AndroidSDK$androidSdk.json"
+ val completeUrl = "$serverUrl/v1/android_sdk_$androidSdk.json"
return Uri.parse(completeUrl)
}
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt b/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
index f388332..88e75d8 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
@@ -21,11 +21,11 @@
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
+import android.system.Os
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.webkit.WebViewCompat
-import java.io.File
import java.util.regex.Pattern
/**
@@ -195,7 +195,7 @@
}
/**
- * Attempts to retrieve the kernel version of the device from the system's '/proc/version' file.
+ * Attempts to retrieve the kernel version of the device using the system's uname() command.
* This method is used to determine the current kernel version which is a part of the security
* state assessment.
*
@@ -204,7 +204,7 @@
*/
private fun getKernelVersion(): String {
try {
- val matcher = kernelReleasePattern.matcher(File("/proc/version").readText())
+ val matcher = kernelReleasePattern.matcher(Os.uname().release)
return if (matcher.matches()) matcher.group(1)!! else ""
} catch (e: Exception) {
return ""
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index ae083f4..ba62223 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -203,7 +203,7 @@
fun testGetVulnerabilityReportUrl_validSdkVersion_returnsCorrectUrl() {
val sdkVersion = 34 // Android 14
val baseUrl = SecurityPatchState.DEFAULT_VULNERABILITY_REPORTS_URL
- val expectedUrl = "$baseUrl/v1/AndroidSDK34.json"
+ val expectedUrl = "$baseUrl/v1/android_sdk_34.json"
doReturn(sdkVersion).`when`(mockSecurityStateManager).getAndroidSdkInt()
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
similarity index 98%
rename from security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt
rename to security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
index 9df96fa..19c2f1d 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
@@ -34,7 +34,7 @@
import org.robolectric.annotation.Config
@RunWith(JUnit4::class)
-class SecurityPatchStateManagerTest {
+class SecurityStateManagerTest {
private val packageManager: PackageManager = mock<PackageManager>()
private val context: Context = mock<Context>() { on { packageManager } doReturn packageManager }
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index c98f15b..78025bf 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -32,7 +32,7 @@
}
dependencies {
- api project(':compose:foundation:foundation')
+ api("androidx.compose.foundation:foundation:1.6.0")
api("androidx.compose.ui:ui:1.6.0")
api("androidx.compose.ui:ui-text:1.6.0")
api("androidx.compose.runtime:runtime:1.6.0")
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
index 0057e26..b25dc9d 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
@@ -26,7 +26,6 @@
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -35,13 +34,11 @@
import androidx.compose.runtime.setValue
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
@@ -945,58 +942,6 @@
}
}
}
-
- @Test
- fun scalingLazyColumnIsFocusedByDefault() {
- var focusSet = false
-
- rule.setContent {
- ScalingLazyColumn(
- modifier =
- Modifier.testTag("scalingLazyColumn").onFocusChanged {
- focusSet = it.isFocused
- },
- ) {
- items(100) { BasicText("item $it") }
- }
- }
-
- assert(focusSet)
- }
-
- @Test
- fun scalingLazyColumnIsNotFocused_withDisabledRotary() {
- var focusSet = false
-
- rule.setContent {
- ScalingLazyColumn(
- modifier =
- Modifier.testTag("scalingLazyColumn").onFocusChanged {
- focusSet = it.isFocused
- },
- // Disable rotary and focus as well
- rotaryScrollableBehavior = null,
- ) {
- items(100) { BasicText("item $it") }
- }
- }
-
- assert(!focusSet)
- }
-
- @Test
- fun scalingLazyColumnIsFocusedByDefault_withSemantics() {
-
- rule.setContent {
- ScalingLazyColumn(
- modifier = Modifier.testTag("scalingLazyColumn"),
- ) {
- items(100) { BasicText("item $it") }
- }
- }
-
- rule.onNodeWithTag("scalingLazyColumn").assertIsFocused()
- }
}
internal const val TestTouchSlop = 18f
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt
deleted file mode 100644
index 4b22c73..0000000
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.wear.compose.foundation.rotary
-
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.focus.FocusTargetModifierNode
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.focused
-import androidx.compose.ui.semantics.requestFocus
-
-/**
- * FocusTarget modifier with Semantics node. Uses underlying focusTarget node with additional
- * semantics node.
- */
-@Stable internal fun Modifier.focusTargetWithSemantics() = this then FocusTargetWithSemanticsElement
-
-private object FocusTargetWithSemanticsElement :
- ModifierNodeElement<FocusTargetWithSemanticsNode>() {
-
- override fun create() = FocusTargetWithSemanticsNode()
-
- override fun update(node: FocusTargetWithSemanticsNode) {}
-
- override fun InspectorInfo.inspectableProperties() {
- name = "focusTargetWithSemanticsElement"
- }
-
- override fun hashCode() = "focusTargetWithSemanticsElement".hashCode()
-
- override fun equals(other: Any?) = other === this
-}
-
-internal class FocusTargetWithSemanticsNode() : DelegatingNode(), SemanticsModifierNode {
-
- private val focusTargetNode =
- delegate(FocusTargetModifierNode(onFocusChange = ::onFocusStateChange))
-
- private var requestFocus: (() -> Boolean)? = null
-
- private fun onFocusStateChange(previousState: FocusState, currentState: FocusState) {
- if (!isAttached) return
- if (currentState.isFocused != previousState.isFocused) invalidateSemantics()
- }
-
- override fun SemanticsPropertyReceiver.applySemantics() {
- focused = focusTargetNode.focusState.isFocused
- if (requestFocus == null) {
- requestFocus = { focusTargetNode.requestFocus() }
- }
- requestFocus(action = requestFocus)
- }
-}
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 ada6885..98e459b 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
@@ -35,6 +35,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.input.rotary.RotaryInputModifierNode
import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.node.ModifierNodeElement
@@ -106,7 +107,7 @@
reverseDirection = reverseDirection,
)
.focusRequester(focusRequester)
- .focusTargetWithSemantics()
+ .focusTarget()
/**
* An interface for handling scroll events. Has implementations for handling scroll with/without
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
new file mode 100644
index 0000000..83ed677
--- /dev/null
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.rememberScalingLazyListState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Benchmark for Wear Compose ScalingLazyColumn. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ScalingLazyColumnBenchmark {
+
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val scalingLazyColumnCaseFactory = { ScalingLazyColumnTestCase() }
+
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(scalingLazyColumnCaseFactory)
+ }
+
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(scalingLazyColumnCaseFactory)
+ }
+
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(scalingLazyColumnCaseFactory)
+ }
+
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(scalingLazyColumnCaseFactory)
+ }
+
+ @Test
+ fun layout() {
+ benchmarkRule.benchmarkLayoutPerf(scalingLazyColumnCaseFactory)
+ }
+
+ @Test
+ fun draw() {
+ benchmarkRule.benchmarkDrawPerf(scalingLazyColumnCaseFactory)
+ }
+}
+
+@Suppress("DEPRECATION")
+internal class ScalingLazyColumnTestCase : LayeredComposeTestCase() {
+ private var itemSizeDp: Dp = 10.dp
+ private var defaultItemSpacingDp: Dp = 4.dp
+
+ @Composable
+ override fun MeasuredContent() {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState(),
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+ ) {
+ items(10) { it -> Box(Modifier.requiredSize(itemSizeDp)) { Text(text = "Item $it") } }
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index af527de..bbe70aa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -30,8 +30,6 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
@@ -52,7 +50,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
@@ -60,6 +57,7 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.tokens.MotionTokens
import androidx.wear.compose.material3.tokens.RadioButtonTokens
+import androidx.wear.compose.material3.tokens.ShapeTokens
import androidx.wear.compose.material3.tokens.SplitRadioButtonTokens
import androidx.wear.compose.materialcore.animateSelectionColor
@@ -263,7 +261,7 @@
secondaryLabel: @Composable (RowScope.() -> Unit)? = null,
label: @Composable RowScope.() -> Unit
) {
- val (startPadding, endPadding) = contentPadding.splitHorizontally()
+ val containerColor = colors.containerColor(enabled, selected).value
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -273,7 +271,6 @@
.height(IntrinsicSize.Min)
.width(IntrinsicSize.Max)
.clip(shape = shape)
- .background(colors.containerColor(enabled, selected).value)
) {
Row(
modifier =
@@ -286,7 +283,9 @@
)
.semantics { role = Role.Button }
.fillMaxHeight()
- .then(startPadding)
+ .clip(SPLIT_SECTIONS_SHAPE)
+ .background(containerColor)
+ .padding(contentPadding)
.weight(1.0f),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -305,9 +304,10 @@
content = secondaryLabel
),
)
- Spacer(modifier = Modifier.size(SELECTION_CONTROL_SPACING))
}
+ Spacer(modifier = Modifier.size(2.dp))
+
val splitContainerColor =
colors.splitContainerColor(enabled = enabled, selected = selected).value
Box(
@@ -320,6 +320,8 @@
interactionSource = selectionInteractionSource
)
.fillMaxHeight()
+ .clip(SPLIT_SECTIONS_SHAPE)
+ .background(containerColor)
.drawWithCache {
onDrawWithContent {
drawRect(color = splitContainerColor)
@@ -329,8 +331,7 @@
.align(Alignment.CenterVertically)
.width(SPLIT_WIDTH)
.wrapContentHeight(align = Alignment.CenterVertically)
- .wrapContentWidth(align = Alignment.End)
- .then(endPadding)
+ .padding(contentPadding)
.semantics {
// For a selectable button, the role is always RadioButton.
// See also b/330869742 for issue with setting the SelectableButton role
@@ -1334,21 +1335,6 @@
}
}
-@Composable
-private fun PaddingValues.splitHorizontally() =
- Modifier.padding(
- start = calculateStartPadding(LocalLayoutDirection.current),
- end = 0.dp,
- top = calculateTopPadding(),
- bottom = calculateBottomPadding()
- ) to
- Modifier.padding(
- start = 0.dp,
- end = calculateEndPadding(layoutDirection = LocalLayoutDirection.current),
- top = calculateTopPadding(),
- bottom = calculateBottomPadding()
- )
-
private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
private val SELECTION_CONTROL_WIDTH = 32.dp
@@ -1357,5 +1343,6 @@
private val ICON_SPACING = 6.dp
private val MIN_HEIGHT = 52.dp
private val SPLIT_WIDTH = 52.dp
-private val CONTROL_WIDTH = 32.dp
+private val CONTROL_WIDTH = 24.dp
private val CONTROL_HEIGHT = 24.dp
+private val SPLIT_SECTIONS_SHAPE = ShapeTokens.CornerExtraSmall
diff --git a/wear/protolayout/protolayout-material-core/build.gradle b/wear/protolayout/protolayout-material-core/build.gradle
index ca1740a..dcf081a 100644
--- a/wear/protolayout/protolayout-material-core/build.gradle
+++ b/wear/protolayout/protolayout-material-core/build.gradle
@@ -26,7 +26,6 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("kotlin-android")
}
android {
@@ -56,7 +55,6 @@
testImplementation(libs.testRunner)
testImplementation(libs.testRules)
testImplementation(libs.truth)
- testImplementation("androidx.core:core-ktx:1.13.1")
}
androidx {
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java
deleted file mode 100644
index 98351ea..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.wear.protolayout.materialcore.fontscaling;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.Arrays;
-
-/**
- * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
- * "dp" dimension according to a non-linear curve.
- *
- * <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
- * slowly than smaller fonts, so we don't get ridiculously huge fonts that don't fit on the screen.
- *
- * <p>The thinking here is that large fonts are already big enough to read, but we still want to
- * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
- */
-// This is copied from
-// https://cs.android.com/android/_/android/platform/frameworks/base/+/2a4e99a798cc69944f64d54b81aee987fbea45d6:core/java/android/content/res/FontScaleConverter.java
-// TODO: b/342359552 - Use Android Platform api instead when it becomes public.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FontScaleConverter {
-
- final float[] mFromSpValues;
- final float[] mToDpValues;
-
- /**
- * Creates a lookup table for the given conversions.
- *
- * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
- *
- * <p>The arrays must be sorted ascending and monotonically increasing.
- *
- * @param fromSp array of dimensions in SP
- * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
- * @throws IllegalArgumentException if the array lengths don't match or are empty
- */
- FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
- if (fromSp.length != toDp.length || fromSp.length == 0) {
- throw new IllegalArgumentException("Array lengths must match and be nonzero");
- }
-
- mFromSpValues = fromSp;
- mToDpValues = toDp;
- }
-
- /** Convert a dimension in "dp" back to "sp" using the lookup table. */
- public float convertDpToSp(float dp) {
- return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
- }
-
- /** Convert a dimension in "sp" to "dp" using the lookup table. */
- public float convertSpToDp(float sp) {
- return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
- }
-
- private static float lookupAndInterpolate(
- float sourceValue, float[] sourceValues, float[] targetValues) {
- final float sourceValuePositive = Math.abs(sourceValue);
- // TODO(b/247861374): find a match at a higher index?
- final float sign = Math.signum(sourceValue);
- // We search for exact matches only, even if it's just a little off. The interpolation will
- // handle any non-exact matches.
- final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
- if (index >= 0) {
- // exact match, return the matching dp
- return sign * targetValues[index];
- } else {
- // must be a value in between index and index + 1: interpolate.
- final int lowerIndex = -(index + 1) - 1;
-
- final float startSp;
- final float endSp;
- final float startDp;
- final float endDp;
-
- if (lowerIndex >= sourceValues.length - 1) {
- // It's past our lookup table. Determine the last elements' scaling factor and use.
- startSp = sourceValues[sourceValues.length - 1];
- startDp = targetValues[sourceValues.length - 1];
-
- if (startSp == 0) {
- return 0;
- }
-
- final float scalingFactor = startDp / startSp;
- return sourceValue * scalingFactor;
- } else if (lowerIndex == -1) {
- // It's smaller than the smallest value in our table. Interpolate from 0.
- startSp = 0;
- startDp = 0;
- endSp = sourceValues[0];
- endDp = targetValues[0];
- } else {
- startSp = sourceValues[lowerIndex];
- endSp = sourceValues[lowerIndex + 1];
- startDp = targetValues[lowerIndex];
- endDp = targetValues[lowerIndex + 1];
- }
-
- return sign
- * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null) {
- return false;
- }
- if (!(o instanceof FontScaleConverter)) {
- return false;
- }
- FontScaleConverter that = (FontScaleConverter) o;
- return Arrays.equals(mFromSpValues, that.mFromSpValues)
- && Arrays.equals(mToDpValues, that.mToDpValues);
- }
-
- @Override
- public int hashCode() {
- int result = Arrays.hashCode(mFromSpValues);
- result = 31 * result + Arrays.hashCode(mToDpValues);
- return result;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "FontScaleConverter{"
- + "fromSpValues="
- + Arrays.toString(mFromSpValues)
- + ", toDpValues="
- + Arrays.toString(mToDpValues)
- + '}';
- }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java
deleted file mode 100644
index 5b8b3f7..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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.wear.protolayout.materialcore.fontscaling;
-
-import android.content.res.Configuration;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-
-/** Stores lookup tables for creating {@link FontScaleConverter}s at various scales. */
-// This is copied from
-// https://cs.android.com/android/_/android/platform/frameworks/base/+/2a4e99a798cc69944f64d54b81aee987fbea45d6:core/java/android/content/res/FontScaleConverterFactory.java
-// TODO: b/342359552 - Use Android Platform api instead when it becomes public.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FontScaleConverterFactory {
- private static final float SCALE_KEY_MULTIPLIER = 100f;
-
- @VisibleForTesting
- static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();
-
- @SuppressWarnings("NonFinalStaticField")
- private static float sMinScaleBeforeCurvesApplied = 1.05f;
-
- static {
- // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
- // manually tweaked for optimum readability.
- put(
- /* scaleKey= */ 1.15f,
- new FontScaleConverter(
- /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
- /* toDp= */ new float[] {
- 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f, 30f, 100
- }));
-
- put(
- /* scaleKey= */ 1.3f,
- new FontScaleConverter(
- /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
- /* toDp= */ new float[] {
- 10.4f, 13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f, 30f, 100
- }));
-
- put(
- /* scaleKey= */ 1.5f,
- new FontScaleConverter(
- /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
- /* toDp= */ new float[] {12f, 15f, 18f, 22f, 24f, 26f, 28f, 30f, 100}));
-
- put(
- /* scaleKey= */ 1.8f,
- new FontScaleConverter(
- /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
- /* toDp= */ new float[] {
- 14.4f, 18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f, 100
- }));
-
- put(
- /* scaleKey= */ 2f,
- new FontScaleConverter(
- /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
- /* toDp= */ new float[] {16f, 20f, 24f, 26f, 30f, 34f, 36f, 38f, 100}));
-
- sMinScaleBeforeCurvesApplied = getScaleFromKey(LOOKUP_TABLES.keyAt(0)) - 0.02f;
- if (sMinScaleBeforeCurvesApplied <= 1.0f) {
- throw new IllegalStateException(
- "You should only apply non-linear scaling to font scales > 1");
- }
- }
-
- private FontScaleConverterFactory() {}
-
- /**
- * Returns true if non-linear font scaling curves would be in effect for the given scale, false
- * if the scaling would follow a linear curve or for no scaling.
- *
- * <p>Example usage: <code>
- * isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code>
- */
- public static boolean isNonLinearFontScalingActive(float fontScale) {
- return fontScale >= sMinScaleBeforeCurvesApplied;
- }
-
- /**
- * Finds a matching FontScaleConverter for the given fontScale factor.
- *
- * @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
- * @return a converter for the given scale, or null if non-linear scaling should not be used.
- */
- @Nullable
- public static FontScaleConverter forScale(float fontScale) {
- if (!isNonLinearFontScalingActive(fontScale)) {
- return null;
- }
-
- FontScaleConverter lookupTable = get(fontScale);
- if (lookupTable != null) {
- return lookupTable;
- }
-
- // Didn't find an exact match: interpolate between two existing tables
- final int index = LOOKUP_TABLES.indexOfKey(getKey(fontScale));
- if (index >= 0) {
- // This should never happen, should have been covered by get() above.
- return LOOKUP_TABLES.valueAt(index);
- }
- // Didn't find an exact match: interpolate between two existing tables
- final int lowerIndex = -(index + 1) - 1;
- final int higherIndex = lowerIndex + 1;
- if (lowerIndex < 0 || higherIndex >= LOOKUP_TABLES.size()) {
- // We have gone beyond our bounds and have nothing to interpolate between. Just give
- // them a straight linear table instead.
- // This works because when FontScaleConverter encounters a size beyond its bounds, it
- // calculates a linear fontScale factor using the ratio of the last element pair.
- return new FontScaleConverter(new float[] {1f}, new float[] {fontScale});
- } else {
- float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex));
- float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex));
- float interpolationPoint =
- MathUtils.constrainedMap(
- /* rangeMin= */ 0f,
- /* rangeMax= */ 1f,
- startScale,
- endScale,
- fontScale);
- return createInterpolatedTableBetween(
- LOOKUP_TABLES.valueAt(lowerIndex),
- LOOKUP_TABLES.valueAt(higherIndex),
- interpolationPoint);
- }
- }
-
- @NonNull
- private static FontScaleConverter createInterpolatedTableBetween(
- FontScaleConverter start, FontScaleConverter end, float interpolationPoint) {
- float[] commonSpSizes = new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f};
- float[] dpInterpolated = new float[commonSpSizes.length];
-
- for (int i = 0; i < commonSpSizes.length; i++) {
- float sp = commonSpSizes[i];
- float startDp = start.convertSpToDp(sp);
- float endDp = end.convertSpToDp(sp);
- dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint);
- }
-
- return new FontScaleConverter(commonSpSizes, dpInterpolated);
- }
-
- private static int getKey(float fontScale) {
- return (int) (fontScale * SCALE_KEY_MULTIPLIER);
- }
-
- private static float getScaleFromKey(int key) {
- return (float) key / SCALE_KEY_MULTIPLIER;
- }
-
- private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
- LOOKUP_TABLES.put(getKey(scaleKey), fontScaleConverter);
- }
-
- @Nullable
- private static FontScaleConverter get(float scaleKey) {
- return LOOKUP_TABLES.get(getKey(scaleKey));
- }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java
deleted file mode 100644
index 5773d03..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.wear.protolayout.materialcore.fontscaling;
-
-import static java.lang.Math.min;
-
-/** A class that contains utility methods related to numbers. */
-final class MathUtils {
- private MathUtils() {}
-
- /**
- * Returns the linear interpolation of {@code amount} between {@code start} and {@code stop}.
- */
- static float lerp(float start, float stop, float amount) {
- return start + (stop - start) * amount;
- }
-
- /**
- * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
- * #lerp}{@code (a, b, s)}
- *
- * <p>If {@code a == b}, then this function will return 0.
- */
- static float lerpInv(float a, float b, float value) {
- return a != b ? ((value - a) / (b - a)) : 0.0f;
- }
-
- /** Returns the single argument constrained between [0.0, 1.0]. */
- static float saturate(float value) {
- return value < 0.0f ? 0.0f : min(1.0f, value);
- }
-
- /** Returns the saturated (constrained between [0, 1]) result of {@link #lerpInv}. */
- static float lerpInvSat(float a, float b, float value) {
- return saturate(lerpInv(a, b, value));
- }
-
- /**
- * Calculates a value in [rangeMin, rangeMax] that maps value in [valueMin, valueMax] to
- * returnVal in [rangeMin, rangeMax].
- *
- * <p>Always returns a constrained value in the range [rangeMin, rangeMax], even if value is
- * outside [valueMin, valueMax].
- *
- * <p>Eg: constrainedMap(0f, 100f, 0f, 1f, 0.5f) = 50f constrainedMap(20f, 200f, 10f, 20f, 20f)
- * = 200f constrainedMap(20f, 200f, 10f, 20f, 50f) = 200f constrainedMap(10f, 50f, 10f, 20f, 5f)
- * = 10f
- *
- * @param rangeMin minimum of the range that should be returned.
- * @param rangeMax maximum of the range that should be returned.
- * @param valueMin minimum of range to map {@code value} to.
- * @param valueMax maximum of range to map {@code value} to.
- * @param value to map to the range [{@code valueMin}, {@code valueMax}]. Note, can be outside
- * this range, resulting in a clamped value.
- * @return the mapped value, constrained to [{@code rangeMin}, {@code rangeMax}.
- */
- static float constrainedMap(
- float rangeMin, float rangeMax, float valueMin, float valueMax, float value) {
- return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value));
- }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt b/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt
deleted file mode 100644
index 841825b..0000000
--- a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * 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.wear.protolayout.materialcore.fontscaling
-
-import androidx.core.util.forEach
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.math.ceil
-import kotlin.math.floor
-import kotlin.random.Random.Default.nextFloat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FontScaleConverterFactoryTest {
-
- @Test
- fun scale200IsTwiceAtSmallSizes() {
- val table = FontScaleConverterFactory.forScale(2F)!!
- assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
- assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
- assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
- assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
- assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
- }
-
- @LargeTest
- @Test
- fun missingLookupTablePastEnd_returnsLinear() {
- val table = FontScaleConverterFactory.forScale(3F)!!
- generateSequenceOfFractions(-10000f..10000f, step = 0.01f).map {
- assertThat(table.convertSpToDp(it)).isWithin(CONVERSION_TOLERANCE).of(it * 3f)
- }
- assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(3f)
- assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(24f)
- assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(30f)
- assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(15f)
- assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
- assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(150f)
- assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(300f)
- }
-
- @SmallTest
- fun missingLookupTable110_returnsInterpolated() {
- val table = FontScaleConverterFactory.forScale(1.1F)!!
-
- assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.1f)
- assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.1f)
- assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(11f)
- assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.1f)
- assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
- assertThat(table.convertSpToDp(50F)).isLessThan(50f * 1.1f)
- assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.1f)
- }
-
- @Test
- fun missingLookupTable199_returnsInterpolated() {
- val table = FontScaleConverterFactory.forScale(1.9999F)!!
- assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
- assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
- assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
- assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
- assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
- }
-
- @Test
- fun missingLookupTable160_returnsInterpolated() {
- val table = FontScaleConverterFactory.forScale(1.6F)!!
- assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f * 1.6F)
- assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.6F)
- assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f * 1.6F)
- assertThat(table.convertSpToDp(20F)).isLessThan(20f * 1.6F)
- assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.6F)
- assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.6F)
- assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
- }
-
- @SmallTest
- fun missingLookupTableNegativeReturnsNull() {
- assertThat(FontScaleConverterFactory.forScale(-1F)).isNull()
- }
-
- @SmallTest
- fun unnecessaryFontScalesReturnsNull() {
- assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
- assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
- assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
- }
-
- @SmallTest
- fun tablesMatchAndAreMonotonicallyIncreasing() {
- FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable ->
- assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size)
- assertThat(lookupTable.mToDpValues).isNotEmpty()
-
- assertThat(lookupTable.mFromSpValues.asList()).isInStrictOrder()
- assertThat(lookupTable.mToDpValues.asList()).isInStrictOrder()
-
- assertThat(lookupTable.mFromSpValues.asList()).containsNoDuplicates()
- assertThat(lookupTable.mToDpValues.asList()).containsNoDuplicates()
- }
- }
-
- @SmallTest
- fun testIsNonLinearFontScalingActive() {
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)).isTrue()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.5f)).isTrue()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(2f)).isTrue()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(3f)).isTrue()
- }
-
- @LargeTest
- @Test
- fun allFeasibleScalesAndConversionsDoNotCrash() {
- generateSequenceOfFractions(-10f..10f, step = 0.1f)
- .fuzzFractions()
- .mapNotNull { FontScaleConverterFactory.forScale(it) }
- .flatMap { table ->
- generateSequenceOfFractions(-2000f..2000f, step = 0.1f).fuzzFractions().map {
- Pair(table, it)
- }
- }
- .forEach { (table, sp) ->
- try {
- // Truth is slow because it creates a bunch of
- // objects. Don't use it unless we need to.
- if (!table.convertSpToDp(sp).isFinite()) {
- assertWithMessage("convertSpToDp(%s) on table: %s", sp, table)
- .that(table.convertSpToDp(sp))
- .isFinite()
- }
- } catch (e: Exception) {
- throw AssertionError("Exception during convertSpToDp($sp) on table: $table", e)
- }
- }
- }
-
- @Test
- fun testGenerateSequenceOfFractions() {
- val fractions = generateSequenceOfFractions(-1000f..1000f, step = 0.1f).toList()
- fractions.forEach {
- assertThat(it).isAtLeast(-1000f)
- assertThat(it).isAtMost(1000f)
- }
-
- assertThat(fractions).isInStrictOrder()
- assertThat(fractions).hasSize(1000 * 2 * 10 + 1) // Don't forget the 0 in the middle!
-
- assertThat(fractions).contains(100f)
- assertThat(fractions).contains(500.1f)
- assertThat(fractions).contains(500.2f)
- assertThat(fractions).contains(0.2f)
- assertThat(fractions).contains(0f)
- assertThat(fractions).contains(-10f)
- assertThat(fractions).contains(-10f)
- assertThat(fractions).contains(-10.3f)
-
- assertThat(fractions).doesNotContain(-10.31f)
- assertThat(fractions).doesNotContain(0.35f)
- assertThat(fractions).doesNotContain(0.31f)
- assertThat(fractions).doesNotContain(-.35f)
- }
-
- @Test
- fun testFuzzFractions() {
- val numFuzzedFractions = 6
- val fractions =
- generateSequenceOfFractions(-1000f..1000f, step = 0.1f).fuzzFractions().toList()
- fractions.forEach {
- assertThat(it).isAtLeast(-1000f)
- assertThat(it).isLessThan(1001f)
- }
-
- val numGeneratedFractions = 1000 * 2 * 10 + 1 // Don't forget the 0 in the middle!
- assertThat(fractions).hasSize(numGeneratedFractions * numFuzzedFractions)
-
- assertThat(fractions).contains(100f)
- assertThat(fractions).contains(500.1f)
- assertThat(fractions).contains(500.2f)
- assertThat(fractions).contains(0.2f)
- assertThat(fractions).contains(0f)
- assertThat(fractions).contains(-10f)
- assertThat(fractions).contains(-10f)
- assertThat(fractions).contains(-10.3f)
- }
-
- companion object {
- private const val CONVERSION_TOLERANCE = 0.05f
- }
-}
-
-fun generateSequenceOfFractions(
- range: ClosedFloatingPointRange<Float>,
- step: Float
-): Sequence<Float> {
- val multiplier = 1f / step
- val start = floor(range.start * multiplier).toInt()
- val endInclusive = ceil(range.endInclusive * multiplier).toInt()
- return generateSequence(start) { it + 1 }
- .takeWhile { it <= endInclusive }
- .map { it.toFloat() / multiplier }
-}
-
-private fun Sequence<Float>.fuzzFractions(): Sequence<Float> {
- return flatMap { i ->
- listOf(i, i + 0.01f, i + 0.054f, i + 0.099f, i + nextFloat(), i + nextFloat())
- }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt b/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt
deleted file mode 100644
index 7dc342a..0000000
--- a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.wear.protolayout.materialcore.fontscaling
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FontScaleConverterTest {
-
- @Test
- fun straightInterpolation() {
- val table = createTable(8f to 8f, 10f to 10f, 20f to 20f)
- verifyConversionBothWays(table, 1f, 1F)
- verifyConversionBothWays(table, 8f, 8F)
- verifyConversionBothWays(table, 10f, 10F)
- verifyConversionBothWays(table, 30f, 30F)
- verifyConversionBothWays(table, 20f, 20F)
- verifyConversionBothWays(table, 5f, 5F)
- verifyConversionBothWays(table, 0f, 0F)
- }
-
- @Test
- fun interpolate200Percent() {
- val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
- verifyConversionBothWays(table, 2f, 1F)
- verifyConversionBothWays(table, 16f, 8F)
- verifyConversionBothWays(table, 20f, 10F)
- verifyConversionBothWays(table, 60f, 30F)
- verifyConversionBothWays(table, 40f, 20F)
- verifyConversionBothWays(table, 10f, 5F)
- verifyConversionBothWays(table, 0f, 0F)
- }
-
- @Test
- fun interpolate150Percent() {
- val table = createTable(2f to 3f, 10f to 15f, 20f to 30f, 100f to 150f)
- verifyConversionBothWays(table, 3f, 2F)
- verifyConversionBothWays(table, 1.5f, 1F)
- verifyConversionBothWays(table, 12f, 8F)
- verifyConversionBothWays(table, 15f, 10F)
- verifyConversionBothWays(table, 30f, 20F)
- verifyConversionBothWays(table, 75f, 50F)
- verifyConversionBothWays(table, 7.5f, 5F)
- verifyConversionBothWays(table, 0f, 0F)
- }
-
- @Test
- fun pastEndsUsesLastScalingFactor() {
- val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
- verifyConversionBothWays(table, 200f, 100F)
- verifyConversionBothWays(table, 62f, 31F)
- verifyConversionBothWays(table, 2000f, 1000F)
- verifyConversionBothWays(table, 4000f, 2000F)
- verifyConversionBothWays(table, 20000f, 10000F)
- }
-
- @Test
- fun negativeSpIsNegativeDp() {
- val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
- verifyConversionBothWays(table, -2f, -1F)
- verifyConversionBothWays(table, -16f, -8F)
- verifyConversionBothWays(table, -20f, -10F)
- verifyConversionBothWays(table, -60f, -30F)
- verifyConversionBothWays(table, -40f, -20F)
- verifyConversionBothWays(table, -10f, -5F)
- verifyConversionBothWays(table, 0f, -0F)
- }
-
- private fun createTable(vararg pairs: Pair<Float, Float>) =
- FontScaleConverter(
- pairs.map { it.first }.toFloatArray(),
- pairs.map { it.second }.toFloatArray()
- )
-
- private fun verifyConversionBothWays(
- table: FontScaleConverter,
- expectedDp: Float,
- spToConvert: Float
- ) {
- assertWithMessage("convertSpToDp")
- .that(table.convertSpToDp(spToConvert))
- .isWithin(CONVERSION_TOLERANCE)
- .of(expectedDp)
-
- assertWithMessage("inverse: convertDpToSp")
- .that(table.convertDpToSp(expectedDp))
- .isWithin(CONVERSION_TOLERANCE)
- .of(spToConvert)
- }
-
- companion object {
- private const val CONVERSION_TOLERANCE = 0.05f
- }
-}
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index b1d7de5..be951af 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -49,7 +49,6 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation("androidx.core:core:1.7.0")
androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(libs.testUiautomator)
androidTestImplementation(project(":wear:protolayout:protolayout-renderer"))
testImplementation(libs.junit)
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
index c25e2f9..61734b9 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -16,13 +16,11 @@
package androidx.wear.protolayout.material;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
-import static androidx.wear.protolayout.material.RunnerUtils.getFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
-import static androidx.wear.protolayout.material.RunnerUtils.setAndAssertFontScale;
-import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
import static androidx.wear.protolayout.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
@@ -36,13 +34,10 @@
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
-import androidx.test.uiautomator.UiDevice;
import androidx.wear.protolayout.DeviceParametersBuilders;
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.protolayout.material.RunnerUtils.TestCase;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -55,9 +50,14 @@
@RunWith(Parameterized.class)
@LargeTest
public class MaterialGoldenXLTest {
- private static final float FONT_SCALE_XXXL = 1.24f;
+ /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+ tests together, first all parametrization (data()) methods are called, and then individual
+ tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+ each test. */
+ private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+ private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
- private static float originalFontScale;
+ private static final float FONT_SCALE_XXXL = 1.24f;
private final TestCase mTestCase;
private final String mExpected;
@@ -66,13 +66,6 @@
public AndroidXScreenshotTestRule mScreenshotRule =
new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
- @BeforeClass
- public static void setUpClass() throws Exception {
- setAndAssertFontScale(
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
- FONT_SCALE_XXXL);
- }
-
public MaterialGoldenXLTest(String expected, TestCase testCase) {
mTestCase = testCase;
mExpected = expected;
@@ -83,18 +76,27 @@
return (int) ((px - 0.5f) / scale);
}
+ @SuppressWarnings("deprecation")
@Parameterized.Parameters(name = "{0}")
- public static Collection<Object[]> data() throws Exception {
- // These "parameters" methods are called before any parameterized test (from any class)
- // executes. We set and later reset the font here to have the correct context during test
- // generation. We later set and reset the font for the actual test in BeforeClass/AfterClass
- // methods.
- UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- originalFontScale = getFontScale(device);
- setAndAssertFontScale(device, FONT_SCALE_XXXL);
-
+ public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ currentDisplayMetrics.setTo(displayMetrics);
+ displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
float scale = displayMetrics.density;
DeviceParameters deviceParameters =
@@ -102,7 +104,6 @@
.setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
.setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
.setScreenDensity(displayMetrics.density)
- .setFontScale(context.getResources().getConfiguration().fontScale)
// Not important for components.
.setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
.build();
@@ -125,17 +126,31 @@
/* isForLtr= */ true));
// Restore state before this method, so other test have correct context.
- setAndAssertFontScale(device, originalFontScale);
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+
waitForNotificationToDisappears();
return testCaseList;
}
- @AfterClass
- public static void tearDownClass() throws Exception {
- setFontScale(
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
- originalFontScale);
+ @Parameterized.BeforeParam
+ public static void restoreBefore() {
+ OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+ getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+ }
+
+ @Parameterized.AfterParam
+ public static void restoreAfter() {
+ getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
}
@Test
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
index 5ba7a7b..0fe42d3 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
@@ -16,8 +16,6 @@
package androidx.wear.protolayout.material;
-import static org.junit.Assert.assertEquals;
-
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
@@ -30,13 +28,11 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
import androidx.test.screenshot.matchers.MSSIMMatcher;
-import androidx.test.uiautomator.UiDevice;
import androidx.wear.protolayout.LayoutElementBuilders.Layout;
import androidx.wear.protolayout.material.test.GoldenTestActivity;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.stream.Collectors;
public class RunnerUtils {
@@ -157,23 +153,4 @@
this.isForLtr = isForLtr;
}
}
-
- public static float getFontScale(UiDevice device) throws Exception {
- String result = device.executeShellCommand("settings get system font_scale");
- try {
- return Float.parseFloat(result);
- } catch (NumberFormatException e) {
- return 1.0f;
- }
- }
-
- public static void setFontScale(UiDevice device, float fontScale) throws Exception {
- device.executeShellCommand("settings put system font_scale " + fontScale);
- }
-
- public static void setAndAssertFontScale(UiDevice device, float fontScale) throws Exception {
- setFontScale(device, fontScale);
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- assertEquals(getFontScale(device), fontScale, 0.0001f);
- }
}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
index af368f2..3c49e92 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -16,13 +16,11 @@
package androidx.wear.protolayout.material.layouts;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
-import static androidx.wear.protolayout.material.RunnerUtils.getFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
-import static androidx.wear.protolayout.material.RunnerUtils.setAndAssertFontScale;
-import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
@@ -34,13 +32,10 @@
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
-import androidx.test.uiautomator.UiDevice;
import androidx.wear.protolayout.DeviceParametersBuilders;
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.protolayout.material.RunnerUtils.TestCase;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,9 +47,14 @@
@RunWith(Parameterized.class)
@LargeTest
public class LayoutsGoldenXLTest {
- private static final float FONT_SCALE_XXXL = 1.24f;
+ /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+ tests together, first all parametrization (data()) methods are called, and then individual
+ tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+ each test. */
+ private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+ private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
- private static float originalFontScale;
+ private static final float FONT_SCALE_XXXL = 1.24f;
private final TestCase mTestCase;
private final String mExpected;
@@ -63,13 +63,6 @@
public AndroidXScreenshotTestRule mScreenshotRule =
new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
- @BeforeClass
- public static void setUpClass() throws Exception {
- setAndAssertFontScale(
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
- FONT_SCALE_XXXL);
- }
-
public LayoutsGoldenXLTest(String expected, TestCase testCase) {
mTestCase = testCase;
mExpected = expected;
@@ -80,18 +73,27 @@
return (int) ((px - 0.5f) / scale);
}
+ @SuppressWarnings("deprecation")
@Parameterized.Parameters(name = "{0}")
- public static Collection<Object[]> data() throws Exception {
- // These "parameters" methods are called before any parameterized test (from any class)
- // executes. We set and later reset the font here to have the correct context during test
- // generation. We later set and reset the font for the actual test in BeforeClass/AfterClass
- // methods.
- UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- originalFontScale = getFontScale(device);
- setAndAssertFontScale(device, FONT_SCALE_XXXL);
-
+ public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ currentDisplayMetrics.setTo(displayMetrics);
+ displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
float scale = displayMetrics.density;
DeviceParameters deviceParameters =
@@ -99,7 +101,6 @@
.setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
.setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
.setScreenDensity(displayMetrics.density)
- .setFontScale(context.getResources().getConfiguration().fontScale)
// TODO(b/231543947): Add test cases for round screen.
.setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
.build();
@@ -110,18 +111,40 @@
/* isForRtl= */ true,
/* isForLtr= */ true);
- // Restore state before this method, so other test have correct context.
- setAndAssertFontScale(device, originalFontScale);
+ // Restore state before this method, so other test have correct context. This is needed here
+ // too, besides in restoreBefore and restoreAfter as the test cases builder uses the context
+ // to apply font scaling, so we need that display metrics passed in. However, after
+ // generating cases we need to restore the state as other data() methods in this package can
+ // work correctly with the default state, as when the tests are run, first all data() static
+ // methods are called, and then parameterized test cases.
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
waitForNotificationToDisappears();
return testCaseList;
}
- @AfterClass
- public static void tearDownClass() throws Exception {
- setFontScale(
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
- originalFontScale);
+ @Parameterized.BeforeParam
+ public static void restoreBefore() {
+ // Set the state as it was in data() method when we generated test cases. This was
+ // overridden by other static data() methods, so we need to restore it.
+ OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+ getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+ }
+
+ @Parameterized.AfterParam
+ public static void restoreAfter() {
+ // Restore the state to default, so the other tests and emulator have the correct starter
+ // state.
+ getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
}
@Test
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
index da9219f..9e1613e 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
@@ -16,9 +16,6 @@
package androidx.wear.protolayout.material;
-import static android.os.Build.VERSION.SDK_INT;
-import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
-
import static androidx.annotation.Dimension.DP;
import static androidx.annotation.Dimension.SP;
import static androidx.wear.protolayout.DimensionBuilders.sp;
@@ -31,6 +28,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
+import android.util.DisplayMetrics;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
@@ -42,8 +40,6 @@
import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
import androidx.wear.protolayout.LayoutElementBuilders.FontVariant;
import androidx.wear.protolayout.LayoutElementBuilders.FontWeight;
-import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverter;
-import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -124,9 +120,6 @@
TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION2, 16f);
TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION3, 14f);
}
-
- private Typography() {}
-
/**
* Returns the {@link FontStyle.Builder} for the given FontStyle code with the recommended size,
* weight and letter spacing. Font will be scalable.
@@ -137,6 +130,8 @@
return getFontStyleBuilder(fontStyleCode, context, true);
}
+ private Typography() {}
+
/**
* Returns the {@link FontStyle.Builder} for the given Typography code with the recommended
* size, weight and letter spacing, with the option to make this font not scalable.
@@ -188,29 +183,17 @@
return sp(checkNotNull(TYPOGRAPHY_TO_LINE_HEIGHT_SP.get(typography)).intValue());
}
- /**
- * This is a helper function to make the font not scalable. It should interpret in value as DP
- * and convert it to SP which is needed to be passed in as a font size. However, we will pass an
- * SP object to it, because the default style is defined in it, but for the case when the font
- * size on device is 1, so the DP is equal to SP.
- */
- @Dimension(unit = SP)
- private static float dpToSp(float fontScale, @Dimension(unit = DP) float valueDp) {
- FontScaleConverter converter =
- (SDK_INT >= UPSIDE_DOWN_CAKE)
- ? FontScaleConverterFactory.forScale(fontScale)
- : null;
-
- if (converter == null) {
- return dpToSpLinear(fontScale, valueDp);
- }
-
- return converter.convertDpToSp(valueDp);
- }
-
- @Dimension(unit = SP)
- private static float dpToSpLinear(float fontScale, @Dimension(unit = DP) float valueDp) {
- return valueDp / fontScale;
+ @NonNull
+ @SuppressLint("ResourceType")
+ @SuppressWarnings("deprecation") // scaledDensity, b/335215227
+ // This is a helper function to make the font not scalable. It should interpret in value as DP
+ // and convert it to SP which is needed to be passed in as a font size. However, we will pass an
+ // SP object to it, because the default style is defined in it, but for the case when the font
+ // size on device in 1, so the DP is equal to SP.
+ private static SpProp dpToSp(@NonNull Context context, @Dimension(unit = DP) float valueDp) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ float scaledSp = (valueDp / metrics.scaledDensity) * metrics.density;
+ return sp(scaledSp);
}
// The @Dimension(unit = SP) on sp() is seemingly being ignored, so lint complains that we're
@@ -223,9 +206,8 @@
float letterSpacing,
boolean isScalable,
@NonNull Context context) {
- float fontScale = context.getResources().getConfiguration().fontScale;
return new FontStyle.Builder()
- .setSize(DimensionBuilders.sp(isScalable ? size : dpToSp(fontScale, size)))
+ .setSize(isScalable ? DimensionBuilders.sp(size) : dpToSp(context, size))
.setLetterSpacing(DimensionBuilders.em(letterSpacing))
.setVariant(variant)
.setWeight(weight);