diff --git a/.agents/skills/run_tests/SKILL.md b/.agents/skills/run_tests/SKILL.md new file mode 100644 index 0000000000000..d513b4ed6a62b --- /dev/null +++ b/.agents/skills/run_tests/SKILL.md @@ -0,0 +1,124 @@ +--- +name: run_tests +description: A skill for identifying and running tests (Unit, Instrumentation, FTL) in the AndroidX repository. +--- + +# Run Tests Skill + +This skill provides comprehensive instructions for executing tests within the AndroidX repository. It covers module discovery, unit testing, instrumentation testing on connected devices, and remote testing via Firebase Test Lab (FTL). + +## 1. How to Find and Run a Specific Test + +If you have a specific failing test (e.g., `BasicTextFieldTest#longText_doesNotCrash_singleLine`) and want to execute it: + +1. **Find the Test File**: Use code search, `find_declaration`, or `find_files` (e.g., search for `BasicTextFieldTest.kt`). +2. **Identify the Module**: The file path determines the Gradle project (e.g., `compose/foundation/foundation/src/...` belongs to `:compose:foundation:foundation`). +3. **Identify Test Type**: + * If the test is in a `test`, `androidHostTest`, or `jvmTest` folder, it is a Unit Test. + * If the test is in `androidTest` or `androidDeviceTest`, it is an Instrumentation Test. +4. **Identify Module Type**: Check if the module is KMP or standard Android (see details below) by inspecting `build.gradle` for `androidXMultiplatform {` or running `./gradlew :tasks | grep "test"`. +5. **Formulate the Task**: Select the proper Gradle task (e.g., `testAndroidHostTest` vs `test`, `connectedAndroidDeviceTest` vs `connectedAndroidTest`). +6. **Filter by Class/Method**: + * **Class**: Append filtering arguments. E.g., for instrumentation: `-Pandroid.testInstrumentationRunnerArguments.class=androidx...BasicTextFieldTest`. For unit tests: `--tests "androidx...BasicTextFieldTest"`. + * **Method**: For instrumentation, append `#` to the class name (e.g., `...BasicTextFieldTest#longText_doesNotCrash_singleLine`). For unit tests, append `.` to the class name in `--tests` (e.g., `--tests "...BasicTextFieldTest.longText_doesNotCrash_singleLine"`). + +## 2. Module Discovery + +Before running tests, you must identify the Gradle project name (module) associated with the code you are testing. + +* **Mapping a path to a project**: Most directories in this repository are Gradle projects. Use the directory structure as a guide (e.g., `appcompat/appcompat` corresponds to `:appcompat:appcompat`). +* **Verification**: Run `./gradlew projects` to see a full list of projects. Because the output is very long, consider chaining a `grep` command to filter the output if you are looking for a specific module (e.g., `./gradlew projects | grep appcompat`). +* **Module Type**: Be aware if the module is a standard Android library (like `appcompat`) or a Kotlin Multiplatform (KMP) library (like `compose:foundation:foundation`). Task names differ significantly. To determine if a module is KMP, you can run `./gradlew :tasks | grep "test"`: if you see tasks like `testAndroidHostTest` or `jvmTest`, it is a KMP module. If you see standard `test` and `connectedAndroidTest` tasks, it is a standard Android module. Alternatively, inspect its `build.gradle` file for `androidXMultiplatform {`. + +## 3. Unit Testing (JVM) + +Unit tests run on the host machine and are the fastest way to verify logic. + +* **Standard Android Modules**: + ```bash + ./gradlew :test + ``` +* **KMP Modules**: + ```bash + ./gradlew :testAndroidHostTest + ./gradlew :jvmStubsTest + ``` +* **Filtering**: + Use the `--tests` filter. You can also append `.` for specific methods. + ```bash + ./gradlew :test --tests "androidx.example.MyTest" + ./gradlew :testAndroidHostTest --tests "androidx.example.MyTest" + ./gradlew :test --tests "androidx.example.MyTest.myMethod" + ``` + +## 4. Instrumentation Testing (Connected Devices) + +These tests run on a physical device or emulator connected via ADB. + +* **Standard Android Modules**: + ```bash + ./gradlew :connectedAndroidTest + ``` +* **KMP Modules**: + ```bash + ./gradlew :connectedAndroidDeviceTest + ``` +* **Filtering**: + Use the `android.testInstrumentationRunnerArguments.class` property. For specific methods, append `#`. + ```bash + ./gradlew :connectedAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest + ./gradlew :connectedAndroidDeviceTest \ + -Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest#myMethod + ``` + +## 5. Remote Testing (Firebase Test Lab) + +AndroidX provides specialized tasks for running **instrumentation tests** on Firebase Test Lab (FTL). FTL is not used for local unit tests. The task suffix changes based on module type. + +* **Standard Android Modules**: `ftlreleaseAndroidTest` +* **KMP Modules**: `ftlandroidDeviceTest` +* **Listing Available Combinations**: + To see the full list of available FTL tasks for a specific project, you can run: + ```bash + ./gradlew :tasks --all | grep ftl + ``` +* **Common Device/API combinations**: + - `mediumphoneapi36` + - `mediumphoneapi35` + - `mediumphoneapi34` + - `mediumphoneapi30` + - `nexus5api23` +* **Example (KMP)**: + ```bash + ./gradlew :ftlmediumphoneapi35androidDeviceTest --className="androidx.example.MyTest" + ``` + +### 5.1. Reproducing Flakes on FTL + +To verify a flaky test, you can run it multiple times on Firebase Test Lab using the `ftlOnApis` task variant. + +1. **Parameterize the Test**: To run a test N times, temporarily modify the test class to be parameterized: + * Add `@RunWith(Parameterized::class)` to the class. + * Update the constructor to accept a repetition parameter: `class MyTest(private val repetition: Int)`. + * Add the companion object for data generation: + ```kotlin + import org.junit.runners.Parameterized + + companion object { + private const val RUNS = 100 // Adjust as needed + @JvmStatic + @Parameterized.Parameters + fun data(): Array = Array(RUNS) { 0 } + } + ``` + * *Note*: If there are many tests in the class but you only want to focus on one, comment out the `@Test` annotation on the irrelevant ones. DO NOT REPLACE ANY LINES OF CODE OR VARIABLES NAMES in the test class aside from making it parameterized. Test class name should remain the same. +2. **Run Locally (Optional)**: Verify it runs a few times locally before deploying to FTL. E.g., for KMP: + ```bash + ./gradlew :connectedAndroidDeviceTest -Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest + ``` +3. **Run on FTL**: Use the `ftlOnApis` task. Specify the API level(s) with `--api` and add a longer timeout `--testTimeout=1h` (if the API level is not provided, you must ask the user for it). + ```bash + # Example for KMP (append 'releaseAndroidTest' instead of 'androidDeviceTest' for standard Android) + ./gradlew :ftlOnApisandroidDeviceTest --testTimeout=1h --api 28 --api 30 --className=androidx.example.MyTest + ``` \ No newline at end of file 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 850ae99db7f2e..32bb65ae0cb52 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 @@ -115,6 +115,11 @@ object IntrospectionHelper { ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "CompileTimeAppFunctionMetadata") val APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionFunctionNotFoundException") + val APP_FUNCTION_CANCELLED_EXCEPTION_CLASS = + ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionCancelledException") + val APP_FUNCTION_APP_UNKNOWN_EXCEPTION_CLASS = + ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionAppUnknownException") + val APP_FUNCTION_EXCEPTION_CLASS = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionException") val APP_FUNCTION_SCHEMA_METADATA_CLASS = ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionSchemaMetadata") val APP_FUNCTION_PARAMETER_METADATA_CLASS = @@ -161,6 +166,10 @@ object IntrospectionHelper { val APP_FUNCTION_RESPONSE_METADATA_CLASS = ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionResponseMetadata") + val DEPENDENCIES_CLASS = ClassName(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, "Dependencies") + val CANCELLATION_EXCEPTION_CLASS = ClassName("kotlinx.coroutines", "CancellationException") + val EXCEPTION_CLASS = ClassName("kotlin", "Exception") + object ConfigurableAppFunctionFactoryClass { val CLASS_NAME = ClassName(APP_FUNCTIONS_SERVICE_INTERNAL_PACKAGE_NAME, "ConfigurableAppFunctionFactory") @@ -184,12 +193,27 @@ object IntrospectionHelper { } } + object AppFunctionExecutionDispatcherClass { + val CLASS_NAME = + ClassName(APP_FUNCTIONS_SERVICE_INTERNAL_PACKAGE_NAME, "AppFunctionExecutionDispatcher") + + object ExecuteAppFunctionMethod { + const val METHOD_NAME = "executeAppFunction" + } + } + object ExecuteAppFunctionRequestClass { val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "ExecuteAppFunctionRequest") } object ExecuteAppFunctionResponseClass { val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "ExecuteAppFunctionResponse") + val SUCCESS_CLASS_NAME = CLASS_NAME.nestedClass("Success") + } + + object ServiceInternalHelper { + const val UNSAFE_GET_PARAMETER_VALUE_METHOD = "unsafeGetParameterValue" + const val UNSAFE_BUILD_RETURN_VALUE_METHOD = "unsafeBuildReturnValue" } object AppFunctionInvokerClass { diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionEntryPointProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionEntryPointProcessor.kt index 3cc68304d0d46..46bf0839f62c5 100644 --- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionEntryPointProcessor.kt +++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionEntryPointProcessor.kt @@ -20,11 +20,14 @@ import androidx.annotation.VisibleForTesting import androidx.appfunctions.compiler.AppFunctionCompiler import androidx.appfunctions.compiler.core.AnnotatedAppFunctionEntryPoint import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver +import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS +import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionExecutionDispatcherClass import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionServiceClass import androidx.appfunctions.compiler.core.IntrospectionHelper.ExecuteAppFunctionRequestClass import androidx.appfunctions.compiler.core.IntrospectionHelper.ExecuteAppFunctionResponseClass import androidx.appfunctions.compiler.core.ProcessingException import androidx.appfunctions.compiler.core.logException +import androidx.appfunctions.compiler.core.toTypeName import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger @@ -34,10 +37,12 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.buildCodeBlock /** * The processor to generate the AppFunctionService subclass for an AppFunction entry point. @@ -80,6 +85,7 @@ class AppFunctionEntryPointProcessor( try { entryPoint.validate() generateAppFunctionService(entryPoint) + // TODO(b/463909015): Generate XML } catch (e: ProcessingException) { logger.logException(e) } @@ -98,19 +104,7 @@ class AppFunctionEntryPointProcessor( TypeSpec.classBuilder(serviceName) .superclass(originalClassName) .addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION) - val executeFunctionBuilder = - FunSpec.builder(AppFunctionServiceClass.ExecuteFunctionMethod.METHOD_NAME) - .addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND) - .addParameter( - AppFunctionServiceClass.ExecuteFunctionMethod.REQUEST_PARAM_NAME, - ExecuteAppFunctionRequestClass.CLASS_NAME, - ) - .returns(ExecuteAppFunctionResponseClass.CLASS_NAME) - // TODO(b/463909015): Implement the routing logic. - .addStatement("TODO(%S)", "Not yet implemented") - .build() - - serviceClassBuilder.addFunction(executeFunctionBuilder) + .addFunction(buildExecuteFunction(entryPoint)) val fileSpec = FileSpec.builder(packageName, serviceName).addType(serviceClassBuilder.build()).build() @@ -128,6 +122,50 @@ class AppFunctionEntryPointProcessor( .use { fileSpec.writeTo(it) } } + private fun buildExecuteFunction(entryPoint: AnnotatedAppFunctionEntryPoint): FunSpec { + return FunSpec.builder(AppFunctionServiceClass.ExecuteFunctionMethod.METHOD_NAME) + .addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND) + .addParameter( + AppFunctionServiceClass.ExecuteFunctionMethod.REQUEST_PARAM_NAME, + ExecuteAppFunctionRequestClass.CLASS_NAME, + ) + .returns(ExecuteAppFunctionResponseClass.CLASS_NAME) + .addCode(buildExecuteFunctionBody(entryPoint)) + .build() + } + + private fun buildExecuteFunctionBody(entryPoint: AnnotatedAppFunctionEntryPoint): CodeBlock { + return buildCodeBlock { + beginControlFlow( + "return %T.%L(request) { parameters ->", + AppFunctionExecutionDispatcherClass.CLASS_NAME, + AppFunctionExecutionDispatcherClass.ExecuteAppFunctionMethod.METHOD_NAME, + ) + beginControlFlow("when (request.functionIdentifier)") + for (function in entryPoint.annotatedAppFunctions.appFunctionDeclarations) { + val identifier = entryPoint.annotatedAppFunctions.getAppFunctionIdentifier(function) + beginControlFlow("%S ->", identifier) + add("this.%N(\n", function.simpleName.asString()) + indent() + for (param in function.parameters) { + val paramName = param.name!!.asString() + addStatement("parameters[%S] as %T,", paramName, param.type.toTypeName()) + } + unindent() + addStatement(")") + endControlFlow() + } + beginControlFlow("else ->") + addStatement( + "throw %T(\n \"\${request.functionIdentifier} is not available\"\n)", + APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS, + ) + endControlFlow() // end else + endControlFlow() // end when + endControlFlow() // end executeAppFunction + } + } + @VisibleForTesting class Provider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/input/entrypoints/valid/SimpleEntryPoint.KT b/appfunctions/appfunctions-compiler/src/test/test-data/input/entrypoints/valid/SimpleEntryPoint.KT index 8581b69e65d7c..f4b7bfa8056d2 100644 --- a/appfunctions/appfunctions-compiler/src/test/test-data/input/entrypoints/valid/SimpleEntryPoint.KT +++ b/appfunctions/appfunctions-compiler/src/test/test-data/input/entrypoints/valid/SimpleEntryPoint.KT @@ -6,7 +6,10 @@ import androidx.appfunctions.service.AppFunction @AppFunctionEntryPoint("MySimpleService", "app_functions_v2.xml") abstract class SimpleEntryPoint: AppFunctionService() { - @AppFunction fun simpleFunction() {} + @AppFunction + internal fun simpleFunction(int: Int, string: String): String { + TODO("Not implemented") + } override fun onCreate() { diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/output/entrypoints/MySimpleService.KT b/appfunctions/appfunctions-compiler/src/test/test-data/output/entrypoints/MySimpleService.KT index 872f903e2b76a..171897a303918 100644 --- a/appfunctions/appfunctions-compiler/src/test/test-data/output/entrypoints/MySimpleService.KT +++ b/appfunctions/appfunctions-compiler/src/test/test-data/output/entrypoints/MySimpleService.KT @@ -1,12 +1,28 @@ package com.testdata +import androidx.appfunctions.AppFunctionFunctionNotFoundException import androidx.appfunctions.ExecuteAppFunctionRequest import androidx.appfunctions.ExecuteAppFunctionResponse +import androidx.appfunctions.service.`internal`.AppFunctionExecutionDispatcher import javax.`annotation`.processing.Generated +import kotlin.Int +import kotlin.String @Generated("androidx.appfunctions.compiler.AppFunctionCompiler") public class MySimpleService : SimpleEntryPoint() { - override suspend fun executeFunction(request: ExecuteAppFunctionRequest): ExecuteAppFunctionResponse { - TODO("Not yet implemented") + override suspend fun executeFunction(request: ExecuteAppFunctionRequest): ExecuteAppFunctionResponse = AppFunctionExecutionDispatcher.executeAppFunction(request) { parameters -> + when (request.functionIdentifier) { + "com.testdata.SimpleEntryPoint#simpleFunction" -> { + this.simpleFunction( + parameters["int"] as Int, + parameters["string"] as String, + ) + } + else -> { + throw AppFunctionFunctionNotFoundException( + "${request.functionIdentifier} is not available" + ) + } + } } } diff --git a/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/internal/$AggregatedAppFunctionInventory_Impl.kt b/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/internal/$AggregatedAppFunctionInventory_Impl.kt index 0d0a97b6ab682..97923ae2b06b5 100644 --- a/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/internal/$AggregatedAppFunctionInventory_Impl.kt +++ b/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/internal/$AggregatedAppFunctionInventory_Impl.kt @@ -20,6 +20,7 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.appfunctions.core.AppFunctionMetadataTestHelper import androidx.appfunctions.metadata.AppFunctionComponentsMetadata +import androidx.appfunctions.metadata.AppFunctionIntTypeMetadata import androidx.appfunctions.metadata.AppFunctionLongTypeMetadata import androidx.appfunctions.metadata.AppFunctionParameterMetadata import androidx.appfunctions.metadata.AppFunctionResponseMetadata @@ -37,6 +38,26 @@ class `$AggregatedAppFunctionInventory_Impl` : AggregatedAppFunctionInventory() override val functionIdToMetadataMap: Map get() = mapOf( + AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT to + CompileTimeAppFunctionMetadata( + id = + AppFunctionMetadataTestHelper.FunctionIds + .NO_SCHEMA_ENABLED_BY_DEFAULT, + isEnabledByDefault = true, + schema = null, + parameters = + listOf( + AppFunctionParameterMetadata( + name = "intParam", + isRequired = true, + dataType = AppFunctionIntTypeMetadata(isNullable = false), + ) + ), + response = + AppFunctionResponseMetadata( + valueType = AppFunctionUnitTypeMetadata(isNullable = false) + ), + ), AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED to CompileTimeAppFunctionMetadata( id = diff --git a/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcherTest.kt b/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcherTest.kt new file mode 100644 index 0000000000000..e110c6280fb15 --- /dev/null +++ b/appfunctions/appfunctions-service/src/androidTest/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcherTest.kt @@ -0,0 +1,144 @@ +/* + * 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.service.internal + +import android.os.Build +import androidx.appfunctions.AppFunctionAppUnknownException +import androidx.appfunctions.AppFunctionCancelledException +import androidx.appfunctions.AppFunctionData +import androidx.appfunctions.AppFunctionDeniedException +import androidx.appfunctions.AppFunctionFunctionNotFoundException +import androidx.appfunctions.ExecuteAppFunctionRequest +import androidx.appfunctions.ExecuteAppFunctionResponse +import androidx.appfunctions.core.AppFunctionMetadataTestHelper +import androidx.appfunctions.metadata.AppFunctionComponentsMetadata +import androidx.test.filters.SdkSuppress +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertThrows +import org.junit.Test + +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) +class AppFunctionExecutionDispatcherTest { + + @Test + fun executeAppFunction_succeeds() = runBlocking { + val request = + ExecuteAppFunctionRequest( + "test_target_package", + AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, + AppFunctionData.Builder( + AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_ENABLED_BY_DEFAULT + .parameters, + AppFunctionComponentsMetadata(), + ) + .setInt("intParam", 100) + .build(), + ) + + var intParamValue: Any? = null + val response = + AppFunctionExecutionDispatcher.executeAppFunction(request) { params -> + intParamValue = params["intParam"] + Unit + } + + assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Success::class.java) + assertThat(intParamValue).isEqualTo(100) + } + + @Test + fun executeAppFunction_throwsFunctionNotFoundException() = runBlocking { + val request = + ExecuteAppFunctionRequest( + "test_target_package", + "non_existent_function_id", + AppFunctionData.EMPTY, + ) + + val exception = + assertThrows(AppFunctionFunctionNotFoundException::class.java) { + runBlocking { + AppFunctionExecutionDispatcher.executeAppFunction(request) { params -> + "Result" + } + } + } + assertThat(exception.errorMessage).isEqualTo("non_existent_function_id is not available") + } + + @Test + fun executeAppFunction_throwsAppFunctionCancelledException() = runBlocking { + val request = + ExecuteAppFunctionRequest( + "test_target_package", + AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, + AppFunctionData.EMPTY, + ) + + val exception = + assertThrows(AppFunctionCancelledException::class.java) { + runBlocking { + AppFunctionExecutionDispatcher.executeAppFunction(request) { params -> + throw CancellationException("Cancelled") + } + } + } + assertThat(exception.message).isEqualTo("Cancelled") + } + + @Test + fun executeAppFunction_throwsAppFunctionException() = runBlocking { + val request = + ExecuteAppFunctionRequest( + "test_target_package", + AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, + AppFunctionData.EMPTY, + ) + + val exception = + assertThrows(AppFunctionDeniedException::class.java) { + runBlocking { + AppFunctionExecutionDispatcher.executeAppFunction(request) { params -> + throw AppFunctionDeniedException("Specific Exception") + } + } + } + assertThat(exception.errorMessage).isEqualTo("Specific Exception") + } + + @Test + fun executeAppFunction_throwsAppFunctionAppUnknownException() = runBlocking { + val request = + ExecuteAppFunctionRequest( + "test_target_package", + AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, + AppFunctionData.EMPTY, + ) + + val exception = + assertThrows(AppFunctionAppUnknownException::class.java) { + runBlocking { + AppFunctionExecutionDispatcher.executeAppFunction(request) { params -> + throw IllegalStateException("Generic Exception") + } + } + } + assertThat(exception.message).isEqualTo("Generic Exception") + } +} diff --git a/appfunctions/appfunctions-service/src/main/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcher.kt b/appfunctions/appfunctions-service/src/main/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcher.kt new file mode 100644 index 0000000000000..f4fbaace4ffb9 --- /dev/null +++ b/appfunctions/appfunctions-service/src/main/java/androidx/appfunctions/service/internal/AppFunctionExecutionDispatcher.kt @@ -0,0 +1,84 @@ +/* + * 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.service.internal + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.RestrictTo +import androidx.appfunctions.AppFunctionAppUnknownException +import androidx.appfunctions.AppFunctionCancelledException +import androidx.appfunctions.AppFunctionException +import androidx.appfunctions.AppFunctionFunctionNotFoundException +import androidx.appfunctions.ExecuteAppFunctionRequest +import androidx.appfunctions.ExecuteAppFunctionResponse +import androidx.appfunctions.internal.Dependencies +import androidx.appfunctions.metadata.AppFunctionComponentsMetadata +import kotlinx.coroutines.CancellationException + +/** Helper class for generated AppFunction services to execute an AppFunction. */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +public object AppFunctionExecutionDispatcher { + + /** + * Executes an AppFunction with the given request. + * + * @param request The request to execute. + * @param block The block of code to execute. The block will be invoked with a map of parameter + * names to their extracted values. + * @return The response of the execution. + * @throws AppFunctionFunctionNotFoundException if the function is not available in the + * inventory. + * @throws AppFunctionCancelledException if the execution is cancelled. + * @throws AppFunctionException if an explicit AppFunctionException is thrown during execution. + * @throws AppFunctionAppUnknownException if any other exception is thrown during execution. + */ + public suspend fun executeAppFunction( + request: ExecuteAppFunctionRequest, + block: suspend (Map) -> Any?, + ): ExecuteAppFunctionResponse { + try { + val inventory = Dependencies.aggregatedAppFunctionInventory + val appFunctionMetadata = + inventory?.functionIdToMetadataMap?.get(request.functionIdentifier) + if (appFunctionMetadata == null) { + throw AppFunctionFunctionNotFoundException( + "${request.functionIdentifier} is not available" + ) + } + val parameters = buildMap { + for (parameterMetadata in appFunctionMetadata.parameters) { + this[parameterMetadata.name] = + request.functionParameters.unsafeGetParameterValue(parameterMetadata) + } + } + val result = block(parameters) + val returnValue = + appFunctionMetadata.response.unsafeBuildReturnValue( + result, + inventory?.componentsMetadata ?: AppFunctionComponentsMetadata(emptyMap()), + ) + return ExecuteAppFunctionResponse.Success(returnValue) + } catch (e: CancellationException) { + throw AppFunctionCancelledException(e.message) + } catch (e: AppFunctionException) { + throw e + } catch (e: Exception) { + throw AppFunctionAppUnknownException(e.message) + } + } +} diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeImageReader.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeImageReader.kt index ded16fa000e84..67720e00c7384 100644 --- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeImageReader.kt +++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeImageReader.kt @@ -16,6 +16,7 @@ package androidx.camera.camera2.pipe.testing +import android.hardware.HardwareBuffer import android.util.Size import android.view.Surface import androidx.camera.camera2.pipe.OutputId @@ -52,11 +53,16 @@ private constructor( * Simulate an image at a specific [imageTimestamp] for a particular (optional) [OutputId]. The * timebase for an imageReader is left undefined. */ - public fun simulateImage(imageTimestamp: Long, outputId: OutputId? = null): FakeImage { + public fun simulateImage( + imageTimestamp: Long, + outputId: OutputId? = null, + hardwareBuffer: HardwareBuffer? = null, + ): FakeImage { val output = outputId ?: outputs.keys.single() val size = checkNotNull(outputs[output]) { "Unexpected $output! Available outputs are $outputs" } - val image = FakeImage(size.width, size.height, format.value, imageTimestamp) + + val image = FakeImage(size.width, size.height, format.value, imageTimestamp, hardwareBuffer) simulateImage(image, output) return image } diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageReaderTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageReaderTest.kt index 066c468f08cef..73200995bf5f4 100644 --- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageReaderTest.kt +++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageReaderTest.kt @@ -16,6 +16,8 @@ package androidx.camera.camera2.pipe.testing +import android.hardware.HardwareBuffer +import android.os.Build import android.util.Size import androidx.camera.camera2.pipe.OutputId import androidx.camera.camera2.pipe.StreamFormat @@ -30,7 +32,13 @@ import org.robolectric.annotation.Config @Config(sdk = [Config.ALL_SDKS]) class FakeImageReaderTest { private val imageReader = - FakeImageReader.create(StreamFormat.PRIVATE, StreamId(32), OutputId(42), Size(640, 480), 10) + FakeImageReader.create( + StreamFormat.PRIVATE, + StreamId(32), + OutputId(42), + Size(IMAGE_WIDTH, IMAGE_HEIGHT), + 10, + ) @After fun cleanup() { @@ -47,13 +55,18 @@ class FakeImageReaderTest { } @Test + @Config(minSdk = Build.VERSION_CODES.P) fun imageReaderCanSimulateImages() { - val fakeImage = imageReader.simulateImage(100) + val hardwareBuffer = HardwareBuffer.create(IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, 1, 1) + val fakeImage = imageReader.simulateImage(100, hardwareBuffer = hardwareBuffer) - assertThat(fakeImage.width).isEqualTo(640) - assertThat(fakeImage.height).isEqualTo(480) + assertThat(fakeImage.width).isEqualTo(IMAGE_WIDTH) + assertThat(fakeImage.height).isEqualTo(IMAGE_HEIGHT) assertThat(fakeImage.format).isEqualTo(StreamFormat.PRIVATE.value) assertThat(fakeImage.timestamp).isEqualTo(100) + assertThat(fakeImage.hardwareBuffer).isNotNull() + assertThat(fakeImage.hardwareBuffer?.width).isEqualTo(IMAGE_WIDTH) + assertThat(fakeImage.hardwareBuffer?.height).isEqualTo(IMAGE_HEIGHT) assertThat(fakeImage.isClosed).isFalse() } @@ -98,4 +111,10 @@ class FakeImageReaderTest { assertThat(fakeListener.onImageEvents[1].streamId).isEqualTo(StreamId(32)) assertThat(fakeListener.onImageEvents[1].outputId).isEqualTo(OutputId(42)) } + + companion object { + private val IMAGE_HEIGHT: Int = 480 + private val IMAGE_WIDTH: Int = 640 + private val IMAGE_FORMAT: Int = 3 + } } diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt index 913c8628f6829..1d421195d6e32 100644 --- a/compose/material3/material3/api/current.txt +++ b/compose/material3/material3/api/current.txt @@ -3209,6 +3209,51 @@ package androidx.compose.material3 { method @BytecodeOnly @androidx.compose.runtime.Composable public static void Scrim-yrwZFoE(String?, androidx.compose.ui.Modifier?, kotlin.jvm.functions.Function0?, kotlin.jvm.functions.Function0?, long, androidx.compose.runtime.Composer?, int, int); } + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ScrollFieldColors { + ctor @KotlinOnly public ScrollFieldColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color unselectedContentColor, androidx.compose.ui.graphics.Color selectedContentColor); + ctor @BytecodeOnly public ScrollFieldColors(long, long, long, kotlin.jvm.internal.DefaultConstructorMarker!); + method @KotlinOnly public androidx.compose.material3.ScrollFieldColors copy(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color unselectedContentColor, optional androidx.compose.ui.graphics.Color selectedContentColor); + method @BytecodeOnly public androidx.compose.material3.ScrollFieldColors copy-ysEtTa8(long, long, long); + method @BytecodeOnly public static androidx.compose.material3.ScrollFieldColors! copy-ysEtTa8$default(androidx.compose.material3.ScrollFieldColors!, long, long, long, int, Object!); + method @BytecodeOnly public long getContainerColor-0d7_KjU(); + method @BytecodeOnly public long getSelectedContentColor-0d7_KjU(); + method @BytecodeOnly public long getUnselectedContentColor-0d7_KjU(); + property public androidx.compose.ui.graphics.Color containerColor; + property public androidx.compose.ui.graphics.Color selectedContentColor; + property public androidx.compose.ui.graphics.Color unselectedContentColor; + } + + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public final class ScrollFieldDefaults { + method @KotlinOnly @androidx.compose.runtime.Composable public void Item(int index, boolean selected, optional androidx.compose.material3.ScrollFieldColors colors); + method @BytecodeOnly @androidx.compose.runtime.Composable public void Item(int, boolean, androidx.compose.material3.ScrollFieldColors?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(androidx.compose.runtime.Composer?, int); + method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color unselectedContentColor, optional androidx.compose.ui.graphics.Color selectedContentColor); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors-RGew2ao(long, long, long, androidx.compose.runtime.Composer?, int, int); + method @BytecodeOnly public float getScrollFieldHeight-D9Ej5fM(); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape(androidx.compose.runtime.Composer?, int); + property public androidx.compose.ui.unit.Dp ScrollFieldHeight; + property @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape shape; + field public static final androidx.compose.material3.ScrollFieldDefaults INSTANCE; + } + + @SuppressCompatibility public final class ScrollFieldKt { + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ScrollField(androidx.compose.material3.ScrollFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.ScrollFieldColors colors, optional kotlin.jvm.functions.Function2 field); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ScrollField(androidx.compose.material3.ScrollFieldState, androidx.compose.ui.Modifier?, androidx.compose.material3.ScrollFieldColors?, kotlin.jvm.functions.Function4?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.ScrollFieldState rememberScrollFieldState(int itemCount, optional int index); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.ScrollFieldState rememberScrollFieldState(int, int, androidx.compose.runtime.Composer?, int, int); + } + + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public final class ScrollFieldState { + ctor public ScrollFieldState(androidx.compose.foundation.pager.PagerState pagerState, int itemCount); + method public suspend Object? animateScrollToOption(int option, kotlin.coroutines.Continuation); + method @InaccessibleFromKotlin public int getItemCount(); + method @InaccessibleFromKotlin public int getSelectedOption(); + method public suspend Object? scrollToOption(int option, kotlin.coroutines.Continuation); + property public int itemCount; + property public int selectedOption; + } + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors { ctor @KotlinOnly @Deprecated public SearchBarColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color dividerColor); ctor @KotlinOnly public SearchBarColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color dividerColor, androidx.compose.material3.TextFieldColors inputFieldColors); diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt index ffd469a290656..f0c9ffa2713e3 100644 --- a/compose/material3/material3/api/restricted_current.txt +++ b/compose/material3/material3/api/restricted_current.txt @@ -3212,6 +3212,51 @@ package androidx.compose.material3 { method @BytecodeOnly @androidx.compose.runtime.Composable public static void Scrim-yrwZFoE(String?, androidx.compose.ui.Modifier?, kotlin.jvm.functions.Function0?, kotlin.jvm.functions.Function0?, long, androidx.compose.runtime.Composer?, int, int); } + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ScrollFieldColors { + ctor @KotlinOnly public ScrollFieldColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color unselectedContentColor, androidx.compose.ui.graphics.Color selectedContentColor); + ctor @BytecodeOnly public ScrollFieldColors(long, long, long, kotlin.jvm.internal.DefaultConstructorMarker!); + method @KotlinOnly public androidx.compose.material3.ScrollFieldColors copy(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color unselectedContentColor, optional androidx.compose.ui.graphics.Color selectedContentColor); + method @BytecodeOnly public androidx.compose.material3.ScrollFieldColors copy-ysEtTa8(long, long, long); + method @BytecodeOnly public static androidx.compose.material3.ScrollFieldColors! copy-ysEtTa8$default(androidx.compose.material3.ScrollFieldColors!, long, long, long, int, Object!); + method @BytecodeOnly public long getContainerColor-0d7_KjU(); + method @BytecodeOnly public long getSelectedContentColor-0d7_KjU(); + method @BytecodeOnly public long getUnselectedContentColor-0d7_KjU(); + property public androidx.compose.ui.graphics.Color containerColor; + property public androidx.compose.ui.graphics.Color selectedContentColor; + property public androidx.compose.ui.graphics.Color unselectedContentColor; + } + + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public final class ScrollFieldDefaults { + method @KotlinOnly @androidx.compose.runtime.Composable public void Item(int index, boolean selected, optional androidx.compose.material3.ScrollFieldColors colors); + method @BytecodeOnly @androidx.compose.runtime.Composable public void Item(int, boolean, androidx.compose.material3.ScrollFieldColors?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(androidx.compose.runtime.Composer?, int); + method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color unselectedContentColor, optional androidx.compose.ui.graphics.Color selectedContentColor); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.ScrollFieldColors colors-RGew2ao(long, long, long, androidx.compose.runtime.Composer?, int, int); + method @BytecodeOnly public float getScrollFieldHeight-D9Ej5fM(); + method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape(androidx.compose.runtime.Composer?, int); + property public androidx.compose.ui.unit.Dp ScrollFieldHeight; + property @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape shape; + field public static final androidx.compose.material3.ScrollFieldDefaults INSTANCE; + } + + @SuppressCompatibility public final class ScrollFieldKt { + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ScrollField(androidx.compose.material3.ScrollFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.ScrollFieldColors colors, optional kotlin.jvm.functions.Function2 field); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ScrollField(androidx.compose.material3.ScrollFieldState, androidx.compose.ui.Modifier?, androidx.compose.material3.ScrollFieldColors?, kotlin.jvm.functions.Function4?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.ScrollFieldState rememberScrollFieldState(int itemCount, optional int index); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.ScrollFieldState rememberScrollFieldState(int, int, androidx.compose.runtime.Composer?, int, int); + } + + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public final class ScrollFieldState { + ctor public ScrollFieldState(androidx.compose.foundation.pager.PagerState pagerState, int itemCount); + method public suspend Object? animateScrollToOption(int option, kotlin.coroutines.Continuation); + method @InaccessibleFromKotlin public int getItemCount(); + method @InaccessibleFromKotlin public int getSelectedOption(); + method public suspend Object? scrollToOption(int option, kotlin.coroutines.Continuation); + property public int itemCount; + property public int selectedOption; + } + @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors { ctor @KotlinOnly @Deprecated public SearchBarColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color dividerColor); ctor @KotlinOnly public SearchBarColors(androidx.compose.ui.graphics.Color containerColor, androidx.compose.ui.graphics.Color dividerColor, androidx.compose.material3.TextFieldColors inputFieldColors); diff --git a/compose/material3/material3/bcv/native/current.txt b/compose/material3/material3/bcv/native/current.txt index e1f1e81550767..6e3b8d1689c1a 100644 --- a/compose/material3/material3/bcv/native/current.txt +++ b/compose/material3/material3/bcv/native/current.txt @@ -1448,6 +1448,33 @@ final class androidx.compose.material3/RippleConfiguration { // androidx.compose final fun toString(): kotlin/String // androidx.compose.material3/RippleConfiguration.toString|toString(){}[0] } +final class androidx.compose.material3/ScrollFieldColors { // androidx.compose.material3/ScrollFieldColors|null[0] + constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // androidx.compose.material3/ScrollFieldColors.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] + + final val containerColor // androidx.compose.material3/ScrollFieldColors.containerColor|{}containerColor[0] + final fun (): androidx.compose.ui.graphics/Color // androidx.compose.material3/ScrollFieldColors.containerColor.|(){}[0] + final val selectedContentColor // androidx.compose.material3/ScrollFieldColors.selectedContentColor|{}selectedContentColor[0] + final fun (): androidx.compose.ui.graphics/Color // androidx.compose.material3/ScrollFieldColors.selectedContentColor.|(){}[0] + final val unselectedContentColor // androidx.compose.material3/ScrollFieldColors.unselectedContentColor|{}unselectedContentColor[0] + final fun (): androidx.compose.ui.graphics/Color // androidx.compose.material3/ScrollFieldColors.unselectedContentColor.|(){}[0] + + final fun copy(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): androidx.compose.material3/ScrollFieldColors // androidx.compose.material3/ScrollFieldColors.copy|copy(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.material3/ScrollFieldColors.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // androidx.compose.material3/ScrollFieldColors.hashCode|hashCode(){}[0] +} + +final class androidx.compose.material3/ScrollFieldState { // androidx.compose.material3/ScrollFieldState|null[0] + constructor (androidx.compose.foundation.pager/PagerState, kotlin/Int) // androidx.compose.material3/ScrollFieldState.|(androidx.compose.foundation.pager.PagerState;kotlin.Int){}[0] + + final val itemCount // androidx.compose.material3/ScrollFieldState.itemCount|{}itemCount[0] + final fun (): kotlin/Int // androidx.compose.material3/ScrollFieldState.itemCount.|(){}[0] + final val selectedOption // androidx.compose.material3/ScrollFieldState.selectedOption|{}selectedOption[0] + final fun (): kotlin/Int // androidx.compose.material3/ScrollFieldState.selectedOption.|(){}[0] + + final suspend fun animateScrollToOption(kotlin/Int) // androidx.compose.material3/ScrollFieldState.animateScrollToOption|animateScrollToOption(kotlin.Int){}[0] + final suspend fun scrollToOption(kotlin/Int) // androidx.compose.material3/ScrollFieldState.scrollToOption|scrollToOption(kotlin.Int){}[0] +} + final class androidx.compose.material3/SegmentedButtonColors { // androidx.compose.material3/SegmentedButtonColors|null[0] constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // androidx.compose.material3/SegmentedButtonColors.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] @@ -3268,6 +3295,17 @@ final object androidx.compose.material3/ScrimDefaults { // androidx.compose.mate final fun (androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color // androidx.compose.material3/ScrimDefaults.color.|(androidx.compose.runtime.Composer?;kotlin.Int){}[0] } +final object androidx.compose.material3/ScrollFieldDefaults { // androidx.compose.material3/ScrollFieldDefaults|null[0] + final val ScrollFieldHeight // androidx.compose.material3/ScrollFieldDefaults.ScrollFieldHeight|{}ScrollFieldHeight[0] + final fun (): androidx.compose.ui.unit/Dp // androidx.compose.material3/ScrollFieldDefaults.ScrollFieldHeight.|(){}[0] + final val shape // androidx.compose.material3/ScrollFieldDefaults.shape|{}shape[0] + final fun (androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Shape // androidx.compose.material3/ScrollFieldDefaults.shape.|(androidx.compose.runtime.Composer?;kotlin.Int){}[0] + + final fun Item(kotlin/Int, kotlin/Boolean, androidx.compose.material3/ScrollFieldColors?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ScrollFieldDefaults.Item|Item(kotlin.Int;kotlin.Boolean;androidx.compose.material3.ScrollFieldColors?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] + final fun colors(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.material3/ScrollFieldColors // androidx.compose.material3/ScrollFieldDefaults.colors|colors(androidx.compose.runtime.Composer?;kotlin.Int){}[0] + final fun colors(androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/ScrollFieldColors // androidx.compose.material3/ScrollFieldDefaults.colors|colors(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +} + final object androidx.compose.material3/SegmentedButtonDefaults { // androidx.compose.material3/SegmentedButtonDefaults|null[0] final val BorderWidth // androidx.compose.material3/SegmentedButtonDefaults.BorderWidth|{}BorderWidth[0] final fun (): androidx.compose.ui.unit/Dp // androidx.compose.material3/SegmentedButtonDefaults.BorderWidth.|(){}[0] @@ -3954,6 +3992,9 @@ final val androidx.compose.material3/androidx_compose_material3_RippleThemeConfi final val androidx.compose.material3/androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop // androidx.compose.material3/androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop|#static{}androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop[0] final val androidx.compose.material3/androidx_compose_material3_ScaffoldDefaults$stableprop // androidx.compose.material3/androidx_compose_material3_ScaffoldDefaults$stableprop|#static{}androidx_compose_material3_ScaffoldDefaults$stableprop[0] final val androidx.compose.material3/androidx_compose_material3_ScrimDefaults$stableprop // androidx.compose.material3/androidx_compose_material3_ScrimDefaults$stableprop|#static{}androidx_compose_material3_ScrimDefaults$stableprop[0] +final val androidx.compose.material3/androidx_compose_material3_ScrollFieldColors$stableprop // androidx.compose.material3/androidx_compose_material3_ScrollFieldColors$stableprop|#static{}androidx_compose_material3_ScrollFieldColors$stableprop[0] +final val androidx.compose.material3/androidx_compose_material3_ScrollFieldDefaults$stableprop // androidx.compose.material3/androidx_compose_material3_ScrollFieldDefaults$stableprop|#static{}androidx_compose_material3_ScrollFieldDefaults$stableprop[0] +final val androidx.compose.material3/androidx_compose_material3_ScrollFieldState$stableprop // androidx.compose.material3/androidx_compose_material3_ScrollFieldState$stableprop|#static{}androidx_compose_material3_ScrollFieldState$stableprop[0] final val androidx.compose.material3/androidx_compose_material3_SearchBarColors$stableprop // androidx.compose.material3/androidx_compose_material3_SearchBarColors$stableprop|#static{}androidx_compose_material3_SearchBarColors$stableprop[0] final val androidx.compose.material3/androidx_compose_material3_SearchBarDefaults$stableprop // androidx.compose.material3/androidx_compose_material3_SearchBarDefaults$stableprop|#static{}androidx_compose_material3_SearchBarDefaults$stableprop[0] final val androidx.compose.material3/androidx_compose_material3_SearchBarState$stableprop // androidx.compose.material3/androidx_compose_material3_SearchBarState$stableprop|#static{}androidx_compose_material3_SearchBarState$stableprop[0] @@ -4235,6 +4276,7 @@ final fun androidx.compose.material3/RangeSlider(kotlin.ranges/ClosedFloatingPoi final fun androidx.compose.material3/Scaffold(androidx.compose.ui/Modifier?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, androidx.compose.material3/FabPosition, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.foundation.layout/WindowInsets?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/Scaffold|Scaffold(androidx.compose.ui.Modifier?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;androidx.compose.material3.FabPosition;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.foundation.layout.WindowInsets?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/Scaffold(androidx.compose.ui/Modifier?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, androidx.compose.material3/FabPosition?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.foundation.layout/WindowInsets?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/Scaffold|Scaffold(androidx.compose.ui.Modifier?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;androidx.compose.material3.FabPosition?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.foundation.layout.WindowInsets?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/Scrim(kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function0?, kotlin/Function0?, androidx.compose.ui.graphics/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/Scrim|Scrim(kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function0?;kotlin.Function0?;androidx.compose.ui.graphics.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.material3/ScrollField(androidx.compose.material3/ScrollFieldState, androidx.compose.ui/Modifier?, androidx.compose.material3/ScrollFieldColors?, kotlin/Function4?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ScrollField|ScrollField(androidx.compose.material3.ScrollFieldState;androidx.compose.ui.Modifier?;androidx.compose.material3.ScrollFieldColors?;kotlin.Function4?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ScrollableTabRow(kotlin/Int, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function2?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ScrollableTabRow|ScrollableTabRow(kotlin.Int;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function2?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/SecondaryScrollableTabRow(kotlin/Int, androidx.compose.ui/Modifier?, androidx.compose.foundation/ScrollState?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin/Function3?, kotlin/Function2?, androidx.compose.ui.unit/Dp, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/SecondaryScrollableTabRow|SecondaryScrollableTabRow(kotlin.Int;androidx.compose.ui.Modifier?;androidx.compose.foundation.ScrollState?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.Function3?;kotlin.Function2?;androidx.compose.ui.unit.Dp;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/SecondaryScrollableTabRow(kotlin/Int, androidx.compose.ui/Modifier?, androidx.compose.foundation/ScrollState?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin/Function3?, kotlin/Function2?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/SecondaryScrollableTabRow|SecondaryScrollableTabRow(kotlin.Int;androidx.compose.ui.Modifier?;androidx.compose.foundation.ScrollState?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.Function3?;kotlin.Function2?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] @@ -4435,6 +4477,9 @@ final fun androidx.compose.material3/androidx_compose_material3_RippleThemeConfi final fun androidx.compose.material3/androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop_getter|androidx_compose_material3_RippleThemeConfiguration_Focus_Opacity$stableprop_getter(){}[0] final fun androidx.compose.material3/androidx_compose_material3_ScaffoldDefaults$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_ScaffoldDefaults$stableprop_getter|androidx_compose_material3_ScaffoldDefaults$stableprop_getter(){}[0] final fun androidx.compose.material3/androidx_compose_material3_ScrimDefaults$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_ScrimDefaults$stableprop_getter|androidx_compose_material3_ScrimDefaults$stableprop_getter(){}[0] +final fun androidx.compose.material3/androidx_compose_material3_ScrollFieldColors$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_ScrollFieldColors$stableprop_getter|androidx_compose_material3_ScrollFieldColors$stableprop_getter(){}[0] +final fun androidx.compose.material3/androidx_compose_material3_ScrollFieldDefaults$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_ScrollFieldDefaults$stableprop_getter|androidx_compose_material3_ScrollFieldDefaults$stableprop_getter(){}[0] +final fun androidx.compose.material3/androidx_compose_material3_ScrollFieldState$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_ScrollFieldState$stableprop_getter|androidx_compose_material3_ScrollFieldState$stableprop_getter(){}[0] final fun androidx.compose.material3/androidx_compose_material3_SearchBarColors$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_SearchBarColors$stableprop_getter|androidx_compose_material3_SearchBarColors$stableprop_getter(){}[0] final fun androidx.compose.material3/androidx_compose_material3_SearchBarDefaults$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_SearchBarDefaults$stableprop_getter|androidx_compose_material3_SearchBarDefaults$stableprop_getter(){}[0] final fun androidx.compose.material3/androidx_compose_material3_SearchBarState$stableprop_getter(): kotlin/Int // androidx.compose.material3/androidx_compose_material3_SearchBarState$stableprop_getter|androidx_compose_material3_SearchBarState$stableprop_getter(){}[0] @@ -4505,6 +4550,7 @@ final fun androidx.compose.material3/rememberDateRangePickerState(kotlin/Long?, final fun androidx.compose.material3/rememberDrawerState(androidx.compose.material3/DrawerValue, kotlin/Function1?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/DrawerState // androidx.compose.material3/rememberDrawerState|rememberDrawerState(androidx.compose.material3.DrawerValue;kotlin.Function1?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/rememberFloatingToolbarState(kotlin/Float, kotlin/Float, kotlin/Float, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/FloatingToolbarState // androidx.compose.material3/rememberFloatingToolbarState|rememberFloatingToolbarState(kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/rememberRangeSliderState(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Function0?, kotlin.ranges/ClosedFloatingPointRange?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/RangeSliderState // androidx.compose.material3/rememberRangeSliderState|rememberRangeSliderState(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Function0?;kotlin.ranges.ClosedFloatingPointRange?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.material3/rememberScrollFieldState(kotlin/Int, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/ScrollFieldState // androidx.compose.material3/rememberScrollFieldState|rememberScrollFieldState(kotlin.Int;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/rememberSliderState(kotlin/Float, kotlin/Int, kotlin/Function0?, kotlin.ranges/ClosedFloatingPointRange?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/SliderState // androidx.compose.material3/rememberSliderState|rememberSliderState(kotlin.Float;kotlin.Int;kotlin.Function0?;kotlin.ranges.ClosedFloatingPointRange?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/rememberSwipeToDismissBoxState(androidx.compose.material3/SwipeToDismissBoxValue?, kotlin/Function1?, kotlin/Function1?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/SwipeToDismissBoxState // androidx.compose.material3/rememberSwipeToDismissBoxState|rememberSwipeToDismissBoxState(androidx.compose.material3.SwipeToDismissBoxValue?;kotlin.Function1?;kotlin.Function1?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/rememberSwipeToDismissBoxState(androidx.compose.material3/SwipeToDismissBoxValue?, kotlin/Function1?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material3/SwipeToDismissBoxState // androidx.compose.material3/rememberSwipeToDismissBoxState|rememberSwipeToDismissBoxState(androidx.compose.material3.SwipeToDismissBoxValue?;kotlin.Function1?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt index eeadf3553715c..dee0680c9464d 100644 --- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt +++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt @@ -417,6 +417,18 @@ private val RadioButtons = examples = RadioButtonsExamples, ) +private val ScrollField = + Component( + id = nextId(), + name = "Scroll field", + description = "Scroll field allows the user to select a value, e.g. time.", + // No scroll field icon + guidelinesUrl = "$ComponentGuidelinesUrl/scroll-field", + docsUrl = "", // TODO(b/441573791): Add docs when available. + sourceUrl = "$Material3SourceUrl/ScrollField.kt", + examples = ScrollFieldExamples, + ) + private val SearchBars = Component( id = nextId(), @@ -596,6 +608,7 @@ val Components = ProgressIndicators, PullToRefreshIndicators, RadioButtons, + ScrollField, SearchBars, SegmentedButtons, Sliders, diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt index 3829a6ecc9071..04bf20464c329 100644 --- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt +++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt @@ -157,7 +157,6 @@ import androidx.compose.material3.samples.MediumFloatingActionButtonSample import androidx.compose.material3.samples.MediumRoundWideIconButtonSample import androidx.compose.material3.samples.MediumToggleButtonWithIconSample import androidx.compose.material3.samples.MenuSample -import androidx.compose.material3.samples.MenuWithCascadingMenusSample import androidx.compose.material3.samples.MenuWithScrollStateSample import androidx.compose.material3.samples.ModalBottomSheetSample import androidx.compose.material3.samples.ModalNavigationDrawerSample @@ -218,6 +217,7 @@ import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar import androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar import androidx.compose.material3.samples.ScaffoldWithMultilineSnackbar import androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar +import androidx.compose.material3.samples.ScrollFieldSample import androidx.compose.material3.samples.ScrollableHorizontalFloatingToolbarSample import androidx.compose.material3.samples.ScrollableVerticalFloatingToolbarSample import androidx.compose.material3.samples.ScrollingFancyIndicatorContainerTabs @@ -280,6 +280,7 @@ import androidx.compose.material3.samples.ThreeLineListItemWithOverlineAndSuppor import androidx.compose.material3.samples.TimeInputSample import androidx.compose.material3.samples.TimePickerSample import androidx.compose.material3.samples.TimePickerSwitchableSample +import androidx.compose.material3.samples.TimeScrollFieldSample import androidx.compose.material3.samples.TintedIconButtonSample import androidx.compose.material3.samples.ToggleButtonSample import androidx.compose.material3.samples.ToggleButtonWithIconSample @@ -1681,14 +1682,6 @@ val MenusExamples = ) { GroupedMenuSample() }, - Example( - name = "MenuWithCascadingMenusSample", - description = MenusExampleDescription, - sourceUrl = MenusExampleSourceUrl, - isExpressive = true, - ) { - MenuWithCascadingMenusSample() - }, Example( name = "MenuWithScrollStateSample", description = MenusExampleDescription, @@ -2161,6 +2154,28 @@ val ToggleButtonsExamples = }, ) +private const val ScrollFieldDescription = "Scroll field examples" +private const val ScrollFieldSourceUrl = "$SampleSourceUrl/ScrollFieldSamples.kt" +val ScrollFieldExamples = + listOf( + Example( + name = "ScrollFieldSample", + description = ScrollFieldDescription, + sourceUrl = ScrollFieldSourceUrl, + isExpressive = true, + ) { + ScrollFieldSample() + }, + Example( + name = "TimeScrollFieldSample", + description = ScrollFieldDescription, + sourceUrl = ScrollFieldSourceUrl, + isExpressive = true, + ) { + TimeScrollFieldSample() + }, + ) + private const val SlidersExampleDescription = "Sliders examples" private const val SlidersExampleSourceUrl = "$SampleSourceUrl/SliderSamples.kt" val SlidersExamples = diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScrollFieldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScrollFieldSamples.kt new file mode 100644 index 0000000000000..079a363b6e4a2 --- /dev/null +++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScrollFieldSamples.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScrollField +import androidx.compose.material3.Text +import androidx.compose.material3.rememberScrollFieldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Sampled +@Composable +@Preview +fun ScrollFieldSample() { + val minVal = 1000 + val maxVal = 2000 + val itemCount = (maxVal - minVal) + 1 + + val state = rememberScrollFieldState(itemCount = itemCount, index = 0) + var selectedValue by remember { mutableIntStateOf(minVal) } + + Row( + modifier = + Modifier.background( + MaterialTheme.colorScheme.surfaceContainerHighest, + RoundedCornerShape(28.dp), + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ScrollField( + state = state, + modifier = Modifier.size(width = 192.dp, height = 160.dp), + field = { index, isSelected -> + val valueToShow = minVal + index + Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) { + Text( + text = valueToShow.toString(), + style = + if (isSelected) { + MaterialTheme.typography.displayLarge + } else { + MaterialTheme.typography.displayMedium + }, + color = + if (isSelected) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.outline + }, + ) + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Sampled +@Composable +@Preview +fun TimeScrollFieldSample() { + val hourCount = 24 + val minuteCount = 60 + + val hourState = rememberScrollFieldState(itemCount = hourCount, index = 12) + val minuteState = rememberScrollFieldState(itemCount = minuteCount, index = 30) + + var selectedHour by remember { mutableIntStateOf(12) } + var selectedMinute by remember { mutableIntStateOf(30) } + + Row( + modifier = + Modifier.background( + MaterialTheme.colorScheme.surfaceContainerHighest, + RoundedCornerShape(28.dp), + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ScrollField(state = hourState, modifier = Modifier.size(width = 80.dp, height = 160.dp)) + + Text( + text = ":", + style = MaterialTheme.typography.displayLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + + ScrollField(state = minuteState, modifier = Modifier.size(width = 80.dp, height = 160.dp)) + } +} diff --git a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ScrollFieldScreenshotTest.kt b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ScrollFieldScreenshotTest.kt new file mode 100644 index 0000000000000..0205a405a1efc --- /dev/null +++ b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ScrollFieldScreenshotTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.material3 + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.testutils.assertAgainstGolden +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +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 kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@MediumTest +@RunWith(AndroidJUnit4::class) +@SdkSuppress(minSdkVersion = 35, maxSdkVersion = 35) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +class ScrollFieldScreenshotTest() { + + @get:Rule val rule = createComposeRule(StandardTestDispatcher()) + + @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3) + + @Test + fun scrollField_lightTheme() { + rule.setMaterialContent(lightColorScheme()) { TestContent() } + assertScrollFieldAgainstGolden("scrollField_lightTheme") + } + + @Test + fun scrollField_darkTheme() { + rule.setMaterialContent(darkColorScheme()) { TestContent() } + assertScrollFieldAgainstGolden("scrollField_darkTheme") + } + + private fun assertScrollFieldAgainstGolden(goldenIdentifier: String) { + rule + .onNodeWithTag(ScrollFieldTestTag) + .captureToImage() + .assertAgainstGolden(screenshotRule, goldenIdentifier) + } + + private val ScrollFieldTestTag = "scrollField" + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + @Composable + private fun TestContent() { + val itemCount = 100 + // The hoisted state ensures the Pager starts exactly at our 'index' + // for a deterministic screenshot. + val state = rememberScrollFieldState(itemCount = itemCount, index = 0) + + ScrollField( + state = state, + // Since this is a static screenshot, a no-op is appropriate. + modifier = Modifier.size(width = 80.dp, height = 160.dp).testTag(ScrollFieldTestTag), + ) + } +} diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt index 43b02ab35a46f..1cd0559669827 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt @@ -710,6 +710,9 @@ class ColorScheme( @OptIn(ExperimentalMaterial3Api::class) internal var defaultTimePickerColorsCached: TimePickerColors? = null + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) + internal var defaultScrollFieldColorsCached: ScrollFieldColors? = null + @OptIn(ExperimentalMaterial3Api::class) internal var defaultRichTooltipColorsCached: RichTooltipColors? = null diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ScrollField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ScrollField.kt new file mode 100644 index 0000000000000..6648354248e7c --- /dev/null +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ScrollField.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.material3 + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.unit.dp + +private const val InfinitePageCount = 100_000 + +/** + * A state object that can be hoisted to observe and control the scrolling behavior of a + * [ScrollField]. + * + * In most cases, this state should be created via [rememberScrollFieldState]. + * + * @param pagerState the underlying [PagerState] used to handle the scroll logic. + * @param itemCount the total number of unique items available in the scroll field. + */ +@ExperimentalMaterial3ExpressiveApi +@Stable +class ScrollFieldState(internal val pagerState: PagerState, val itemCount: Int) { + /** + * The index of the currently selected option. + * + * This value is always clamped between 0 and [itemCount] - 1. When the internal pager is + * scrolled, this value updates to reflect the item closest to the snap position. + */ + val selectedOption: Int + get() = if (itemCount > 0) pagerState.currentPage % itemCount else 0 + + /** + * Instantly scrolls to the specified [option]. + * + * @param option the index of the item to scroll to. + * @see animateScrollToOption for a smooth transition. + */ + suspend fun scrollToOption(option: Int) { + val targetPage = calculateTargetPage(option) + pagerState.scrollToPage(targetPage) + } + + /** + * Animates the scroll to the specified [option]. + * + * @param option the index of the item to animate to. + * @see scrollToOption for an instant scroll. + */ + suspend fun animateScrollToOption(option: Int) { + val targetPage = calculateTargetPage(option) + pagerState.animateScrollToPage(targetPage) + } + + private fun calculateTargetPage(option: Int): Int { + val currentContextPage = pagerState.currentPage + val currentOption = currentContextPage % itemCount + val diff = option - currentOption + return currentContextPage + diff + } +} + +/** + * Creates and remembers a [ScrollFieldState] to be used with a [ScrollField]. + * + * @param itemCount the total number of unique items to be displayed in the scrollable wheel. + * @param index the initial selected index of the scroll field. + * @return a [ScrollFieldState] that can be used to control or observe the scroll field. + */ +@ExperimentalMaterial3ExpressiveApi +@Composable +fun rememberScrollFieldState(itemCount: Int, index: Int = 0): ScrollFieldState { + val initialPage = + remember(itemCount, index) { + (InfinitePageCount / 2) - (InfinitePageCount / 2 % itemCount) + index + } + val pagerState = rememberPagerState(initialPage = initialPage) { InfinitePageCount } + + return remember(pagerState, itemCount) { ScrollFieldState(pagerState, itemCount) } +} + +/** + * ScrollField's can be used to provide a more interactive way to select a time or other numerical + * value. + * + * Generic ScrollField for scrollable numerical selection: + * + * @sample androidx.compose.material3.samples.ScrollFieldSample + * + * ScrollField for time selection: + * + * @sample androidx.compose.material3.samples.TimeScrollFieldSample + * @param state the state object to be used to control or observe the pager's state. + * @param modifier the [Modifier] to be applied to the ScrollField container. + * @param colors [ScrollFieldColors] that will be used to resolve the colors used for this + * ScrollField in different states. + * @param field the composable used to render each item in the wheel. + */ +@ExperimentalMaterial3ExpressiveApi +@Composable +fun ScrollField( + state: ScrollFieldState, + modifier: Modifier = Modifier, + colors: ScrollFieldColors = ScrollFieldDefaults.colors(), + field: @Composable (index: Int, selected: Boolean) -> Unit = { index, selected -> + ScrollFieldDefaults.Item(index = index, selected = selected, colors = colors) + }, +) { + VerticalPager( + state = state.pagerState, + modifier = modifier.background(colors.containerColor, shape = ScrollFieldDefaults.shape), + pageSize = PageSize.Fixed(ScrollFieldDefaults.ScrollFieldHeight / 3), + horizontalAlignment = Alignment.CenterHorizontally, + snapPosition = SnapPosition.Center, + ) { page -> + val index = page % state.itemCount + val isSelected = state.pagerState.currentPage == page + + Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) { + field(index, isSelected) + } + } +} + +/** Represents the colors used by a [ScrollField] in different states. */ +@ExperimentalMaterial3ExpressiveApi +@Immutable +class ScrollFieldColors( + val containerColor: Color, + val unselectedContentColor: Color, + val selectedContentColor: Color, +) { + + /** + * Returns a copy of this ScrollFieldColors, optionally overriding some of the values. This uses + * the Color.Unspecified to mean “use the value from the source". + */ + fun copy( + containerColor: Color = this.containerColor, + unselectedContentColor: Color = this.unselectedContentColor, + selectedContentColor: Color = this.selectedContentColor, + ) = + ScrollFieldColors( + containerColor.takeOrElse { this.containerColor }, + unselectedContentColor.takeOrElse { this.unselectedContentColor }, + selectedContentColor.takeOrElse { this.selectedContentColor }, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ScrollFieldColors) return false + + if (containerColor != other.containerColor) return false + if (unselectedContentColor != other.unselectedContentColor) return false + if (selectedContentColor != other.selectedContentColor) return false + + return true + } + + override fun hashCode(): Int { + var result = containerColor.hashCode() + result = 31 * result + unselectedContentColor.hashCode() + result = 31 * result + selectedContentColor.hashCode() + return result + } +} + +/** Object to hold defaults used by [ScrollField]. */ +@ExperimentalMaterial3ExpressiveApi +@Stable +object ScrollFieldDefaults { + /** + * The default height for a [ScrollField]. This can be used as a reference when providing a + * Modifier.height to the ScrollField to ensure enough vertical space is available to display + * the typical three-item layout. + */ + val ScrollFieldHeight = 200.dp + + /** The default shape for the [ScrollField] container background. */ + val shape: Shape + @Composable get() = ShapeDefaults.Large + + /** Default colors used by a [ScrollField]. */ + @Composable fun colors(): ScrollFieldColors = MaterialTheme.colorScheme.defaultScrollFieldColors + + /** + * Creates a [ScrollFieldColors] that represents the default container, unselected, and selected + * colors used in a [ScrollField]. + * + * @param containerColor The color of the [ScrollField] container. + * @param unselectedContentColor The color of the numerical value(s) visible on the screen that + * are not chosen. + * @param selectedContentColor The color of the numerical value that is centered and snapped + * into place. + */ + @Composable + fun colors( + containerColor: Color = Color.Unspecified, + unselectedContentColor: Color = Color.Unspecified, + selectedContentColor: Color = Color.Unspecified, + ) = + MaterialTheme.colorScheme.defaultScrollFieldColors.copy( + containerColor = containerColor, + unselectedContentColor = unselectedContentColor, + selectedContentColor = selectedContentColor, + ) + + internal val ColorScheme.defaultScrollFieldColors: ScrollFieldColors + @Composable + get() { + return defaultScrollFieldColorsCached + ?: ScrollFieldColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + unselectedContentColor = MaterialTheme.colorScheme.outline, + selectedContentColor = MaterialTheme.colorScheme.onSurface, + ) + .also { defaultScrollFieldColorsCached = it } + } + + /** + * The default item implementation for [ScrollField]. + * + * @param index the current item index. + * @param selected whether this item is currently selected (centered). + * @param colors the colors to use for the text content. + */ + @Composable + fun Item(index: Int, selected: Boolean, colors: ScrollFieldColors = colors()) { + Text( + text = index.toLocalString(minDigits = 2), + style = + if (selected) { + MaterialTheme.typography.displayLarge + } else { + MaterialTheme.typography.displayMedium + }, + color = if (selected) colors.selectedContentColor else colors.unselectedContentColor, + ) + } +} diff --git a/compose/remote/integration-tests/benchmark/build.gradle b/compose/remote/integration-tests/benchmark/build.gradle new file mode 100644 index 0000000000000..7f590483fdbfb --- /dev/null +++ b/compose/remote/integration-tests/benchmark/build.gradle @@ -0,0 +1,55 @@ +/* + * Copyright (C) 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. + */ + +/** + * This file was created using the `createProject` gradle task (./gradlew createProject) + * + * Please use the task when creating a new project, rather than copying an existing project and + * modifying its settings. + */ +import androidx.build.SoftwareType + +plugins { + id("AndroidXPlugin") + id("com.android.library") + id("AndroidXComposePlugin") + id("androidx.benchmark") +} + +dependencies { + implementation(project(":benchmark:benchmark-junit4")) + implementation(project(":compose:runtime:runtime")) + implementation(project(":compose:ui:ui-test-junit4")) + implementation(libs.kotlinReflect) + implementation(libs.testRules) + implementation(libs.junit) + implementation(project(":compose:remote:remote-creation")) + implementation(project(":compose:remote:remote-creation-compose")) + + androidTestImplementation(project(":compose:runtime:runtime")) + androidTestImplementation(project(":compose:benchmark-utils")) + androidTestImplementation(project(":compose:ui:ui")) + androidTestImplementation(libs.kotlinTest) + androidTestImplementation(libs.truth) +} +android { + compileSdk { version = release(36) } + namespace = "androidx.compose.remote.integration.benchmark" +} + +androidx { + type = SoftwareType.BENCHMARK +} diff --git a/compose/remote/integration-tests/benchmark/src/androidTest/AndroidManifest.xml b/compose/remote/integration-tests/benchmark/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000000..56a1d896020cd --- /dev/null +++ b/compose/remote/integration-tests/benchmark/src/androidTest/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/compose/remote/integration-tests/benchmark/src/androidTest/java/androidx/compose/remote/integration/tests/benchmark/RemoteComposeBenchmark.kt b/compose/remote/integration-tests/benchmark/src/androidTest/java/androidx/compose/remote/integration/tests/benchmark/RemoteComposeBenchmark.kt new file mode 100644 index 0000000000000..0ee0f2f11a5a4 --- /dev/null +++ b/compose/remote/integration-tests/benchmark/src/androidTest/java/androidx/compose/remote/integration/tests/benchmark/RemoteComposeBenchmark.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.tests.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.compose.remote.creation.compose.capture.captureSingleRemoteDocument +import androidx.compose.remote.creation.compose.layout.RemoteColumn +import androidx.compose.remote.creation.compose.layout.RemoteText +import androidx.compose.remote.creation.compose.modifier.RemoteModifier +import androidx.compose.remote.creation.compose.modifier.fillMaxSize +import androidx.compose.remote.creation.compose.modifier.fillMaxWidth +import androidx.compose.remote.creation.compose.modifier.padding +import androidx.compose.remote.creation.compose.modifier.rememberRemoteScrollState +import androidx.compose.remote.creation.compose.modifier.verticalScroll +import androidx.compose.remote.creation.compose.state.rdp +import androidx.compose.remote.creation.compose.state.rs +import androidx.compose.remote.creation.profile.RcPlatformProfiles +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RemoteComposeBenchmark { + + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun documentGeneration() { + benchmarkRule.measureRepeated { + runBlocking { + captureSingleRemoteDocument( + profile = RcPlatformProfiles.ANDROIDX, + context = InstrumentationRegistry.getInstrumentation().context, + ) { + val scrollState = rememberRemoteScrollState() + RemoteColumn( + modifier = RemoteModifier.fillMaxSize().verticalScroll(scrollState) + ) { + repeat(500) { index -> + RemoteText( + ("Item $index").rs, + modifier = RemoteModifier.fillMaxWidth().padding(vertical = 8.rdp), + ) + } + } + } + } + } + } +} diff --git a/compose/remote/integration-tests/macrobenchmark-target/OWNERS b/compose/remote/integration-tests/macrobenchmark-target/OWNERS new file mode 100644 index 0000000000000..5a68eda9cf3c4 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/OWNERS @@ -0,0 +1,2 @@ +# Bug component 1437331 +nkovacevic@google.com \ No newline at end of file diff --git a/compose/remote/integration-tests/macrobenchmark-target/build.gradle b/compose/remote/integration-tests/macrobenchmark-target/build.gradle new file mode 100644 index 0000000000000..07e525d02c152 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/build.gradle @@ -0,0 +1,58 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("AndroidXPlugin") + id("AndroidXComposePlugin") + id("com.android.application") +} + +group="androidx.compose.remote" + +android { + compileSdk { version = release(36) } + + namespace = "androidx.compose.remote.integration.macrobenchmark.target" + defaultConfig.minSdk { version = release(29) } + buildTypes { + release { + minifyEnabled = true + shrinkResources = true + proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), + "proguard-benchmark.pro") + } + } +} + +dependencies { + implementation(project(":compose:remote:remote-core")) + implementation(project(":compose:remote:remote-creation")) + implementation(project(":compose:remote:remote-creation-compose")) + implementation(project(":compose:remote:remote-creation-core")) + implementation(project(":compose:remote:remote-player-compose")) + implementation(project(":compose:remote:remote-player-core")) + + implementation(project(":compose:foundation:foundation-layout")) + implementation(project(":compose:ui:ui")) + implementation(project(":compose:foundation:foundation")) + implementation(project(":compose:runtime:runtime")) + implementation(project(":compose:ui:ui-tooling")) + implementation("androidx.activity:activity-compose:1.10.1") + implementation(project(":profileinstaller:profileinstaller")) + implementation(project(":tracing:tracing")) + implementation(project(":compose:material3:material3")) + +} diff --git a/compose/remote/integration-tests/macrobenchmark-target/proguard-benchmark.pro b/compose/remote/integration-tests/macrobenchmark-target/proguard-benchmark.pro new file mode 100644 index 0000000000000..0674e77454d35 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/proguard-benchmark.pro @@ -0,0 +1 @@ +-dontobfuscate \ No newline at end of file diff --git a/compose/remote/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/remote/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..7db675c8299a7 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/Common.kt b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/Common.kt new file mode 100644 index 0000000000000..503cc6b8f897d --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/Common.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.macrobenchmark.target + +const val LIST_CONTENT_DESCRIPTION = "ScrollableColumn" +const val BENCHMARK_MODE_ARG = "benchmark_mode" +const val MODE_RENDER_FROM_CACHE = "render_from_cache" +const val MODE_LOCAL = "mode_local" +const val DOCUMENT_READY = "READY" diff --git a/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentGenerationActivity.kt b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentGenerationActivity.kt new file mode 100644 index 0000000000000..3c7b1c4ed2c3c --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentGenerationActivity.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.macrobenchmark.target + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.remote.creation.compose.capture.captureSingleRemoteDocument +import androidx.compose.remote.creation.compose.layout.RemoteColumn +import androidx.compose.remote.creation.compose.layout.RemoteText +import androidx.compose.remote.creation.compose.modifier.RemoteModifier +import androidx.compose.remote.creation.compose.modifier.contentDescription +import androidx.compose.remote.creation.compose.modifier.fillMaxSize +import androidx.compose.remote.creation.compose.modifier.fillMaxWidth +import androidx.compose.remote.creation.compose.modifier.padding +import androidx.compose.remote.creation.compose.modifier.rememberRemoteScrollState +import androidx.compose.remote.creation.compose.modifier.semantics +import androidx.compose.remote.creation.compose.modifier.verticalScroll +import androidx.compose.remote.creation.compose.state.rdp +import androidx.compose.remote.creation.compose.state.rs +import androidx.compose.remote.creation.profile.RcPlatformProfiles +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import kotlinx.coroutines.runBlocking + +open class DocumentGenerationActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + runBlocking { + BenchmarkCache.documentBytes = + captureSingleRemoteDocument( + profile = RcPlatformProfiles.ANDROIDX, + context = this@DocumentGenerationActivity, + ) { + val scrollState = rememberRemoteScrollState() + RemoteColumn( + modifier = + RemoteModifier.fillMaxSize() + .semantics { contentDescription = LIST_CONTENT_DESCRIPTION.rs } + .verticalScroll(scrollState) + ) { + repeat(500) { index -> + RemoteText( + ("Item $index").rs, + modifier = + RemoteModifier.fillMaxWidth().padding(vertical = 8.rdp), + ) + } + } + } + .bytes + } + setContent { + Box( + Modifier.semantics { contentDescription = DOCUMENT_READY }.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(DOCUMENT_READY) + } + } + } +} diff --git a/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentTraceActivity.kt b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentTraceActivity.kt new file mode 100644 index 0000000000000..a1c2ed6742172 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/remote/integration/macrobenchmark/target/DocumentTraceActivity.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.macrobenchmark.target + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.NonNull +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.remote.creation.compose.capture.captureSingleRemoteDocument +import androidx.compose.remote.creation.compose.layout.RemoteColumn +import androidx.compose.remote.creation.compose.layout.RemoteText +import androidx.compose.remote.creation.compose.modifier.RemoteModifier +import androidx.compose.remote.creation.compose.modifier.contentDescription +import androidx.compose.remote.creation.compose.modifier.fillMaxSize +import androidx.compose.remote.creation.compose.modifier.fillMaxWidth +import androidx.compose.remote.creation.compose.modifier.padding +import androidx.compose.remote.creation.compose.modifier.rememberRemoteScrollState +import androidx.compose.remote.creation.compose.modifier.semantics +import androidx.compose.remote.creation.compose.modifier.verticalScroll +import androidx.compose.remote.creation.compose.state.rdp +import androidx.compose.remote.creation.compose.state.rs +import androidx.compose.remote.creation.profile.RcPlatformProfiles +import androidx.compose.remote.player.compose.RemoteDocumentPlayer +import androidx.compose.remote.player.core.RemoteDocument +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.trace +import java.util.Objects.requireNonNull + +object BenchmarkCache { + var documentBytes: ByteArray? = null +} + +open class DocumentTraceActivity : ComponentActivity() { + + @Composable + open fun RemoteComposePlayer(@NonNull remoteDocumentBytes: ByteArray) { + val windowInfo = LocalWindowInfo.current + RemoteDocumentPlayer( + document = + remember(remoteDocumentBytes) { + trace("CreateRemoteDocument:parsing") { + RemoteDocument(remoteDocumentBytes) + } + } + .document, + documentWidth = windowInfo.containerSize.width, + documentHeight = windowInfo.containerSize.height, + modifier = Modifier.fillMaxSize(), + debugMode = 0, + onNamedAction = { _, _, _ -> }, + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + when (intent.getStringExtra(BENCHMARK_MODE_ARG)) { + MODE_RENDER_FROM_CACHE -> { + requireNonNull(BenchmarkCache.documentBytes, "BenchmarkCache is empty") + RemoteComposePlayer(BenchmarkCache.documentBytes!!) + } + MODE_LOCAL -> { + Column( + modifier = + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).semantics { + contentDescription = LIST_CONTENT_DESCRIPTION + } + ) { + repeat(500) { index -> + Text( + "Item $index", + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) + } + } + } + else -> { + ScrollableRemoteContent() + } + } + } + } + + @Composable + @Suppress("RestrictedApiAndroidX") + fun ScrollableRemoteContent() { + var documentBytes by remember { mutableStateOf(null) } + val context = LocalContext.current + LaunchedEffect(Unit) { + documentBytes = + captureSingleRemoteDocument( + profile = RcPlatformProfiles.ANDROIDX, + context = context, + ) { + val scrollState = rememberRemoteScrollState() + RemoteColumn( + modifier = + RemoteModifier.fillMaxSize() + .semantics { contentDescription = LIST_CONTENT_DESCRIPTION.rs } + .verticalScroll(scrollState) + ) { + repeat(500) { index -> + RemoteText( + ("Item $index").rs, + modifier = + RemoteModifier.fillMaxWidth().padding(vertical = 8.rdp), + ) + } + } + } + .bytes + } + + // Play the remote document + documentBytes?.let { RemoteComposePlayer(it) } + } +} diff --git a/compose/remote/integration-tests/macrobenchmark/OWNERS b/compose/remote/integration-tests/macrobenchmark/OWNERS new file mode 100644 index 0000000000000..eed1ac82b0fcf --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1437331 +nkovacevic@google.com \ No newline at end of file diff --git a/compose/remote/integration-tests/macrobenchmark/build.gradle b/compose/remote/integration-tests/macrobenchmark/build.gradle new file mode 100644 index 0000000000000..051a4e820f255 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("AndroidXPlugin") + id("com.android.test") +} + +android { + compileSdk { version = release(36) } + defaultConfig { + minSdk { version = release(29) } + } + namespace = "androidx.compose.remote.integration.macrobenchmark" + targetProjectPath = ":compose:remote:integration-tests:macrobenchmark-target" + experimentalProperties["android.experimental.self-instrumenting"] = true +} + + +android.buildTypes { + release { + proguardFiles("proguard-benchmark.pro") + } +} +androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } } +dependencies { + implementation(project(":benchmark:benchmark-junit4")) + implementation(project(":benchmark:benchmark-macro-junit4")) + implementation(project(":internal-testutils-benchmark-macro")) + implementation(project(":tracing:tracing")) + implementation(libs.testRules) + implementation(libs.testExtJunit) + implementation(libs.testCore) + implementation(libs.testRunner) + implementation(libs.testUiautomator) +} diff --git a/compose/remote/integration-tests/macrobenchmark/proguard-benchmark.pro b/compose/remote/integration-tests/macrobenchmark/proguard-benchmark.pro new file mode 100644 index 0000000000000..0674e77454d35 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/proguard-benchmark.pro @@ -0,0 +1 @@ +-dontobfuscate \ No newline at end of file diff --git a/compose/remote/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/compose/remote/integration-tests/macrobenchmark/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..34d5616b75c9f --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/Common.kt b/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/Common.kt new file mode 100644 index 0000000000000..74daecbfbea51 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/Common.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.macrobenchmark + +internal const val LIST_CONTENT_DESCRIPTION = "ScrollableColumn" +internal const val PACKAGE_NAME = "androidx.compose.remote.integration.macrobenchmark.target" +internal const val SCROLL_ACTIVITY = + "androidx.compose.remote.integration.macrobenchmark.target.SCROLL_ACTIVITY" +internal const val DOCUMENT_TRACING_ACTIVITY = + "androidx.compose.remote.integration.macrobenchmark.target.DOCUMENT_TRACING_ACTIVITY" +internal const val DOCUMENT_GENERATING_ACTIVITY = + "androidx.compose.remote.integration.macrobenchmark.target.DOCUMENT_GENERATING_ACTIVITY" + +const val BENCHMARK_MODE_ARG = "benchmark_mode" +const val DOCUMENT_READY = "READY" +const val MODE_RENDER_FROM_CACHE = "render_from_cache" +const val MODE_LOCAL = "mode_local" +public val recordingTraces: List = + listOf( + "CaptureRemoteDocument:captureSingleRemoteDocument", + "CaptureRemoteDocument:captureSingleRemoteDocument:compositionInitialization", + "CaptureRemoteDocument:captureSingleRemoteDocument:rootNodeRender", + "CaptureRemoteDocument:captureSingleRemoteDocument:toByteArray", + ) +public val decodingTraces: List = listOf("CreateRemoteDocument:parsing") +public val allTraces: List = recordingTraces + decodingTraces diff --git a/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/RemoteComposeDocumentTracingMacrobenchmark.kt b/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/RemoteComposeDocumentTracingMacrobenchmark.kt new file mode 100644 index 0000000000000..ccd9197622534 --- /dev/null +++ b/compose/remote/integration-tests/macrobenchmark/src/main/java/androidx/compose/remote/integration/macrobenchmark/RemoteComposeDocumentTracingMacrobenchmark.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.remote.integration.macrobenchmark + +import android.content.Intent +import androidx.benchmark.macro.BaselineProfileMode +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.ExperimentalMetricApi +import androidx.benchmark.macro.MemoryUsageMetric +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.TraceSectionMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.filters.LargeTest +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@OptIn(ExperimentalMetricApi::class) +@LargeTest +@RunWith(Parameterized::class) +open class RemoteComposeDocumentTracingMacrobenchmark(val compilationMode: CompilationMode) { + @get:Rule val benchmarkRule = MacrobenchmarkRule() + + @Test + fun documentGenerationOnly() = + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = + recordingTraces.map { TraceSectionMetric(it) } + + MemoryUsageMetric(MemoryUsageMetric.Mode.Max), + iterations = 5, + compilationMode = compilationMode, + startupMode = StartupMode.WARM, + setupBlock = { pressHome() }, + measureBlock = { + val intent = Intent() + intent.action = DOCUMENT_GENERATING_ACTIVITY + startActivityAndWait(intent) + device.wait(Until.hasObject(By.text(DOCUMENT_READY)), 10000) + }, + ) + + @Test + fun documentRenderingOnly() = + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = + listOf(StartupTimingMetric()) + + decodingTraces.map { TraceSectionMetric(it) } + + MemoryUsageMetric(MemoryUsageMetric.Mode.Max), + iterations = 5, + compilationMode = compilationMode, + startupMode = StartupMode.WARM, + setupBlock = { + pressHome() + val intent = Intent() + intent.action = DOCUMENT_GENERATING_ACTIVITY + startActivityAndWait(intent) + device.wait(Until.hasObject(By.text(DOCUMENT_READY)), 10000) + pressHome() + }, + measureBlock = { + val intent = Intent() + intent.action = DOCUMENT_TRACING_ACTIVITY + intent.putExtra(BENCHMARK_MODE_ARG, MODE_RENDER_FROM_CACHE) + startActivityAndWait(intent) + device.waitForIdle() + }, + ) + + @Test + fun startupWithDocumentGeneration() = + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = + listOf(StartupTimingMetric()) + + allTraces.map { TraceSectionMetric(it) } + + MemoryUsageMetric(MemoryUsageMetric.Mode.Max), + iterations = 5, + compilationMode = compilationMode, + startupMode = StartupMode.WARM, + setupBlock = { pressHome() }, + measureBlock = { + val intent = Intent() + intent.action = DOCUMENT_TRACING_ACTIVITY + startActivityAndWait(intent) + device.waitForIdle() + }, + ) + + @Test + fun startupLocal() { + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = listOf(StartupTimingMetric()), + iterations = 5, + compilationMode = compilationMode, + startupMode = StartupMode.WARM, + setupBlock = { pressHome() }, + measureBlock = { + val intent = Intent() + intent.action = DOCUMENT_TRACING_ACTIVITY + intent.putExtra(BENCHMARK_MODE_ARG, MODE_LOCAL) + startActivityAndWait(intent) + device.waitForIdle() + }, + ) + } + + companion object { + + @Parameterized.Parameters(name = "compilation={0}") + @JvmStatic + fun parameters() = + listOf( + arrayOf(CompilationMode.None()), + arrayOf( + CompilationMode.Partial( + baselineProfileMode = BaselineProfileMode.Disable, + warmupIterations = 3, + ) + ), + ) + } +} diff --git a/libraryversions.toml b/libraryversions.toml index 934e865edcdd7..ad1edb25f5f56 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -102,7 +102,7 @@ MEDIA = "1.8.0-alpha01" MEDIAROUTER = "1.9.0-alpha01" METRICS = "1.0.0-rc01" NAVIGATION = "2.10.0-alpha03" -NAVIGATION3 = "1.2.0-alpha01" +NAVIGATION3 = "1.2.0-alpha02" NAVIGATIONEVENT = "1.1.0-rc01" PAGING = "3.5.0-beta01" PALETTE = "1.1.0-alpha01" @@ -120,7 +120,7 @@ REMOTECALLBACK = "1.0.0-alpha03" REMOTECOMPOSE = "1.0.0-alpha08" REMOTECOMPOSE_WEAR = "1.0.0-alpha02" RESOURCEINSPECTION = "1.1.0-alpha01" -ROOM3 = "3.0.0-alpha03" +ROOM3 = "3.0.0-alpha04" SAFEPARCEL = "1.0.0-alpha01" SAVEDSTATE = "1.5.0-beta01" SECURITY = "1.1.0-rc01" @@ -136,7 +136,7 @@ SLICE_BENCHMARK = "1.1.0-alpha03" SLICE_BUILDERS_KTX = "1.0.0-alpha09" SLICE_REMOTECALLBACK = "1.0.0-alpha01" SLIDINGPANELAYOUT = "1.3.0-alpha01" -SQLITE = "2.7.0-alpha03" +SQLITE = "2.7.0-alpha04" SQLITE_INSPECTOR = "2.1.0-alpha01" STABLE_AIDL = "1.0.0-alpha01" STARTUP = "1.2.0-rc01" @@ -145,7 +145,7 @@ TESTEXT = "1.0.0-alpha03" TESTSCREENSHOT = "1.0.0-alpha01" TEST_UIAUTOMATOR = "2.4.0-beta02" TEXT = "1.0.0-alpha04" -TRACING = "2.0.0-alpha05" +TRACING = "2.0.0-alpha06" TRACING_DRIVER = "1.0.0-alpha01" TRACING_PERFETTO = "1.0.1" TRANSITION = "1.8.0-alpha01" @@ -181,7 +181,7 @@ WINDOW_SIDECAR = "1.0.0-rc01" WORK = "2.12.0-alpha01" XR_ARCORE = "1.0.0-alpha13" XR_COMPOSE = "1.0.0-alpha13" -XR_GLIMMER = "1.0.0-alpha10" +XR_GLIMMER = "1.0.0-alpha11" XR_PROJECTED = "1.0.0-alpha07" XR_RUNTIME = "1.0.0-alpha13" XR_SCENECORE = "1.0.0-alpha14" diff --git a/settings.gradle b/settings.gradle index 4246c6a8ec525..e6a573f336957 100644 --- a/settings.gradle +++ b/settings.gradle @@ -630,6 +630,9 @@ includeProject(":compose:material:material:material-samples", "compose/material/ includeProject(":compose:material3:material3:material3-samples", "compose/material3/material3/samples", [BuildType.COMPOSE]) includeProject(":compose:remote:integration-tests:demos", [BuildType.MAIN]) includeProject(":compose:remote:integration-tests:player-view-demos", [BuildType.MAIN]) +includeProject(":compose:remote:integration-tests:benchmark", [BuildType.MAIN]) +includeProject(":compose:remote:integration-tests:macrobenchmark", [BuildType.MAIN]) +includeProject(":compose:remote:integration-tests:macrobenchmark-target", [BuildType.MAIN]) includeProject(":compose:remote:remote-core", [BuildType.MAIN]) includeProject(":compose:remote:remote-core-testutils", [BuildType.MAIN]) includeProject(":compose:remote:remote-creation-compose", [BuildType.MAIN]) diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java index 627754b0ed437..df0ef7ce71906 100644 --- a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java +++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java @@ -24,6 +24,7 @@ import androidx.webkit.test.common.WebkitUtils; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -148,6 +149,7 @@ public void testSettingCacheConfig() { * Test to make sure that calling the setMaxPrefetches API won't cause any obvious errors. */ @Test + @Ignore("https://crbug.com/501069897") public void testSetMaxPrefetches() throws Exception { WebkitUtils.checkFeature(WebViewFeature.PREFETCH_CACHE_V1); WebkitUtils.onMainThreadSync(() -> { @@ -163,6 +165,7 @@ public void testSetMaxPrefetches() throws Exception { * Test to make sure that calling the setPrefetchTtlSeconds API won't cause any obvious errors. */ @Test + @Ignore("https://crbug.com/501069897") public void testSetPrefetchTtlSeconds() throws Exception { WebkitUtils.checkFeature(WebViewFeature.PREFETCH_CACHE_V1); WebkitUtils.onMainThreadSync(() -> { diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.java deleted file mode 100644 index 8de0681252988..0000000000000 --- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.androidx.webkit; - -import android.app.Activity; -import android.os.Bundle; -import android.webkit.WebView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SwitchCompat; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewClientCompat; -import androidx.webkit.WebViewCompat; -import androidx.webkit.WebViewFeature; - -import org.jspecify.annotations.Nullable; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * An {@link Activity} to demonstrate how to allowlist a set of domains from Safe Browsing checks. - * This includes buttons to toggle whether the allowlist is on or off. - */ -public class AllowlistActivity extends AppCompatActivity { - - private WebView mAllowlistWebView; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_allowlist); - setTitle(R.string.allowlist_activity_title); - WebkitHelpers.enableEdgeToEdge(this); - WebkitHelpers.appendWebViewVersionToTitle(this); - - if (!WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) { - WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available); - return; - } - - SwitchCompat allowlistSwitch = findViewById(R.id.allowlist_switch); - allowlistSwitch.setChecked(true); - allowlistSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (isChecked) { - allowlistSafeBrowsingTestSite(null); - } else { - clearAllowlist(); - } - }); - - mAllowlistWebView = findViewById(R.id.allowlist_webview); - - // Allow mAllowlistWebView to handle navigations. - mAllowlistWebView.setWebViewClient(new WebViewClientCompat()); - - if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { - WebSettingsCompat.setSafeBrowsingEnabled(mAllowlistWebView.getSettings(), true); - } - - // Set the allowlist and load the test site. - allowlistSafeBrowsingTestSite( - () -> mAllowlistWebView.loadUrl(SafeBrowsingHelpers.TEST_SAFE_BROWSING_SITE)); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - clearAllowlist(); - } - - @SuppressWarnings("deprecation") - @Override - public void onBackPressed() { - if (mAllowlistWebView.canGoBack()) { - mAllowlistWebView.goBack(); - } else { - super.onBackPressed(); - } - } - - private void clearAllowlist() { - // To clear the allowlist (and check all domains with Safe Browsing), pass an empty list. - final Activity activity = this; - WebViewCompat.setSafeBrowsingAllowlist( - Collections.emptySet(), success -> { - if (!success) { - WebkitHelpers.showMessageInActivity(activity, - R.string.invalid_allowlist_input_message); - } - // Nothing interesting to do if this succeeds, let user continue to use the app. - }); - } - - private void allowlistSafeBrowsingTestSite(@Nullable Runnable onSuccess) { - // Configure an allowlist of domains. Pages/resources loaded from these domains will never - // be checked by Safe Browsing (until a new allowlist is applied). - final Set allowlist = new HashSet<>(); - allowlist.add(SafeBrowsingHelpers.TEST_SAFE_BROWSING_DOMAIN); - final Activity activity = this; - WebViewCompat.setSafeBrowsingAllowlist(allowlist, success -> { - if (success) { - if (onSuccess != null) { - onSuccess.run(); - } - } else { - WebkitHelpers.showMessageInActivity(activity, - R.string.invalid_allowlist_input_message); - } - }); - } -} diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.kt b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.kt new file mode 100644 index 0000000000000..b7ec9ea37c94e --- /dev/null +++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AllowlistActivity.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2026 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 com.example.androidx.webkit + +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SwitchCompat +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature + +/** + * An {@link Activity} to demonstrate how to allowlist a set of domains from Safe Browsing checks. + * This includes buttons to toggle whether the allowlist is on or off. + */ +class AllowlistActivity : AppCompatActivity() { + + private lateinit var allowListWebView: WebView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_allowlist) + setTitle(R.string.allowlist_activity_title) + setUpDemoAppActivity() + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) { + showMessage(R.string.webkit_api_not_available) + return + } + + findViewById(R.id.allowlist_switch).apply { + isChecked = true + setOnCheckedChangeListener { _, _ -> + if (isChecked) { + allowlistSafeBrowsingTestSite() + } else { + clearAllowlist() + } + } + } + + allowListWebView = + findViewById(R.id.allowlist_webview).apply { + // Allow allowListWebView to handle navigations. + webViewClient = WebViewClient() + } + + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { + WebSettingsCompat.setSafeBrowsingEnabled(allowListWebView.settings, true) + } + + // Set the allowlist and load the test site. + allowlistSafeBrowsingTestSite { + allowListWebView.loadUrl(SafeBrowsingHelpers.TEST_SAFE_BROWSING_SITE) + } + + onBackPressedDispatcher.addCallback( + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (allowListWebView.canGoBack()) { + allowListWebView.goBack() + } else { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + } + ) + } + + override fun onDestroy() { + super.onDestroy() + clearAllowlist() + } + + // To clear the allowlist (and check all domains with Safe Browsing), pass an empty list. + private fun clearAllowlist() = + WebViewCompat.setSafeBrowsingAllowlist(emptySet()) { success -> + if (!success) { + showMessage(R.string.invalid_allowlist_input_message) + } + // Nothing interesting to do if this succeeds, let user continue to use the app. + } + + private fun allowlistSafeBrowsingTestSite(onSuccess: Runnable? = null) { + // Configure an allowlist of domains. Pages/resources loaded from these domains will never + // be checked by Safe Browsing (until a new allowlist is applied). + val allowList = setOf(SafeBrowsingHelpers.TEST_SAFE_BROWSING_DOMAIN) + WebViewCompat.setSafeBrowsingAllowlist(allowList) { success -> + if (success) { + onSuccess?.run() + } else { + showMessage(R.string.invalid_allowlist_input_message) + } + } + } +} diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebkitDemoAppHelpers.kt b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebkitDemoAppHelpers.kt index a9d62b7e0db73..f9e9f0a3ce752 100644 --- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebkitDemoAppHelpers.kt +++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebkitDemoAppHelpers.kt @@ -67,7 +67,7 @@ fun AppCompatActivity.enableEdgeToEdge() { v: View, insets: WindowInsetsCompat -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.top, systemBars.bottom) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } } diff --git a/xr/scenecore/scenecore-runtime/build.gradle b/xr/scenecore/scenecore-runtime/build.gradle index a620714c6464e..ad51c564cc71d 100644 --- a/xr/scenecore/scenecore-runtime/build.gradle +++ b/xr/scenecore/scenecore-runtime/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation(libs.androidx.core) implementation("androidx.concurrent:concurrent-futures:1.0.0") implementation("com.google.errorprone:error_prone_annotations:2.30.0") - implementation("androidx.media3:media3-exoplayer:1.9.0-beta01") + implementation("androidx.media3:media3-exoplayer:1.10.0") testImplementation(libs.junit) testImplementation(libs.mockitoCore4) diff --git a/xr/scenecore/scenecore-spatial-core/build.gradle b/xr/scenecore/scenecore-spatial-core/build.gradle index 63f8ee644460a..e1a073b70752b 100644 --- a/xr/scenecore/scenecore-spatial-core/build.gradle +++ b/xr/scenecore/scenecore-spatial-core/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation("androidx.annotation:annotation:1.8.1") implementation("androidx.concurrent:concurrent-futures:1.0.0") - implementation("androidx.media3:media3-exoplayer:1.9.0-beta01") + implementation("androidx.media3:media3-exoplayer:1.10.0") implementation(libs.androidx.core) testImplementation(libs.junit) diff --git a/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/SoundFieldAudioComponentImpl.kt b/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/SoundFieldAudioComponentImpl.kt index cd040e96d9bcd..00f619a5abb45 100644 --- a/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/SoundFieldAudioComponentImpl.kt +++ b/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/SoundFieldAudioComponentImpl.kt @@ -16,12 +16,9 @@ package androidx.xr.scenecore.spatial.core -import android.annotation.SuppressLint import android.content.Context import android.media.AudioFormat import android.media.AudioTrack -import androidx.annotation.OptIn -import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.audio.AudioOutputProvider import androidx.media3.exoplayer.audio.AudioTrackAudioOutputProvider import androidx.media3.exoplayer.audio.ForwardingAudioOutputProvider @@ -56,10 +53,6 @@ internal class SoundFieldAudioComponentImpl( private val forwardingProvider = object : ForwardingAudioOutputProvider(audioTrackAudioOutputProvider) { - // TODO: b/486263448 - Upgrade to exoplayer 1.10.0 once its released. The needed - // constants are stable in that version. - @SuppressLint("IllegalExperimentalApiUsage") // This library is still in alpha. - @OptIn(UnstableApi::class) override fun getOutputConfig( formatConfig: AudioOutputProvider.FormatConfig ): AudioOutputProvider.OutputConfig { diff --git a/xr/scenecore/scenecore-testing/build.gradle b/xr/scenecore/scenecore-testing/build.gradle index 1dbb4d7aa9c45..4a61829b5ea2e 100644 --- a/xr/scenecore/scenecore-testing/build.gradle +++ b/xr/scenecore/scenecore-testing/build.gradle @@ -40,7 +40,7 @@ dependencies { implementation(libs.testExtTruth) implementation("androidx.annotation:annotation:1.8.1") implementation("androidx.concurrent:concurrent-futures:1.0.0") - implementation("androidx.media3:media3-exoplayer:1.9.0-beta01") + implementation("androidx.media3:media3-exoplayer:1.10.0") testImplementation(project(":kruth:kruth")) testImplementation(libs.testRunner) diff --git a/xr/scenecore/scenecore/build.gradle b/xr/scenecore/scenecore/build.gradle index b56d32d9e3da6..e647caf2c90d8 100644 --- a/xr/scenecore/scenecore/build.gradle +++ b/xr/scenecore/scenecore/build.gradle @@ -35,7 +35,7 @@ dependencies { api(project(":xr:arcore:arcore")) api(project(":xr:runtime:runtime")) api(project(":xr:scenecore:scenecore-runtime")) - api("androidx.media3:media3-exoplayer:1.9.0-beta01") + api("androidx.media3:media3-exoplayer:1.10.0") implementation(project(":xr:scenecore:scenecore-spatial-core")) implementation(project(":xr:scenecore:scenecore-spatial-rendering"))