Merge changes from topic "revert-3454934-tc_change_2501170544_1-PDFZXELQSF" into androidx-main
* changes:
Revert "Import translations. DO NOT MERGE ANYWHERE"
Revert "Import translations. DO NOT MERGE ANYWHERE"
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSchemaDefinition.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSchemaDefinition.kt
new file mode 100644
index 0000000..3469992
--- /dev/null
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSchemaDefinition.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 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.appfunctions
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
+
+/**
+ * Annotates an interface defining the schema for an app function, outlining its input, output, and
+ * behavior
+ *
+ * Example Usage:
+ * ```kotlin
+ * @AppFunctionSchemaDefinition(name = "findNotes", version = 1, category = "Notes")
+ * interface FindNotes {
+ * suspend fun findNotes(
+ * appFunctionContext: AppFunctionContext,
+ * findNotesParams: FindNotesParams,
+ * ): List<Note>
+ * }
+ * ```
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@Retention(
+ // Binary because it's used to determine the annotation values from the compiled schema library.
+ AnnotationRetention.BINARY
+)
+@Target(AnnotationTarget.CLASS)
+public annotation class AppFunctionSchemaDefinition(
+ val name: String,
+ val version: Int,
+ val category: String
+)
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
index 6151d45..8acc8bc 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
@@ -20,6 +20,7 @@
import androidx.appfunctions.compiler.core.logException
import androidx.appfunctions.compiler.processors.AppFunctionIdProcessor
import androidx.appfunctions.compiler.processors.AppFunctionInventoryProcessor
+import androidx.appfunctions.compiler.processors.AppFunctionLegacyIndexXmlProcessor
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
@@ -53,8 +54,11 @@
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
val idProcessor = AppFunctionIdProcessor(environment.codeGenerator)
val inventoryProcessor = AppFunctionInventoryProcessor(environment.codeGenerator)
+ // TODO: Add compiler option to disable legacy xml generator.
+ val legacyIndexXmlProcessor =
+ AppFunctionLegacyIndexXmlProcessor(environment.codeGenerator)
return AppFunctionCompiler(
- listOf(idProcessor, inventoryProcessor),
+ listOf(idProcessor, inventoryProcessor, legacyIndexXmlProcessor),
environment.logger,
)
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
index 2bac5ac..a7d1d5c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
@@ -20,6 +20,7 @@
import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionAnnotation
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
/** The helper class to resolve AppFunction related symbols. */
@@ -113,5 +114,8 @@
val methodName = functionDeclaration.simpleName.asString()
return "${packageName}.${className}#${methodName}"
}
+
+ /** Returns the file containing the class declaration and app functions. */
+ fun getSourceFile(): KSFile? = classDeclaration.containingFile
}
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
index fe0d3fa..6f90f16 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
@@ -28,6 +28,14 @@
// Annotation classes
object AppFunctionAnnotation {
val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunction")
+ const val PROPERTY_IS_ENABLED = "isEnabled"
+ }
+
+ object AppFunctionSchemaDefinitionAnnotation {
+ val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionSchemaDefinition")
+ const val PROPERTY_CATEGORY = "category"
+ const val PROPERTY_NAME = "name"
+ const val PROPERTY_VERSION = "version"
}
// Classes
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
index 1fa4707..f723a5cb 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
@@ -16,8 +16,12 @@
package androidx.appfunctions.compiler.core
+import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSName
import com.google.devtools.ksp.symbol.KSTypeReference
import com.squareup.kotlinpoet.ClassName
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
/**
* Checks if the type reference is of the given type.
@@ -27,12 +31,45 @@
* @throws ProcessingException If unable to resolve the type.
*/
fun KSTypeReference.isOfType(type: ClassName): Boolean {
- val ksType = this.resolve()
val typeName =
- ksType.declaration.qualifiedName
+ resolveTypeName()
?: throw ProcessingException(
"Unable to resolve the type to check if it is of type [${type}]",
this
)
return typeName.asString() == type.canonicalName
}
+
+/**
+ * Finds and returns an annotation of [annotationClass] type.
+ *
+ * @param annotationClass the annotation class to find
+ */
+fun Sequence<KSAnnotation>.findAnnotation(annotationClass: ClassName): KSAnnotation? =
+ this.singleOrNull() {
+ val shortName = it.shortName.getShortName()
+ if (shortName != annotationClass.simpleName) {
+ false
+ } else {
+ val typeName =
+ it.annotationType.resolveTypeName()
+ ?: throw ProcessingException(
+ "Unable to resolve type for [$shortName]",
+ it.annotationType
+ )
+ typeName.asString() == annotationClass.canonicalName
+ }
+ }
+
+private fun KSTypeReference.resolveTypeName(): KSName? = resolve().declaration.qualifiedName
+
+/** Returns the value of the annotation property if found. */
+fun <T : Any> KSAnnotation.requirePropertyValueOfType(
+ propertyName: String,
+ expectedType: KClass<T>,
+): T {
+ val propertyValue =
+ this.arguments.singleOrNull { it.name?.asString() == propertyName }?.value
+ ?: throw ProcessingException("Unable to find property with name: $propertyName", this)
+ return expectedType.cast(propertyValue)
+}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt
new file mode 100644
index 0000000..d41a4d3e
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2025 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.appfunctions.compiler.processors
+
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver.AnnotatedAppFunctions
+import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionAnnotation
+import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionSchemaDefinitionAnnotation
+import androidx.appfunctions.compiler.core.ProcessingException
+import androidx.appfunctions.compiler.core.findAnnotation
+import androidx.appfunctions.compiler.core.requirePropertyValueOfType
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.squareup.kotlinpoet.ClassName
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+
+/**
+ * Generates AppFunction's index xml file for the legacy AppSearch indexer to index.
+ *
+ * The generator would write an XML file as `/assets/app_functions.xml`. The file would be packaged
+ * into the APK's asset when assembled. So that the AppSearch indexer can look up the asset and
+ * inject metadata into platform AppSearch database accordingly.
+ *
+ * The new indexer will index additional properties based on the schema defined in SDK instead of
+ * the pre-defined one in AppSearch.
+ */
+class AppFunctionLegacyIndexXmlProcessor(
+ private val codeGenerator: CodeGenerator,
+) : SymbolProcessor {
+
+ override fun process(resolver: Resolver): List<KSAnnotated> {
+ generateLegacyIndexXml(AppFunctionSymbolResolver(resolver).resolveAnnotatedAppFunctions())
+ return emptyList()
+ }
+
+ /**
+ * Generates AppFunction's legacy index xml files for v1 indexer in App Search.
+ *
+ * @param appFunctionsByClass a collection of functions annotated with @AppFunction grouped by
+ * their enclosing classes.
+ */
+ private fun generateLegacyIndexXml(
+ appFunctionsByClass: List<AnnotatedAppFunctions>,
+ ) {
+ if (appFunctionsByClass.isEmpty()) {
+ return
+ }
+ val xmlDetails = appFunctionsByClass.flatMap(::getAppFunctionXmlDetail)
+ writeXmlFile(xmlDetails, appFunctionsByClass)
+ }
+
+ private fun getAppFunctionXmlDetail(
+ appFunctionsByClass: AnnotatedAppFunctions
+ ): List<AppFunctionXmlDetails> {
+
+ return appFunctionsByClass.appFunctionDeclarations.map {
+ val appFunctionAnnotation =
+ it.annotations.findAnnotation(AppFunctionAnnotation.CLASS_NAME)
+ ?: throw ProcessingException("Function not annotated with @AppFunction.", it)
+ val enabled =
+ appFunctionAnnotation.requirePropertyValueOfType(
+ AppFunctionAnnotation.PROPERTY_IS_ENABLED,
+ Boolean::class,
+ )
+
+ val schemaDetail = getAppFunctionSchemaDetail(it)
+
+ AppFunctionXmlDetails(
+ appFunctionsByClass.getAppFunctionIdentifier(it),
+ enabled,
+ schemaDetail,
+ )
+ }
+ }
+
+ private fun getAppFunctionSchemaDetail(
+ function: KSFunctionDeclaration
+ ): AppFunctionSchemaDetail? {
+ val rootInterfaceWithAppFunctionSchemaDefinition =
+ findRootInterfaceWithAnnotation(
+ function,
+ AppFunctionSchemaDefinitionAnnotation.CLASS_NAME
+ ) ?: return null
+
+ val schemaFunctionAnnotation =
+ rootInterfaceWithAppFunctionSchemaDefinition.annotations.findAnnotation(
+ AppFunctionSchemaDefinitionAnnotation.CLASS_NAME
+ ) ?: return null
+ val schemaCategory =
+ schemaFunctionAnnotation.requirePropertyValueOfType(
+ AppFunctionSchemaDefinitionAnnotation.PROPERTY_CATEGORY,
+ String::class,
+ )
+ val schemaName =
+ schemaFunctionAnnotation.requirePropertyValueOfType(
+ AppFunctionSchemaDefinitionAnnotation.PROPERTY_NAME,
+ String::class,
+ )
+ val schemaVersion =
+ schemaFunctionAnnotation.requirePropertyValueOfType(
+ AppFunctionSchemaDefinitionAnnotation.PROPERTY_VERSION,
+ Int::class,
+ )
+ return AppFunctionSchemaDetail(schemaCategory, schemaName, schemaVersion)
+ }
+
+ private fun findRootInterfaceWithAnnotation(
+ function: KSFunctionDeclaration,
+ annotationName: ClassName
+ ): KSClassDeclaration? {
+ val parentDeclaration = function.parentDeclaration as? KSClassDeclaration ?: return null
+
+ // Check if the enclosing class has the @AppFunctionSchemaDefinition
+ val annotation = parentDeclaration.annotations.findAnnotation(annotationName)
+ if (annotation != null) {
+ return parentDeclaration
+ }
+
+ val superClassFunction = (function.findOverridee() as? KSFunctionDeclaration) ?: return null
+ return findRootInterfaceWithAnnotation(superClassFunction, annotationName)
+ }
+
+ private fun writeXmlFile(
+ xmlDetailsList: List<AppFunctionXmlDetails>,
+ appFunctionsByClass: List<AnnotatedAppFunctions>,
+ ) {
+ val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true }
+
+ val appFunctionsElement = xmlDocument.createElement(XmlElement.APP_FUNCTIONS_ELEMENTS_TAG)
+ xmlDocument.appendChild(appFunctionsElement)
+
+ for (xmlDetails in xmlDetailsList) {
+ appFunctionsElement.appendChild(xmlDocument.createAppFunctionElement(xmlDetails))
+ }
+
+ val transformer =
+ TransformerFactory.newInstance().newTransformer().apply {
+ setOutputProperty(OutputKeys.INDENT, "yes")
+ setOutputProperty(OutputKeys.ENCODING, "UTF-8")
+ setOutputProperty(OutputKeys.VERSION, "1.0")
+ setOutputProperty(OutputKeys.STANDALONE, "yes")
+ }
+
+ codeGenerator
+ .createNewFile(
+ Dependencies(
+ aggregating = true,
+ *appFunctionsByClass.mapNotNull { it.getSourceFile() }.toTypedArray()
+ ),
+ XML_PACKAGE_NAME,
+ XML_FILE_NAME,
+ XML_EXTENSION
+ )
+ .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) }
+ }
+
+ private fun Document.createAppFunctionElement(xmlDetails: AppFunctionXmlDetails): Element =
+ createElement(XmlElement.APP_FUNCTION_ITEM_TAG).apply {
+ appendChild(
+ createElementWithTextNode(XmlElement.APP_FUNCTION_ID_TAG, xmlDetails.functionId)
+ )
+
+ val schemaDetail = xmlDetails.schemaDetail
+ if (schemaDetail != null) {
+ appendChild(
+ createElementWithTextNode(
+ XmlElement.APP_FUNCTION_SCHEMA_CATEGORY_TAG,
+ schemaDetail.schemaCategory,
+ )
+ )
+ appendChild(
+ createElementWithTextNode(
+ XmlElement.APP_FUNCTION_SCHEMA_NAME_TAG,
+ schemaDetail.schemaName,
+ )
+ )
+ appendChild(
+ createElementWithTextNode(
+ XmlElement.APP_FUNCTION_SCHEMA_VERSION_TAG,
+ schemaDetail.schemaVersion.toString(),
+ )
+ )
+ }
+ appendChild(
+ createElementWithTextNode(
+ XmlElement.APP_FUNCTION_ENABLE_BY_DEFAULT_TAG,
+ xmlDetails.enabled.toString(),
+ )
+ )
+ }
+
+ private fun Document.createElementWithTextNode(elementName: String, text: String): Element =
+ createElement(elementName).apply { appendChild(createTextNode(text)) }
+
+ /** Details of an app function that are needed to generate its XML file. */
+ private data class AppFunctionXmlDetails(
+ val functionId: String,
+ val enabled: Boolean,
+ val schemaDetail: AppFunctionSchemaDetail?,
+ )
+
+ /** Details of an schema function that are needed to generate its XML file. */
+ private data class AppFunctionSchemaDetail(
+ val schemaCategory: String,
+ val schemaName: String,
+ val schemaVersion: Int,
+ )
+
+ private companion object {
+ private const val XML_PACKAGE_NAME = "assets"
+ private const val XML_FILE_NAME = "app_functions"
+ private const val XML_EXTENSION = "xml"
+
+ private object XmlElement {
+ const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions"
+ const val APP_FUNCTION_ITEM_TAG = "appfunction"
+ const val APP_FUNCTION_ID_TAG = "function_id"
+ const val APP_FUNCTION_SCHEMA_CATEGORY_TAG = "schema_category"
+ const val APP_FUNCTION_SCHEMA_NAME_TAG = "schema_name"
+ const val APP_FUNCTION_SCHEMA_VERSION_TAG = "schema_version"
+ const val APP_FUNCTION_ENABLE_BY_DEFAULT_TAG = "enabled_by_default"
+ }
+ }
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
index 8860052..39b1ebb 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
@@ -46,9 +46,9 @@
fun testSimpleFunction_genAppFunctionIds_success() {
val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
- compilationTestHelper.assertSuccessWithContent(
+ compilationTestHelper.assertSuccessWithSourceContent(
report = report,
- expectGeneratedFileName = "SimpleFunctionIds.kt",
+ expectGeneratedSourceFileName = "SimpleFunctionIds.kt",
goldenFileName = "SimpleFunctionIds.KT"
)
}
@@ -85,10 +85,25 @@
fun testSimpleFunction_genAppFunctionInventoryImpl_success() {
val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
- compilationTestHelper.assertSuccessWithContent(
+ compilationTestHelper.assertSuccessWithSourceContent(
report = report,
- expectGeneratedFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
+ expectGeneratedSourceFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
goldenFileName = "$%s".format("SimpleFunction_AppFunctionInventory_Impl.KT")
)
}
+
+ // TODO: Add more tests for legacy index processor.
+ @Test
+ fun testSampleNoParamImp_genLegacyIndexXmlFile_success() {
+ val report =
+ compilationTestHelper.compileAll(
+ sourceFileNames = listOf("FakeNoArgImpl.KT", "FakeSchemas.KT")
+ )
+
+ compilationTestHelper.assertSuccessWithResourceContent(
+ report = report,
+ expectGeneratedResourceFileName = "app_functions.xml",
+ goldenFileName = "fakeNoArgImpl_app_function.xml"
+ )
+ }
}
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
index 4e6253d..cbb3c4b 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
@@ -17,6 +17,7 @@
package androidx.appfunctions.compiler.testings
import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Resource
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
import androidx.room.compiler.processing.util.compiler.TestCompilationResult
@@ -99,28 +100,20 @@
}
/**
- * Asserts that the compilation succeeds and contains [expectGeneratedFileName] in generated
- * sources that is identical to the content of [goldenFileName].
+ * Asserts that the compilation succeeds and contains [expectGeneratedSourceFileName] in
+ * generated sources that is identical to the content of [goldenFileName].
*/
- fun assertSuccessWithContent(
+ fun assertSuccessWithSourceContent(
report: CompilationReport,
- expectGeneratedFileName: String,
+ expectGeneratedSourceFileName: String,
goldenFileName: String,
) {
- Truth.assertWithMessage(
- """
- Compile failed with error:
- ${report.printDiagnostics(Diagnostic.Kind.ERROR)}
- """
- .trimIndent()
- )
- .that(report.isSuccess)
- .isTrue()
+ assertCompilationSuccess(report)
val goldenFile = getGoldenFile(goldenFileName)
val generatedSourceFile =
report.generatedSourceFiles.single { sourceFile ->
- sourceFile.source.relativePath.contains(expectGeneratedFileName)
+ sourceFile.source.relativePath.contains(expectGeneratedSourceFileName)
}
Truth.assertWithMessage(
"""
@@ -136,6 +129,48 @@
.isEqualTo(goldenFile.readText())
}
+ /**
+ * Asserts that the compilation succeeds and contains [expectGeneratedResourceFileName] in
+ * generated resources that is identical to the content of [goldenFileName].
+ */
+ fun assertSuccessWithResourceContent(
+ report: CompilationReport,
+ expectGeneratedResourceFileName: String,
+ goldenFileName: String,
+ ) {
+ assertCompilationSuccess(report)
+
+ val goldenFile = getGoldenFile(goldenFileName)
+ val generatedResourceFile =
+ report.generatedResourceFiles.single { resourceFile ->
+ resourceFile.resource.relativePath.contains(expectGeneratedResourceFileName)
+ }
+ Truth.assertWithMessage(
+ """
+ Content of generated file [${generatedResourceFile.resource.relativePath}] does not match
+ the content of golden file [${goldenFile.path}].
+
+ To update the golden file,
+ run `cp ${generatedResourceFile.resourceFilePath} ${goldenFile.absolutePath}`
+ """
+ .trimIndent()
+ )
+ .that(generatedResourceFile.resource.getContents())
+ .isEqualTo(goldenFile.readText())
+ }
+
+ private fun assertCompilationSuccess(report: CompilationReport) {
+ Truth.assertWithMessage(
+ """
+ Compile failed with error:
+ ${report.printDiagnostics(Diagnostic.Kind.ERROR)}
+ """
+ .trimIndent()
+ )
+ .that(report.isSuccess)
+ .isTrue()
+ }
+
fun assertErrorWithMessage(report: CompilationReport, expectedErrorMessage: String) {
Truth.assertWithMessage("Compile succeed").that(report.isSuccess).isFalse()
@@ -196,6 +231,8 @@
val generatedSourceFiles: List<GeneratedSourceFile>,
/** A map of diagnostics results. */
val diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>,
+ /** A list of generated source files. */
+ val generatedResourceFiles: List<GeneratedResourceFile>,
) {
/** Print the diagnostics result of type [kind]. */
fun printDiagnostics(kind: Diagnostic.Kind): String {
@@ -216,7 +253,11 @@
result.generatedSources.map { source ->
GeneratedSourceFile.create(source, outputDir)
},
- diagnostics = result.diagnostics
+ diagnostics = result.diagnostics,
+ generatedResourceFiles =
+ result.generatedResources.map { resource ->
+ GeneratedResourceFile.create(resource, outputDir)
+ }
)
}
}
@@ -236,4 +277,22 @@
}
}
}
+
+ /** A wrapper class contains [Resource] with its file path. */
+ data class GeneratedResourceFile(val resource: Resource, val resourceFilePath: Path) {
+ companion object {
+ internal fun create(resource: Resource, outputDir: Path): GeneratedResourceFile {
+ val filePath =
+ outputDir.resolve(resource.relativePath).apply {
+ parent?.createDirectories()
+ createFile()
+ writeText(resource.getContents())
+ }
+ return GeneratedResourceFile(resource, filePath)
+ }
+ }
+ }
}
+
+private fun Resource.getContents(): String =
+ openInputStream().bufferedReader().use { it.readText() }
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT
new file mode 100644
index 0000000..4b09194
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT
@@ -0,0 +1,8 @@
+package com.testdata
+
+import androidx.appfunctions.AppFunction
+import androidx.appfunctions.AppFunctionContext
+
+class FakeNoArgImpl : FakeNoArg {
+ @AppFunction override fun noArg(appFunctionContext: AppFunctionContext) {}
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT
new file mode 100644
index 0000000..a59c737
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT
@@ -0,0 +1,11 @@
+package com.testdata
+
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionSchemaDefinition
+
+private const val FAKE_CATEGORY = "fake_schema_category"
+
+@AppFunctionSchemaDefinition(name = "noArg", version = 1, category = FAKE_CATEGORY)
+interface FakeNoArg {
+ fun noArg(appFunctionContext: AppFunctionContext)
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml b/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml
new file mode 100644
index 0000000..229bae7
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<appfunctions>
+ <appfunction>
+ <function_id>com.testdata.FakeNoArgImpl#noArg</function_id>
+ <schema_category>fake_schema_category</schema_category>
+ <schema_name>noArg</schema_name>
+ <schema_version>1</schema_version>
+ <enabled_by_default>true</enabled_by_default>
+ </appfunction>
+</appfunctions>
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
index 2f84338..0c80536 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
@@ -96,9 +96,6 @@
Debug.traceStart { "Create CameraPipe" }
val timeSource = SystemTimeSource()
val start = Timestamps.now(timeSource)
- // Enable pruning device manager when tested in the MH lab.
- val usePruningDeviceManager =
- android.util.Log.isLoggable(CAMERA_PIPE_MH_FLAG, android.util.Log.DEBUG)
val cameraPipe =
CameraPipe(
@@ -110,16 +107,11 @@
sharedInteropCallbacks.sessionStateCallback,
openRetryMaxTimeout
),
- usePruningDeviceManager = usePruningDeviceManager
+ usePruningDeviceManager = true
)
)
Log.debug { "Created CameraPipe in ${start.measureNow(timeSource).formatMs()}" }
Debug.traceStop()
return cameraPipe
}
-
- private companion object {
- // Flag set when being tested in the lab. Refer to CameraPipeConfigTestRule for more info.
- const val CAMERA_PIPE_MH_FLAG = "CameraPipeMH"
- }
}
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt
new file mode 100644
index 0000000..c04af8b
--- /dev/null
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2025 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.pdf.view
+
+import android.graphics.Point
+import android.graphics.PointF
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class PdfViewZoomStateTest {
+
+ private lateinit var activityScenario: ActivityScenario<PdfViewTestActivity>
+
+ @Before
+ fun setup() {
+ activityScenario = ActivityScenario.launch(PdfViewTestActivity::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ PdfViewTestActivity.onCreateCallback = {}
+ activityScenario.close()
+ }
+
+ private fun setupPdfView(fakePdfDocument: FakePdfDocument?) {
+ PdfViewTestActivity.onCreateCallback = { activity ->
+ val container = FrameLayout(activity)
+ val pdfView =
+ PdfView(activity).apply {
+ pdfDocument = fakePdfDocument
+ id = PDF_VIEW_ID
+ minZoom = 0.5f
+ maxZoom = 5.0f
+ }
+ container.addView(pdfView, ViewGroup.LayoutParams(PAGE_WIDTH, PAGE_HEIGHT * 2))
+ activity.setContentView(container)
+ }
+ }
+
+ @Test
+ fun testInitialZoom_fitWidth() = runTest {
+ val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+
+ setupPdfView(fakePdfDocument)
+
+ with(ActivityScenario.launch(PdfViewTestActivity::class.java)) {
+ fakePdfDocument.waitForLayout(untilPage = 3)
+ onView(withId(PDF_VIEW_ID)).check { view, noViewFoundException ->
+ view ?: throw noViewFoundException
+ val pdfView = view as PdfView
+ assertThat(pdfView.isInitialZoomDone).isTrue()
+ assertThat(pdfView.zoom).isWithin(0.01f).of(1.0f)
+ }
+ }
+ }
+
+ @Test
+ fun testGetDefaultZoom_fitWidth() = runTest {
+ val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+
+ setupPdfView(fakePdfDocument)
+ activityScenario.recreate()
+
+ activityScenario.onActivity { activity ->
+ val pdfView = activity.findViewById<PdfView>(PDF_VIEW_ID)
+ pdfView.zoom = 2.0f
+ val expectedZoom = 1.0f
+ val actualZoom = pdfView.getDefaultZoom()
+ assertThat(actualZoom).isWithin(0.01f).of(expectedZoom)
+ }
+ }
+
+ @Test
+ fun testRestoreUserZoomAndScrollPosition() = runTest {
+ val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+ val savedZoom = 2.5f
+ val savedScrollPosition = PointF(100f, PAGE_HEIGHT * 1f / savedZoom)
+
+ setupPdfView(fakePdfDocument)
+ activityScenario.recreate()
+
+ activityScenario.onActivity { activity ->
+ val pdfView = activity.findViewById<PdfView>(PDF_VIEW_ID)
+ pdfView.zoom = savedZoom
+ pdfView.scrollTo(
+ (savedScrollPosition.x * savedZoom - pdfView.viewportWidth / 2f).toInt(),
+ (savedScrollPosition.y * savedZoom - pdfView.viewportHeight / 2f).toInt()
+ )
+ pdfView.isInitialZoomDone = true
+ }
+
+ activityScenario.recreate()
+
+ onView(withId(PDF_VIEW_ID)).check { view, _ ->
+ view as PdfView
+ assertThat(view.zoom).isWithin(0.01f).of(savedZoom)
+ val expectedScrollX =
+ (savedScrollPosition.x * savedZoom - view.viewportWidth / 2f).toInt()
+ val expectedScrollY =
+ (savedScrollPosition.y * savedZoom - view.viewportHeight / 2f).toInt()
+ assertThat(view.scrollX).isEqualTo(expectedScrollX)
+ assertThat(view.scrollY).isEqualTo(expectedScrollY)
+ }
+ }
+}
+
+/** Arbitrary fixed ID for PdfView */
+private const val PDF_VIEW_ID = 123456789
+private const val PAGE_WIDTH = 500
+private const val PAGE_HEIGHT = 800
+
+/** The height of the viewport, minus padding */
+val PdfView.viewportHeight: Int
+ get() = bottom - top - paddingBottom - paddingTop
+
+/** The width of the viewport, minus padding */
+val PdfView.viewportWidth: Int
+ get() = right - left - paddingRight - paddingLeft
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
index 0c1973b..2621d74 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
@@ -215,6 +215,7 @@
private var awaitingFirstLayout: Boolean = true
private var scrollPositionToRestore: PointF? = null
private var zoomToRestore: Float? = null
+ @VisibleForTesting internal var isInitialZoomDone: Boolean = false
/**
* The width of the PdfView before the last layout change (e.g., before rotation). Used to
* preserve the zoom level when the device is rotated.
@@ -521,6 +522,7 @@
val superState = super.onSaveInstanceState()
val state = PdfViewSavedState(superState)
state.zoom = zoom
+ state.isInitialZoomDone = isInitialZoomDone
state.viewWidth = width
state.contentCenterX = toContentX(viewportWidth.toFloat() / 2f)
state.contentCenterY = toContentY(viewportHeight.toFloat() / 2f)
@@ -605,7 +607,8 @@
}
}
- private fun getDefaultZoom(): Float {
+ @VisibleForTesting
+ internal fun getDefaultZoom(): Float {
if (contentWidth == 0 || viewportWidth == 0) return DEFAULT_INIT_ZOOM
val widthZoom = viewportWidth.toFloat() / contentWidth
return MathUtils.clamp(widthZoom, minZoom, maxZoom)
@@ -649,6 +652,7 @@
scrollPositionToRestore = positionToRestore
zoomToRestore = localStateToRestore.zoom
oldWidth = localStateToRestore.viewWidth
+ isInitialZoomDone = localStateToRestore.isInitialZoomDone
} else {
scrollToRestoredPosition(positionToRestore, localStateToRestore.zoom)
}
@@ -927,7 +931,10 @@
// centering if it's needed. It doesn't override any restored state because we're scrolling
// to the current scroll position.
if (pageNum == 0) {
- this.zoom = getDefaultZoom()
+ if (!isInitialZoomDone) {
+ this.zoom = getDefaultZoom()
+ isInitialZoomDone = true
+ }
scrollTo(scrollX, scrollY)
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
index c0c5047..dd0ab91 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
@@ -30,6 +30,7 @@
var zoom: Float = 1F
var documentUri: Uri? = null
var paginationModel: PaginationModel? = null
+ var isInitialZoomDone: Boolean = false
/**
* The width of the PdfView before the last layout change (e.g., before rotation). Used to
* preserve the zoom level when the device is rotated.
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 1cf5db4..e8eed4f 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -32,12 +32,14 @@
}
androidXMultiplatform {
+ js()
jvm() {
withJava()
}
mac()
linux()
ios()
+ wasmJs()
defaultPlatform(PlatformIdentifier.JVM)
@@ -45,7 +47,7 @@
commonMain {
dependencies {
api(libs.kotlinStdlib)
- api("androidx.annotation:annotation:1.8.1")
+ api("androidx.annotation:annotation:1.9.1")
}
}
diff --git a/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt b/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt
new file mode 100644
index 0000000..b43bd1e
--- /dev/null
+++ b/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import kotlin.reflect.AssociatedObjectKey
+import kotlin.reflect.ExperimentalAssociatedObjects
+import kotlin.reflect.KClass
+
+/**
+ * Defines the [androidx.room.RoomDatabaseConstructor] that will instantiate the Room generated
+ * implementation of the annotated [Database].
+ *
+ * A [androidx.room.RoomDatabase] database definition must be annotated with this annotation if it
+ * is located in a common source set on a Kotlin Multiplatform project such that at runtime the
+ * implementation generated by the annotation processor can be used. The [value] must be an 'expect
+ * object' that implements [androidx.room.RoomDatabaseConstructor].
+ *
+ * Example usage:
+ * ```
+ * @Database(version = 1, entities = [Song::class, Album::class])
+ * @ConstructedBy(MusicDatabaseConstructor::class)
+ * abstract class MusicDatabase : RoomDatabase
+ *
+ * expect object MusicDatabaseConstructor : RoomDatabaseConstructor<MusicDatabase>
+ * ```
+ *
+ * @see androidx.room.RoomDatabaseConstructor
+ */
+@OptIn(ExperimentalAssociatedObjects::class)
+@AssociatedObjectKey
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+actual annotation class ConstructedBy(
+ /**
+ * The 'expect' declaration of an 'object' that implements
+ * [androidx.room.RoomDatabaseConstructor] and is able to instantiate a
+ * [androidx.room.RoomDatabase].
+ */
+ actual val value: KClass<*>
+)
diff --git a/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt b/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt
new file mode 100644
index 0000000..b43bd1e
--- /dev/null
+++ b/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import kotlin.reflect.AssociatedObjectKey
+import kotlin.reflect.ExperimentalAssociatedObjects
+import kotlin.reflect.KClass
+
+/**
+ * Defines the [androidx.room.RoomDatabaseConstructor] that will instantiate the Room generated
+ * implementation of the annotated [Database].
+ *
+ * A [androidx.room.RoomDatabase] database definition must be annotated with this annotation if it
+ * is located in a common source set on a Kotlin Multiplatform project such that at runtime the
+ * implementation generated by the annotation processor can be used. The [value] must be an 'expect
+ * object' that implements [androidx.room.RoomDatabaseConstructor].
+ *
+ * Example usage:
+ * ```
+ * @Database(version = 1, entities = [Song::class, Album::class])
+ * @ConstructedBy(MusicDatabaseConstructor::class)
+ * abstract class MusicDatabase : RoomDatabase
+ *
+ * expect object MusicDatabaseConstructor : RoomDatabaseConstructor<MusicDatabase>
+ * ```
+ *
+ * @see androidx.room.RoomDatabaseConstructor
+ */
+@OptIn(ExperimentalAssociatedObjects::class)
+@AssociatedObjectKey
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+actual annotation class ConstructedBy(
+ /**
+ * The 'expect' declaration of an 'object' that implements
+ * [androidx.room.RoomDatabaseConstructor] and is able to instantiate a
+ * [androidx.room.RoomDatabase].
+ */
+ actual val value: KClass<*>
+)
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 1fb47dc..ca1d072 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -1650,12 +1650,10 @@
}
public final class TimeTextDefaults {
- method public float getAutoTextWeight();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
method @androidx.compose.runtime.Composable public String timeFormat();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
- property public final float AutoTextWeight;
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.CurvedTextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
property public static final float MaxSweepAngle;
property public static final String TimeFormat12Hours;
@@ -1667,14 +1665,9 @@
}
public final class TimeTextKt {
- method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.ui.text.TextStyle timeTextStyle, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.TimeTextScope,kotlin.Unit> content);
- }
-
- public abstract sealed class TimeTextScope {
- method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
- method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
- method public abstract void time();
+ method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.CurvedScope,? super java.lang.String,kotlin.Unit> content);
+ method public static void timeTextCurvedText(androidx.wear.compose.foundation.CurvedScope, String time, optional androidx.wear.compose.foundation.CurvedTextStyle? style);
+ method public static void timeTextSeparator(androidx.wear.compose.foundation.CurvedScope, optional androidx.wear.compose.foundation.CurvedTextStyle? curvedTextStyle, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding);
}
public final class TouchTargetAwareSizeKt {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 1fb47dc..ca1d072 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -1650,12 +1650,10 @@
}
public final class TimeTextDefaults {
- method public float getAutoTextWeight();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
method @androidx.compose.runtime.Composable public String timeFormat();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
- property public final float AutoTextWeight;
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.CurvedTextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
property public static final float MaxSweepAngle;
property public static final String TimeFormat12Hours;
@@ -1667,14 +1665,9 @@
}
public final class TimeTextKt {
- method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.ui.text.TextStyle timeTextStyle, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.TimeTextScope,kotlin.Unit> content);
- }
-
- public abstract sealed class TimeTextScope {
- method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
- method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
- method public abstract void time();
+ method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.CurvedScope,? super java.lang.String,kotlin.Unit> content);
+ method public static void timeTextCurvedText(androidx.wear.compose.foundation.CurvedScope, String time, optional androidx.wear.compose.foundation.CurvedTextStyle? style);
+ method public static void timeTextSeparator(androidx.wear.compose.foundation.CurvedScope, optional androidx.wear.compose.foundation.CurvedTextStyle? curvedTextStyle, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding);
}
public final class TouchTargetAwareSizeKt {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
index ce9aa66..9b3d8c3 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
@@ -41,6 +41,7 @@
import androidx.wear.compose.material3.ScreenStage
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.curvedText
import androidx.wear.compose.material3.samples.ScrollAwaySample
import androidx.wear.compose.material3.scrollAway
@@ -100,7 +101,7 @@
else ScreenStage.Idle
}
),
- content = { text("ScrollAway") }
+ content = { curvedText("ScrollAway") }
)
}
}
@@ -151,7 +152,7 @@
else ScreenStage.Idle
}
),
- content = { text("ScrollAway") }
+ content = { curvedText("ScrollAway") }
)
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
index 397f24d..d53c350 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
@@ -33,6 +33,7 @@
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.material3.Button
@@ -43,13 +44,17 @@
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TimeText
import androidx.wear.compose.material3.TimeTextDefaults
+import androidx.wear.compose.material3.curvedText
import androidx.wear.compose.material3.samples.TimeTextClockOnly
import androidx.wear.compose.material3.samples.TimeTextWithStatus
+import androidx.wear.compose.material3.samples.TimeTextWithStatusEllipsized
+import androidx.wear.compose.material3.timeTextSeparator
val TimeTextDemos =
listOf(
ComposableDemo("Clock only") { TimeTextClockOnly() },
ComposableDemo("Clock with Status") { TimeTextWithStatus() },
+ ComposableDemo("Clock with Ellipsized Status") { TimeTextWithStatusEllipsized() },
ComposableDemo("Clock with long Status") { TimeTextWithLongStatus() },
ComposableDemo("Clock with Icon") { TimeTextWithIcon() },
ComposableDemo("Clock with custom colors") { TimeTextWithCustomColors() },
@@ -61,10 +66,10 @@
@Composable
fun TimeTextWithLongStatus() {
- TimeText {
- text("Some long leading text")
- separator()
- time()
+ TimeText { time ->
+ curvedText("Some long leading text")
+ timeTextSeparator()
+ curvedText(time)
}
}
@@ -72,12 +77,12 @@
fun TimeTextWithCustomColors() {
val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
- TimeText {
- text("ETA", customStyle)
- composable { Spacer(modifier = Modifier.size(4.dp)) }
- text("12:48")
- separator()
- time()
+ TimeText { time ->
+ curvedText("ETA", style = customStyle)
+ curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+ curvedText("12:48")
+ timeTextSeparator()
+ curvedText(time)
}
}
@@ -85,19 +90,19 @@
fun TimeTextCustomSize() {
val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Green, fontSize = 24.sp)
- TimeText {
- text("ETA", customStyle)
- separator()
- time()
+ TimeText { time ->
+ curvedText("ETA", style = customStyle)
+ timeTextSeparator()
+ curvedText(time)
}
}
@Composable
fun TimeTextWithIcon() {
- TimeText {
- time()
- separator()
- composable {
+ TimeText { time ->
+ curvedText(time)
+ timeTextSeparator()
+ curvedComposable {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "Favorite",
@@ -151,13 +156,13 @@
items(10) { Text("Some extra items ($it) to scroll", Modifier.padding(5.dp)) }
}
// Timetext later so it's on top.
- TimeText { time() }
+ TimeText { time -> curvedText(time) }
}
}
@Composable
fun TimeTextOnScreenWhiteBackground() {
- Box(Modifier.fillMaxSize().background(Color.White)) { TimeText { time() } }
+ Box(Modifier.fillMaxSize().background(Color.White)) { TimeText { time -> curvedText(time) } }
}
@Composable
@@ -166,7 +171,7 @@
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(background = Color.Transparent)
) {
- TimeText { time() }
+ TimeText { time -> curvedText(time) }
}
}
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
index 225dd56f..42f1ba7 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
@@ -33,7 +33,9 @@
import androidx.wear.compose.material3.ScreenStage
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.curvedText
import androidx.wear.compose.material3.scrollAway
+import androidx.wear.compose.material3.timeTextSeparator
@Sampled
@Composable
@@ -73,10 +75,10 @@
if (state.isScrollInProgress) ScreenStage.Scrolling else ScreenStage.Idle
}
),
- content = {
- text("ScrollAway")
- separator()
- time()
+ content = { time ->
+ curvedText("ScrollAway")
+ timeTextSeparator()
+ curvedText(time)
}
)
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
index 020e098..47ba1e6 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
@@ -18,9 +18,14 @@
import androidx.annotation.Sampled
import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.weight
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.TimeText
import androidx.wear.compose.material3.TimeTextDefaults
+import androidx.wear.compose.material3.curvedText
+import androidx.wear.compose.material3.timeTextSeparator
@Sampled
@Composable
@@ -34,9 +39,23 @@
fun TimeTextWithStatus() {
val primaryStyle =
TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primaryContainer)
- TimeText {
- text("ETA 12:48", style = primaryStyle)
- separator()
- time()
+ TimeText { time ->
+ curvedText("ETA 12:48", style = primaryStyle)
+ timeTextSeparator()
+ curvedText(time)
+ }
+}
+
+@Sampled
+@Composable
+fun TimeTextWithStatusEllipsized() {
+ TimeText { time ->
+ curvedText(
+ "Long status that should be ellipsized.",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis
+ )
+ timeTextSeparator()
+ curvedText(time)
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
index 90674cb..4849bdc 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
@@ -48,6 +48,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.wear.compose.foundation.ScrollInfoProvider
+import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
@@ -207,7 +208,7 @@
)
.testTag(TIME_TEXT_TAG),
) {
- composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+ curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
}
}
}
@@ -233,7 +234,7 @@
)
.testTag(TIME_TEXT_TAG)
) {
- composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+ curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
}
LazyColumn(state = scrollState, modifier = Modifier.testTag(SCROLL_TAG)) {
item { ListHeader { Text("Buttons") } }
@@ -253,7 +254,6 @@
.testTag(TEST_TAG)
) {
TimeText(
- contentColor = timeTextColor,
modifier =
Modifier.scrollAway(
scrollInfoProvider = ScrollInfoProvider(scrollState),
@@ -264,7 +264,7 @@
)
.testTag(TIME_TEXT_TAG)
) {
- composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+ curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
}
Column(modifier = Modifier.verticalScroll(scrollState).testTag(SCROLL_TAG)) {
ListHeader { Text("Buttons") }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
index 5c81306..526b932 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
@@ -29,23 +29,25 @@
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.DeviceConfigurationOverride
import androidx.compose.ui.test.ForcedSize
-import androidx.compose.ui.test.RoundScreen
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.then
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
-import com.google.testing.junit.testparameterinjector.TestParameter
-import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedScope
+import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.weight
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
@MediumTest
-@RunWith(TestParameterInjector::class)
+@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class TimeTextScreenshotTest {
@get:Rule val rule = createComposeRule()
@@ -60,60 +62,34 @@
}
@Test
- fun time_text_with_clock_only_on_round_device() = verifyScreenshot {
+ fun time_text_with_clock_only() = verifyScreenshot {
TimeText(
modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
- ) {
- time()
+ )
+ }
+
+ @Test
+ fun time_text_with_status() = verifyScreenshot {
+ TimeText(
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+ timeSource = MockTimeSource,
+ ) { time ->
+ curvedText("ETA 12:48")
+ timeTextSeparator()
+ curvedText(time)
}
}
@Test
- fun time_text_with_clock_only_on_non_round_device() =
- verifyScreenshot(false) {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- time()
- }
- }
-
- @Test
- fun time_text_with_status_on_round_device() = verifyScreenshot {
+ fun time_text_with_icon() = verifyScreenshot {
TimeText(
modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
- ) {
- text("ETA 12:48")
- separator()
- time()
- }
- }
-
- @Test
- fun time_text_with_status_on_non_round_device() =
- verifyScreenshot(false) {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- text("ETA 12:48")
- separator()
- time()
- }
- }
-
- @Test
- fun time_text_with_icon_on_round_device() = verifyScreenshot {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- time()
- separator()
- composable {
+ ) { time ->
+ curvedText(time)
+ timeTextSeparator()
+ curvedComposable {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "Favorite",
@@ -124,193 +100,126 @@
}
@Test
- fun time_text_with_icon_on_non_round_device() =
- verifyScreenshot(false) {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- time()
- separator()
- composable {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Favorite",
- modifier = Modifier.size(13.dp)
- )
- }
- }
- }
-
- @Test
- fun time_text_with_custom_colors_on_round_device() = verifyScreenshot {
+ fun time_text_with_custom_colors() = verifyScreenshot {
val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
- ) {
- text("ETA", customStyle)
- composable { Spacer(modifier = Modifier.size(4.dp)) }
- text("12:48")
- separator(separatorStyle)
- time()
+ ) { time ->
+ curvedText("ETA 12:48", style = customStyle)
+ curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+ curvedText("12:48", style = customStyle)
+ timeTextSeparator(separatorStyle)
+ curvedText(time, style = timeTextStyle)
}
}
@Test
- fun time_text_with_long_status_on_round_device() = verifyScreenshot {
+ fun time_text_with_long_status() = verifyScreenshot {
val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
- ) {
- text("Long status that should be ellipsized.")
- composable { Spacer(modifier = Modifier.size(4.dp)) }
- time()
- }
- }
-
- @Test
- fun time_text_with_custom_colors_on_non_round_device() =
- verifyScreenshot(false) {
- val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
- val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
- val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
- TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- text("ETA", customStyle)
- composable { Spacer(modifier = Modifier.size(4.dp)) }
- text("12:48")
- separator(separatorStyle)
- time()
- }
- }
-
- @Test
- fun time_text_with_very_long_text_on_round_device() =
- verifyScreenshot(true) {
- val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
- val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
- val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
- TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
- maxSweepAngle = 180f,
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- text(
- "Very long text to ensure we are respecting the maxSweep parameter",
- customStyle
- )
- separator(separatorStyle)
- time()
- }
- }
-
- @Test
- fun time_text_with_very_long_text_non_round_device() =
- verifyScreenshot(false) {
- val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
- val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
- val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
- TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- text(
- "Very long text to ensure we are not taking more than one line and " +
- "leaving room for the time",
- customStyle
- )
- separator(separatorStyle)
- time()
- }
- }
-
- @Test
- fun time_text_with_very_long_text_smaller_angle_on_round_device() =
- verifyScreenshot(true) {
- val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
- val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
- val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
- TimeText(
- contentColor = Color.Green,
- timeTextStyle = timeTextStyle,
- maxSweepAngle = 90f,
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- ) {
- text(
- "Very long text to ensure we are respecting the maxSweep parameter",
- customStyle
- )
- separator(separatorStyle)
- time()
- }
- }
-
- @Test
- fun time_text_long_text_before_time(@TestParameter shape: ScreenShape) =
- TimeTextWithDefaults(shape.isRound) {
- text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
- separator()
- time()
- separator()
- text("More")
- }
-
- @Test
- fun time_text_long_text_after_time(@TestParameter shape: ScreenShape) =
- TimeTextWithDefaults(shape.isRound) {
- text("More")
- separator()
- time()
- separator()
- text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
- }
-
- // This is to get better names, so it says 'round_device' instead of 'true'
- enum class ScreenShape(val isRound: Boolean) {
- ROUND_DEVICE(true),
- SQUARE_DEVICE(false)
- }
-
- private fun TimeTextWithDefaults(isDeviceRound: Boolean, content: TimeTextScope.() -> Unit) =
- verifyScreenshot(isDeviceRound) {
- TimeText(
- contentColor = Color.Green,
- maxSweepAngle = 180f,
- modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
- timeSource = MockTimeSource,
- content = content
+ ) { time ->
+ curvedText(
+ "Long status that should be ellipsized.",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis,
+ style = timeTextStyle
)
+ curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+ curvedText(time, style = timeTextStyle)
}
+ }
- private fun verifyScreenshot(isDeviceRound: Boolean = true, content: @Composable () -> Unit) {
+ @Test
+ fun time_text_with_very_long_text() = verifyScreenshot {
+ val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
+ val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
+ val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ TimeText(
+ maxSweepAngle = 180f,
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+ timeSource = MockTimeSource,
+ ) { time ->
+ curvedText(
+ "Very long text to ensure we are respecting the maxSweep parameter",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis,
+ maxSweepAngle = 180f,
+ style = customStyle
+ )
+ timeTextSeparator(separatorStyle)
+ curvedText(time, style = timeTextStyle)
+ }
+ }
+
+ @Test
+ fun time_text_with_very_long_text_smaller_angle() = verifyScreenshot {
+ val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
+ val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
+ val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ TimeText(
+ maxSweepAngle = 90f,
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+ timeSource = MockTimeSource,
+ ) { time ->
+ curvedText(
+ "Very long text to ensure we are respecting the maxSweep parameter",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis,
+ style = customStyle
+ )
+ timeTextSeparator(separatorStyle)
+ curvedText(time, style = timeTextStyle)
+ }
+ }
+
+ @Test
+ fun time_text_long_text_before_time() = TimeTextWithDefaults { time ->
+ curvedText(
+ "Very long text to ensure we are respecting the weight parameter",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis
+ )
+ timeTextSeparator()
+ curvedText(time)
+ timeTextSeparator()
+ curvedText("More")
+ }
+
+ @Test
+ fun time_text_long_text_after_time() = TimeTextWithDefaults { time ->
+ curvedText("More")
+ timeTextSeparator()
+ curvedText(time)
+ timeTextSeparator()
+ curvedText(
+ "Very long text to ensure we are respecting the weight parameter",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ private fun TimeTextWithDefaults(content: CurvedScope.(String) -> Unit) = verifyScreenshot {
+ TimeText(
+ maxSweepAngle = 180f,
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+ timeSource = MockTimeSource,
+ content = content
+ )
+ }
+
+ private fun verifyScreenshot(content: @Composable () -> Unit) {
rule.verifyScreenshot(
- // Valid characters for golden identifiers are [A-Za-z0-9_-]
- // TestParameterInjector adds '[' + parameter_values + ']' to the test name.
- methodName = testName.methodName.replace("[", "_").replace("]", ""),
+ methodName = testName.methodName,
screenshotRule = screenshotRule,
content = {
val screenSize = LocalContext.current.resources.configuration.smallestScreenWidthDp
DeviceConfigurationOverride(
- DeviceConfigurationOverride.ForcedSize(
- DpSize(screenSize.dp, screenSize.dp)
- ) then DeviceConfigurationOverride.RoundScreen(isDeviceRound)
+ DeviceConfigurationOverride.ForcedSize(DpSize(screenSize.dp, screenSize.dp))
) {
content()
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
index f932e4a..e8a7528 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
@@ -21,20 +21,13 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.DeviceConfigurationOverride
-import androidx.compose.ui.test.RoundScreen
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.unit.sp
import androidx.test.filters.SdkSuppress
+import androidx.wear.compose.foundation.curvedComposable
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
@@ -47,101 +40,54 @@
@Test
fun supports_testtag() {
- rule.setContentWithTheme { TimeText(modifier = Modifier.testTag(TEST_TAG)) { time() } }
+ rule.setContentWithTheme { TimeText(modifier = Modifier.testTag(TEST_TAG)) }
rule.onNodeWithTag(TEST_TAG).assertExists()
}
@Test
- fun shows_time_by_default_on_non_round_device() {
+ fun shows_time_by_default() {
val timeText = "time"
rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeText
- },
- )
- }
+ TimeText(
+ timeSource =
+ object : TimeSource {
+ @Composable override fun currentTime(): String = timeText
+ },
+ )
}
// Note that onNodeWithText doesn't work for curved text, so only testing for non-round.
- rule.onNodeWithText(timeText).assertIsDisplayed()
+ rule.onNodeWithContentDescription(timeText).assertIsDisplayed()
}
@Test
- fun updates_clock_when_source_changes_on_non_round_device() {
+ fun updates_clock_when_source_changes() {
val timeState = mutableStateOf("Unchanged")
rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG),
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeState.value
- },
- ) {
- time()
- }
- }
+ TimeText(
+ modifier = Modifier.testTag(TEST_TAG),
+ timeSource =
+ object : TimeSource {
+ @Composable override fun currentTime(): String = timeState.value
+ },
+ )
}
timeState.value = "Changed"
- rule.onNodeWithText("Changed").assertIsDisplayed()
- }
-
- @Test
- fun updates_clock_when_source_changes_on_round_device() {
- val timeState = mutableStateOf("Unchanged")
-
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- TimeText(
- modifier = Modifier.testTag(TEST_TAG),
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeState.value
- },
- ) {
- time()
- }
- }
- }
- timeState.value = "Changed"
- rule.waitForIdle()
rule.onNodeWithContentDescription("Changed").assertIsDisplayed()
}
@Test
- fun checks_status_displayed_on_non_round_device() {
+ fun checks_status_displayed() {
val statusText = "Status"
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText {
- text(statusText)
- separator()
- time()
- }
- }
- }
-
- rule.onNodeWithText(statusText).assertIsDisplayed()
- }
-
- @Test
- fun checks_status_displayed_on_round_device() {
- val statusText = "Status"
-
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- TimeText {
- text(statusText)
- separator()
- time()
- }
+ rule.setContentWithTheme() {
+ TimeText { time ->
+ curvedText(statusText)
+ timeTextSeparator()
+ curvedText(time)
}
}
@@ -149,54 +95,26 @@
}
@Test
- fun checks_separator_displayed_on_non_round_device() {
+ fun checks_separator_displayed() {
val statusText = "Status"
val separatorText = "·"
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText {
- text(statusText)
- separator()
- time()
- }
- }
- }
-
- rule.onNodeWithText(separatorText).assertIsDisplayed()
- }
-
- @Test
- fun checks_separator_displayed_on_round_device() {
- val statusText = "Status"
- val separatorText = "·"
-
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- TimeText {
- text(statusText)
- separator()
- time()
- }
- }
- }
+ rule.setContentWithTheme { BasicTimeTextWithStatus(statusText) }
rule.onNodeWithContentDescription(separatorText).assertIsDisplayed()
}
@Test
- fun checks_composable_displayed_on_non_round_device() {
+ fun checks_composable_displayed() {
rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText {
- time()
- separator()
- composable {
- Text(
- modifier = Modifier.testTag(TEST_TAG),
- text = "Compose",
- )
- }
+ TimeText { time ->
+ curvedText(time)
+ timeTextSeparator()
+ curvedComposable {
+ Text(
+ modifier = Modifier.testTag(TEST_TAG),
+ text = "Compose",
+ )
}
}
}
@@ -205,178 +123,6 @@
}
@Test
- fun checks_composable_displayed_on_round_device() {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- TimeText {
- time()
- separator()
- composable {
- Text(
- modifier = Modifier.testTag(TEST_TAG),
- text = "Compose",
- )
- }
- }
- }
- }
-
- rule.onNodeWithTag(TEST_TAG).assertIsDisplayed()
- }
-
- @Test
- fun changes_timeTextStyle_on_non_round_device() {
- val timeText = "testTime"
-
- val testTextStyle =
- TextStyle(color = Color.Green, background = Color.Black, fontSize = 20.sp)
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeText
- },
- timeTextStyle = testTextStyle
- ) {
- time()
- }
- }
- }
- val actualStyle = rule.textStyleOf(timeText)
- Assert.assertEquals(testTextStyle.color, actualStyle.color)
- Assert.assertEquals(testTextStyle.background, actualStyle.background)
- Assert.assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
- }
-
- @Test
- fun changes_material_theme_on_non_round_device_except_color() {
- val timeText = "testTime"
-
- val testTextStyle =
- TextStyle(
- color = Color.Green,
- background = Color.Black,
- fontStyle = FontStyle.Italic,
- fontSize = 25.sp,
- fontFamily = FontFamily.SansSerif
- )
- rule.setContent {
- MaterialTheme(typography = MaterialTheme.typography.copy(arcMedium = testTextStyle)) {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeText
- },
- ) {
- time()
- }
- }
- }
- }
- val actualStyle = rule.textStyleOf(timeText)
- Assert.assertEquals(testTextStyle.background, actualStyle.background)
- Assert.assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
- Assert.assertEquals(testTextStyle.fontStyle, actualStyle.fontStyle)
- Assert.assertEquals(testTextStyle.fontFamily, actualStyle.fontFamily)
- Assert.assertNotEquals(testTextStyle.color, actualStyle.color)
- }
-
- @Test
- fun color_remains_onBackground_when_material_theme_changed_on_non_round_device() {
- val timeText = "testTime"
- var onBackgroundColor = Color.Unspecified
-
- val testTextStyle =
- TextStyle(
- color = Color.Green,
- background = Color.Black,
- fontStyle = FontStyle.Italic,
- fontSize = 25.sp,
- fontFamily = FontFamily.SansSerif
- )
- rule.setContent {
- MaterialTheme(typography = MaterialTheme.typography.copy(labelSmall = testTextStyle)) {
- onBackgroundColor = MaterialTheme.colorScheme.onBackground
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- timeSource =
- object : TimeSource {
- @Composable override fun currentTime(): String = timeText
- },
- ) {
- time()
- }
- }
- }
- }
- val actualStyle = rule.textStyleOf(timeText)
- Assert.assertEquals(onBackgroundColor, actualStyle.color)
- }
-
- @Test
- fun has_correct_default_leading_text_color_on_non_round_device() {
- val leadingText = "leadingText"
- var primaryColor = Color.Unspecified
-
- rule.setContentWithTheme {
- primaryColor = MaterialTheme.colorScheme.primary
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText {
- text(leadingText)
- separator()
- time()
- }
- }
- }
- val actualStyle = rule.textStyleOf(leadingText)
- Assert.assertEquals(primaryColor, actualStyle.color)
- }
-
- @Test
- fun supports_custom_leading_text_color_on_non_round_device() {
- val leadingText = "leadingText"
- val customColor = Color.Green
-
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(
- contentColor = customColor,
- ) {
- text(leadingText)
- separator()
- time()
- }
- }
- }
- val actualStyle = rule.textStyleOf(leadingText)
- Assert.assertEquals(customColor, actualStyle.color)
- }
-
- @Test
- fun supports_custom_text_style_on_non_round_device() {
- val leadingText = "leadingText"
-
- val timeTextStyle = TextStyle(background = Color.Blue, fontSize = 14.sp)
- val contentTextStyle =
- TextStyle(color = Color.Green, background = Color.Black, fontSize = 20.sp)
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
- TimeText(contentColor = Color.Red, timeTextStyle = timeTextStyle) {
- text(leadingText, contentTextStyle)
- separator()
- time()
- }
- }
- }
- val actualStyle = rule.textStyleOf(leadingText)
- Assert.assertEquals(contentTextStyle.color, actualStyle.color)
- Assert.assertEquals(contentTextStyle.background, actualStyle.background)
- Assert.assertEquals(contentTextStyle.fontSize, actualStyle.fontSize)
- }
-
- @Test
fun formats_current_time() {
val currentTimeInMillis = 1631544258000L // 2021-09-13 14:44:18
val format = "HH:mm:ss"
@@ -418,4 +164,13 @@
}
Assert.assertEquals(expectedTime, actualTime)
}
+
+ @Composable
+ private fun BasicTimeTextWithStatus(statusText: String) {
+ TimeText { time ->
+ curvedText(statusText)
+ timeTextSeparator()
+ curvedText(time)
+ }
+ }
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
index edffe7e..51fc42b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
@@ -46,7 +46,7 @@
@Composable
public fun AppScaffold(
modifier: Modifier = Modifier,
- timeText: @Composable () -> Unit = { TimeText { time() } },
+ timeText: @Composable () -> Unit = { TimeText() },
content: @Composable BoxScope.() -> Unit,
) {
CompositionLocalProvider(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
index f95bd3c..351a47c 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
@@ -22,17 +22,8 @@
import android.content.IntentFilter
import android.text.format.DateFormat
import androidx.annotation.VisibleForTesting
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
@@ -42,17 +33,14 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedAlignment
import androidx.wear.compose.foundation.CurvedDirection
@@ -61,35 +49,29 @@
import androidx.wear.compose.foundation.CurvedScope
import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.foundation.background
-import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.basicCurvedText
import androidx.wear.compose.foundation.curvedRow
import androidx.wear.compose.foundation.padding
import androidx.wear.compose.foundation.sizeIn
-import androidx.wear.compose.foundation.weight
-import androidx.wear.compose.material3.TimeTextDefaults.CurvedTextSeparator
-import androidx.wear.compose.material3.TimeTextDefaults.TextSeparator
import androidx.wear.compose.material3.TimeTextDefaults.timeFormat
+import androidx.wear.compose.material3.TimeTextDefaults.timeTextStyle
import androidx.wear.compose.materialcore.currentTimeMillis
import androidx.wear.compose.materialcore.is24HourFormat
-import androidx.wear.compose.materialcore.isRoundDevice
import java.util.Calendar
import java.util.Locale
/**
- * Layout to show the current time and a label at the top of the screen. If device has a round
- * screen, then the time will be curved along the top edge of the screen, if rectangular - then the
- * text and the time will be straight.
+ * Layout to show the current time and a label, they will be drawn in a curve, following the top
+ * edge of the screen.
*
* Note that Wear Material UX guidance recommends that time text should not be larger than
- * [TimeTextDefaults.MaxSweepAngle] of the screen edge on round devices, which is enforced by
- * default. It is recommended that additional content, if any, is limited to short status messages
- * before the [TimeTextScope.time] using the MaterialTheme.colorScheme.primary color.
+ * [TimeTextDefaults.MaxSweepAngle] of the screen edge, which is enforced by default. It is
+ * recommended that additional content, if any, is limited to short status messages before the time
+ * using the MaterialTheme.colorScheme.primary color.
*
* For more information, see the
* [Curved Text](https://developer.android.com/training/wearables/components/curved-text) guide.
*
- * Different components of [TimeText] can be added through methods of [TimeTextScope].
- *
* A simple [TimeText] which shows the current time:
*
* @sample androidx.wear.compose.material3.samples.TimeTextClockOnly
@@ -97,15 +79,19 @@
* A [TimeText] with a short app status message shown:
*
* @sample androidx.wear.compose.material3.samples.TimeTextWithStatus
+ *
+ * A [TimeText] with a long status message, that needs ellipsizing:
+ *
+ * @sample androidx.wear.compose.material3.samples.TimeTextWithStatusEllipsized
* @param modifier The modifier to be applied to the component.
* @param curvedModifier The [CurvedModifier] used to restrict the arc in which [TimeText] is drawn.
* @param maxSweepAngle The default maximum sweep angle in degrees.
* @param timeSource [TimeSource] which retrieves the current time and formats it.
- * @param timeTextStyle [TextStyle] for the time text itself.
- * @param contentColor [Color] of content of displayed through [TimeTextScope.text] and
- * [TimeTextScope.composable].
* @param contentPadding The spacing values between the container and the content.
- * @param content The content of the [TimeText] - displays the current time by default.
+ * @param content The content of the [TimeText] - displays the current time by default. This lambda
+ * receives the current time as a String and should display it using curvedText. Note that if long
+ * curved text is included here, it should specify [CurvedModifier.weight] on it so that the space
+ * available is suitably allocated.
*/
@Composable
public fun TimeText(
@@ -113,91 +99,31 @@
curvedModifier: CurvedModifier = CurvedModifier,
maxSweepAngle: Float = TimeTextDefaults.MaxSweepAngle,
timeSource: TimeSource = TimeTextDefaults.rememberTimeSource(timeFormat()),
- timeTextStyle: TextStyle = TimeTextDefaults.timeTextStyle(),
- contentColor: Color = MaterialTheme.colorScheme.primary,
contentPadding: PaddingValues = TimeTextDefaults.ContentPadding,
- content: TimeTextScope.() -> Unit = { time() }
+ content: CurvedScope.(String) -> Unit = { time -> timeTextCurvedText(time) }
) {
- val timeText = timeSource.currentTime()
+ val currentTime = timeSource.currentTime()
val backgroundColor = CurvedTextDefaults.backgroundColor()
- if (isRoundDevice()) {
- CurvedLayout(modifier = modifier) {
- curvedRow(
- modifier =
- curvedModifier
- .sizeIn(maxSweepDegrees = maxSweepAngle)
- .padding(contentPadding.toArcPadding())
- .background(backgroundColor, StrokeCap.Round),
- radialAlignment = CurvedAlignment.Radial.Center
- ) {
- CurvedTimeTextScope(timeText, timeTextStyle, maxSweepAngle, contentColor).apply {
- content()
- Show()
- }
- }
- }
- } else {
- Box(modifier.fillMaxSize()) {
- Row(
- modifier =
- Modifier.align(Alignment.TopCenter)
- .background(backgroundColor, CircleShape)
- .padding(contentPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- LinearTimeTextScope(timeText, timeTextStyle, contentColor).apply {
- content()
- Show()
- }
- }
+ CurvedLayout(modifier = modifier) {
+ curvedRow(
+ modifier =
+ curvedModifier
+ .sizeIn(maxSweepDegrees = maxSweepAngle)
+ .padding(contentPadding.toArcPadding())
+ .background(backgroundColor, StrokeCap.Round),
+ radialAlignment = CurvedAlignment.Radial.Center
+ ) {
+ content(currentTime)
}
}
}
-/** Receiver scope which is used by [TimeText]. */
-public sealed class TimeTextScope {
- /**
- * Adds a composable [Text] for non-round devices and [curvedText] for round devices to
- * [TimeText] content. Typically used to add a short status message ahead of the time text.
- *
- * @param text The text to display.
- * @param style configuration for the [text] such as color, font etc.
- * @param weight Size the text's width proportional to its weight relative to other weighted
- * sibling elements in the TimeText. Specify NaN to make this text not have a weight
- * specified. The default value, [TimeTextDefaults.AutoTextWeight], makes this text have
- * weight 1f if it's the only one, and not have weight if there are two or more.
- */
- public abstract fun text(
- text: String,
- style: TextStyle? = null,
- weight: Float = TimeTextDefaults.AutoTextWeight
- )
-
- /** Adds a text displaying current time. */
- public abstract fun time()
-
- /**
- * Adds a separator in [TimeText].
- *
- * @param style configuration for the [separator] such as color, font etc.
- */
- public abstract fun separator(style: TextStyle? = null)
-
- /**
- * Adds a composable in content of [TimeText]. This can be used to display non-text information
- * such as an icon.
- *
- * @param content Slot for the [composable] to be displayed.
- */
- public abstract fun composable(content: @Composable () -> Unit)
-}
-
/** Contains the default values used by [TimeText]. */
public object TimeTextDefaults {
/** The default padding from the edge of the screen. */
private val Padding = PaddingDefaults.edgePadding
+
/** Default format for 24h clock. */
public const val TimeFormat24Hours: String = "HH:mm"
@@ -228,8 +154,8 @@
}
/**
- * Creates a [TextStyle] with default parameters used for showing time on square screens. By
- * default a copy of MaterialTheme.typography.arcMedium style is created.
+ * Creates a [CurvedTextStyle] with default parameters used for showing time. By default a copy
+ * of MaterialTheme.typography.arcMedium style is created.
*
* @param background The background color.
* @param color The main color.
@@ -240,9 +166,11 @@
background: Color = Color.Unspecified,
color: Color = MaterialTheme.colorScheme.onBackground,
fontSize: TextUnit = TextUnit.Unspecified,
- ): TextStyle =
- MaterialTheme.typography.arcMedium +
- TextStyle(color = color, background = background, fontSize = fontSize)
+ ): CurvedTextStyle =
+ CurvedTextStyle(
+ MaterialTheme.typography.arcMedium +
+ TextStyle(color = color, background = background, fontSize = fontSize)
+ )
/**
* Creates a default implementation of [TimeSource] and remembers it. Once the system time
@@ -260,48 +188,37 @@
@Composable
public fun rememberTimeSource(timeFormat: String): TimeSource =
remember(timeFormat) { DefaultTimeSource(timeFormat) }
+}
- /**
- * A default implementation of Separator shown between any text/composable and the time on
- * non-round screens.
- *
- * @param modifier A default modifier for the separator.
- * @param textStyle A [TextStyle] for the separator.
- * @param contentPadding The spacing values between the container and the separator.
- */
- @Composable
- internal fun TextSeparator(
- modifier: Modifier = Modifier,
- textStyle: TextStyle = timeTextStyle(),
- contentPadding: PaddingValues = PaddingValues(horizontal = 4.dp)
+/**
+ * Default curved text to use in a [TimeText], for displaying the time
+ *
+ * @param time The time to display.
+ * @param style A [CurvedTextStyle] to override the style used.
+ */
+public fun CurvedScope.timeTextCurvedText(time: String, style: CurvedTextStyle? = null) {
+ basicCurvedText(
+ time,
) {
- Text(text = "·", style = textStyle, modifier = modifier.padding(contentPadding))
+ style?.let { timeTextStyle() + it } ?: timeTextStyle()
}
+}
- /**
- * A default implementation of Separator shown between any text/composable and the time on round
- * screens.
- *
- * @param curvedTextStyle A [CurvedTextStyle] for the separator.
- * @param contentArcPadding [ArcPaddingValues] for the separator text.
- */
- internal fun CurvedScope.CurvedTextSeparator(
- curvedTextStyle: CurvedTextStyle? = null,
- contentArcPadding: ArcPaddingValues = ArcPaddingValues(angular = 4.dp)
- ) {
- curvedText(
- text = "·",
- style = curvedTextStyle,
- modifier = CurvedModifier.padding(contentArcPadding)
- )
- }
-
- /**
- * Weight value used to specify that the value is automatic. It will be 1f when there is one
- * text, and no weight will be used if there are 2 or more texts. For the 2+ texts case, usually
- * one of them should have weight manually specified to ensure its properly cut and ellipsized.
- */
- public val AutoTextWeight: Float = -1f
+/**
+ * A default implementation of Separator, to be shown between any text/composable and the time.
+ *
+ * @param curvedTextStyle A [CurvedTextStyle] for the separator.
+ * @param contentArcPadding [ArcPaddingValues] for the separator text.
+ */
+public fun CurvedScope.timeTextSeparator(
+ curvedTextStyle: CurvedTextStyle? = null,
+ contentArcPadding: ArcPaddingValues = ArcPaddingValues(angular = 4.dp)
+) {
+ curvedText(
+ text = "·",
+ style = curvedTextStyle,
+ modifier = CurvedModifier.padding(contentArcPadding)
+ )
}
public interface TimeSource {
@@ -314,122 +231,6 @@
@Composable public fun currentTime(): String
}
-/** Implementation of [TimeTextScope] for round devices. */
-internal class CurvedTimeTextScope(
- private val timeText: String,
- private val timeTextStyle: TextStyle,
- private val maxSweepAngle: Float,
- contentColor: Color,
-) : TimeTextScope() {
- private var textCount = 0
- private val pending = mutableListOf<CurvedScope.() -> Unit>()
- private val contentTextStyle = timeTextStyle.merge(contentColor)
-
- override fun text(text: String, style: TextStyle?, weight: Float) {
- textCount++
- pending.add {
- curvedText(
- text = text,
- overflow = TextOverflow.Ellipsis,
- maxSweepAngle = maxSweepAngle,
- style = CurvedTextStyle(style = contentTextStyle.merge(style)),
- modifier =
- if (weight.isValidWeight()) CurvedModifier.weight(weight)
- // Note that we are creating a lambda here, but textCount is actually read
- // later, during the call to Show, when the pending list is fully constructed.
- else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
- CurvedModifier.weight(1f)
- else CurvedModifier
- )
- }
- }
-
- override fun time() {
- pending.add {
- curvedText(
- timeText,
- maxSweepAngle = maxSweepAngle,
- style = CurvedTextStyle(timeTextStyle)
- )
- }
- }
-
- override fun separator(style: TextStyle?) {
- pending.add { CurvedTextSeparator(CurvedTextStyle(style = timeTextStyle.merge(style))) }
- }
-
- override fun composable(content: @Composable () -> Unit) {
- pending.add {
- curvedComposable {
- CompositionLocalProvider(
- LocalContentColor provides contentTextStyle.color,
- LocalTextStyle provides contentTextStyle,
- content = content
- )
- }
- }
- }
-
- fun CurvedScope.Show() {
- pending.fastForEach { it() }
- }
-}
-
-/** Implementation of [TimeTextScope] for non-round devices. */
-internal class LinearTimeTextScope(
- private val timeText: String,
- private val timeTextStyle: TextStyle,
- contentColor: Color,
-) : TimeTextScope() {
- private var textCount = 0
- private val pending = mutableListOf<@Composable RowScope.() -> Unit>()
- private val contentTextStyle = timeTextStyle.merge(contentColor)
-
- override fun text(text: String, style: TextStyle?, weight: Float) {
- textCount++
- pending.add {
- Text(
- text = text,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = contentTextStyle.merge(style),
- modifier =
- if (weight.isValidWeight()) Modifier.weight(weight, fill = false)
- // Note that we are creating a lambda here, but textCount is actually read
- // later, during the call to Show, when the pending list is fully constructed.
- else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
- Modifier.weight(1f, fill = false)
- else Modifier
- )
- }
- }
-
- override fun time() {
- pending.add { Text(timeText, style = timeTextStyle) }
- }
-
- override fun separator(style: TextStyle?) {
- pending.add { TextSeparator(textStyle = timeTextStyle.merge(style)) }
- }
-
- override fun composable(content: @Composable () -> Unit) {
- pending.add {
- CompositionLocalProvider(
- LocalContentColor provides contentTextStyle.color,
- LocalTextStyle provides contentTextStyle,
- content = content
- )
- }
- }
-
- @Composable
- fun RowScope.Show() {
- pending.fastForEach { it() }
- }
-}
-
-private fun Float.isValidWeight() = !isNaN() && this > 0f
-
internal class DefaultTimeSource(timeFormat: String) : TimeSource {
private val _timeFormat = timeFormat
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index a674c88..685cf6d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -109,7 +109,7 @@
@Composable { it: @Composable () -> Unit ->
// Only material3 demos benefit from the Material3 ScreenScaffold
if (category.materialVersion == 3) {
- val timeText = @Composable { androidx.wear.compose.material3.TimeText { time() } }
+ val timeText = @Composable { androidx.wear.compose.material3.TimeText() }
androidx.wear.compose.material3.ScreenScaffold(
scrollState = state,
timeText = remember { timeText },