Merge "Rename AppFunctionEntity to AppFunctionSerializable" into androidx-main
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
index 865af3a..d37793f 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
@@ -163,8 +163,6 @@
javaSample("sample.kotlin.UseKtExperimentalFromJava")
)
- // TODO(b/210881073): Access to annotated property `field` is still not detected.
-
val expected =
"""
src/sample/kotlin/UseKtExperimentalFromJava.java:28: Error: This declaration is opt-in and its usage should be marked with @sample.kotlin.ExperimentalKotlinAnnotation or @OptIn(markerClass = sample.kotlin.ExperimentalKotlinAnnotation.class) [UnsafeOptInUsageError]
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
index c2c6356..25a9639 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
@@ -229,8 +229,6 @@
javaSample("sample.optin.UseKtExperimentalFromJava")
)
- // TODO(b/210881073): Access to annotated property `field` is still not detected.
-
val expected =
"""
src/sample/optin/UseKtExperimentalFromJava.java:28: Error: This declaration is opt-in and its usage should be marked with @sample.optin.ExperimentalKotlinAnnotation or @OptIn(markerClass = sample.optin.ExperimentalKotlinAnnotation.class) [UnsafeOptInUsageError]
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
index 81f6291..6151d45 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
@@ -19,6 +19,7 @@
import androidx.appfunctions.compiler.core.ProcessingException
import androidx.appfunctions.compiler.core.logException
import androidx.appfunctions.compiler.processors.AppFunctionIdProcessor
+import androidx.appfunctions.compiler.processors.AppFunctionInventoryProcessor
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
@@ -51,10 +52,9 @@
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
val idProcessor = AppFunctionIdProcessor(environment.codeGenerator)
+ val inventoryProcessor = AppFunctionInventoryProcessor(environment.codeGenerator)
return AppFunctionCompiler(
- listOf(
- idProcessor,
- ),
+ listOf(idProcessor, inventoryProcessor),
environment.logger,
)
}
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 f3f5fbb..fe0d3fa 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
@@ -22,6 +22,8 @@
object IntrospectionHelper {
// Package names
private const val APP_FUNCTIONS_PACKAGE_NAME = "androidx.appfunctions"
+ private const val APP_FUNCTIONS_INTERNAL_PACKAGE_NAME = "androidx.appfunctions.internal"
+ private const val APP_FUNCTIONS_METADATA_PACKAGE_NAME = "androidx.appfunctions.metadata"
// Annotation classes
object AppFunctionAnnotation {
@@ -30,4 +32,8 @@
// Classes
val APP_FUNCTION_CONTEXT_CLASS = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionContext")
+ val APP_FUNCTION_INVENTORY_CLASS =
+ ClassName(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, "AppFunctionInventory")
+ val APP_FUNCTION_METADATA_CLASS =
+ ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionMetadata")
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt
new file mode 100644
index 0000000..cedc597
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appfunctions.compiler.processors
+
+import androidx.appfunctions.compiler.AppFunctionCompiler
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver.AnnotatedAppFunctions
+import androidx.appfunctions.compiler.core.IntrospectionHelper
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asClassName
+import com.squareup.kotlinpoet.buildCodeBlock
+
+/**
+ * Generates implementations for the AppFunctionInventory interface.
+ *
+ * It resolves all functions in a class annotated with `@AppFunction`, and generates the
+ * corresponding metadata for those functions.
+ */
+class AppFunctionInventoryProcessor(
+ private val codeGenerator: CodeGenerator,
+) : SymbolProcessor {
+ override fun process(resolver: Resolver): List<KSAnnotated> {
+ val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
+ val appFunctionClasses = appFunctionSymbolResolver.resolveAnnotatedAppFunctions()
+ for (appFunctionClass in appFunctionClasses) {
+ generateAppFunctionInventoryClass(appFunctionClass)
+ }
+ return emptyList()
+ }
+
+ private fun generateAppFunctionInventoryClass(appFunctionClass: AnnotatedAppFunctions) {
+ val originalPackageName = appFunctionClass.classDeclaration.packageName.asString()
+ val originalClassName = appFunctionClass.classDeclaration.simpleName.asString()
+
+ val inventoryClassName = getAppFunctionInventoryClassName(originalClassName)
+ val inventoryClassBuilder = TypeSpec.classBuilder(inventoryClassName)
+ inventoryClassBuilder.addSuperinterface(IntrospectionHelper.APP_FUNCTION_INVENTORY_CLASS)
+ inventoryClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
+ inventoryClassBuilder.addProperty(buildFunctionIdToMetadataMapProperty())
+
+ val fileSpec =
+ FileSpec.builder(originalPackageName, inventoryClassName)
+ .addType(inventoryClassBuilder.build())
+ .build()
+ codeGenerator
+ .createNewFile(
+ Dependencies(
+ aggregating = false,
+ checkNotNull(appFunctionClass.classDeclaration.containingFile)
+ ),
+ originalPackageName,
+ inventoryClassName
+ )
+ .bufferedWriter()
+ .use { fileSpec.writeTo(it) }
+ }
+
+ /** Creates the `functionIdToMetadataMap` property of the `AppFunctionInventory`. */
+ private fun buildFunctionIdToMetadataMapProperty(): PropertySpec {
+ return PropertySpec.builder(
+ "functionIdToMetadataMap",
+ Map::class.asClassName()
+ .parameterizedBy(
+ String::class.asClassName(),
+ IntrospectionHelper.APP_FUNCTION_METADATA_CLASS
+ ),
+ )
+ .addModifiers(KModifier.OVERRIDE)
+ // TODO: Actually build map properties
+ .initializer(buildCodeBlock { addStatement("mapOf()") })
+ .build()
+ }
+
+ private fun getAppFunctionInventoryClassName(functionClassName: String): String {
+ return "$%s_AppFunctionInventory_Impl".format(functionClassName)
+ }
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
index 3e628c7..8860052 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
@@ -43,7 +43,7 @@
}
@Test
- fun testSimpleFunction() {
+ fun testSimpleFunction_genAppFunctionIds_success() {
val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
compilationTestHelper.assertSuccessWithContent(
@@ -54,7 +54,7 @@
}
@Test
- fun testMissingFirstParameter() {
+ fun testMissingFirstParameter_hasCompileError() {
val report =
compilationTestHelper.compileAll(sourceFileNames = listOf("MissingFirstParameter.KT"))
@@ -68,7 +68,7 @@
}
@Test
- fun testIncorrectFirstParameter() {
+ fun testIncorrectFirstParameter_hasCompileError() {
val report =
compilationTestHelper.compileAll(sourceFileNames = listOf("IncorrectFirstParameter.KT"))
@@ -80,4 +80,15 @@
" ^"
)
}
+
+ @Test
+ fun testSimpleFunction_genAppFunctionInventoryImpl_success() {
+ val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
+
+ compilationTestHelper.assertSuccessWithContent(
+ report = report,
+ expectGeneratedFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
+ goldenFileName = "$%s".format("SimpleFunction_AppFunctionInventory_Impl.KT")
+ )
+ }
}
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT b/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT
new file mode 100644
index 0000000..e7653db3
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT
@@ -0,0 +1,12 @@
+package com.testdata
+
+import androidx.appfunctions.`internal`.AppFunctionInventory
+import androidx.appfunctions.metadata.AppFunctionMetadata
+import javax.`annotation`.processing.Generated
+import kotlin.String
+import kotlin.collections.Map
+
+@Generated("androidx.appfunctions.compiler.AppFunctionCompiler")
+public class `$SimpleFunction_AppFunctionInventory_Impl` : AppFunctionInventory {
+ override val functionIdToMetadataMap: Map<String, AppFunctionMetadata> = mapOf()
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
new file mode 100644
index 0000000..59c4bca
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.internal
+
+import androidx.annotation.RestrictTo
+import androidx.appfunctions.AppFunctionContext
+
+/**
+ * An interface for invoking app functions.
+ *
+ * This interface defines a contract for invoking a set of AppFunctions declared in an application.
+ * Each AppFunction implementation class has a corresponding generated Invoker class that implements
+ * this interface.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppFunctionInvoker {
+ /**
+ * Sets of function ids that the invoker supports.
+ *
+ * For example, consider the following AppFunction implementation class:
+ * ```kotlin
+ * class NoteFunctions : CreateNote, EditNote {
+ * @AppFunction
+ * override suspend fun createNote() : Note { ... }
+ *
+ * @AppFunction
+ * override suspend fun editNote() : Note { ... }
+ * }
+ * ```
+ *
+ * The set of supported Ids would include the respective function id for the `createNote` and
+ * `editNote` functions.
+ */
+ public val supportedFunctionIds: Set<String>
+
+ /**
+ * Invokes an AppFunction identified by [functionIdentifier], with [parameters].
+ *
+ * @throws [androidx.appfunctions.AppFunctionException] with error code
+ * [androidx.appfunctions.AppFunctionException.ERROR_FUNCTION_NOT_FOUND] if called with
+ * invalid function identifier or code
+ * [androidx.appfunctions.AppFunctionException.ERROR_INVALID_ARGUMENT] if called with invalid
+ * parameters.
+ */
+ public suspend fun unsafeInvoke(
+ appFunctionContext: AppFunctionContext,
+ functionIdentifier: String,
+ parameters: Map<String, Any?>,
+ ): Any?
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index aea21e9..7322275 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -698,9 +698,14 @@
project,
kotlinMultiplatformAndroidComponentsExtension
)
- // Propagate the compileSdk value into minCompileSdk.
- kotlinMultiplatformAndroidTarget.aarMetadata.minCompileSdk =
- kotlinMultiplatformAndroidTarget.compileSdk
+ kotlinMultiplatformAndroidComponentsExtension.apply {
+ finalizeDsl {
+ // Propagate the compileSdk value into minCompileSdk. Must be done after the DSL in
+ // build.gradle files (that sets compileSdk in the first place) is evaluated.
+ kotlinMultiplatformAndroidTarget.aarMetadata.minCompileSdk =
+ kotlinMultiplatformAndroidTarget.compileSdk
+ }
+ }
project.disableStrictVersionConstraints()
project.configureProjectForApiTasks(AndroidMultiplatformApiTaskConfig, androidXExtension)
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index 28281b1..dfeb5d2 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -102,6 +102,7 @@
cameraQuirks,
ZslControlNoOpImpl(),
NoOpTemplateParamsOverride,
+ cameraMetadata,
)
val cameraGraph = cameraPipe.create(cameraGraphConfig)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 0a33480..a999884 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.OutputConfiguration
@@ -32,6 +33,7 @@
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode
import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.AT_LEAST
import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.InputStream
@@ -125,6 +127,7 @@
constructor(
private val cameraPipe: CameraPipe,
private val cameraDevices: CameraDevices,
+ private val cameraMetadata: CameraMetadata?,
@GuardedBy("lock") private val cameraCoordinator: CameraCoordinator,
private val callbackMap: CameraCallbackMap,
private val requestListener: ComboRequestListener,
@@ -658,7 +661,7 @@
streamConfigMap: MutableMap<CameraStream.Config, DeferrableSurface>,
isExtensions: Boolean = false,
): CameraGraph.Config {
- return Companion.createCameraGraphConfig(
+ return createCameraGraphConfig(
sessionConfigAdapter,
streamConfigMap,
callbackMap,
@@ -667,6 +670,7 @@
cameraQuirks,
zslControl,
templateParamsOverride,
+ cameraMetadata,
isExtensions,
)
}
@@ -909,6 +913,7 @@
cameraQuirks: CameraQuirks,
zslControl: ZslControl,
templateParamsOverride: TemplateParamsOverride,
+ cameraMetadata: CameraMetadata?,
isExtensions: Boolean = false,
): CameraGraph.Config {
var containsVideo = false
@@ -964,7 +969,8 @@
streamUseCase =
getStreamUseCase(
deferrableSurface,
- sessionConfigAdapter.surfaceToStreamUseCaseMap
+ sessionConfigAdapter.surfaceToStreamUseCaseMap,
+ cameraMetadata,
),
streamUseHint =
getStreamUseHint(
@@ -1058,9 +1064,26 @@
private fun getStreamUseCase(
deferrableSurface: DeferrableSurface,
- mapping: Map<DeferrableSurface, Long>
+ mapping: Map<DeferrableSurface, Long>,
+ cameraMetadata: CameraMetadata?,
): OutputStream.StreamUseCase? {
- return mapping[deferrableSurface]?.let { OutputStream.StreamUseCase(it) }
+ val expectedStreamUseCase =
+ mapping[deferrableSurface]?.let { OutputStream.StreamUseCase(it) }
+ return if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ expectedStreamUseCase != null &&
+ cameraMetadata
+ ?.get(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES)
+ ?.contains(expectedStreamUseCase.value) == true
+ ) {
+ expectedStreamUseCase
+ } else {
+ Log.warn {
+ "Expected stream use case for $deferrableSurface, " +
+ "$expectedStreamUseCase cannot be set!"
+ }
+ null
+ }
}
private fun getStreamUseHint(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index de2b4c5..743e748 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -689,6 +689,7 @@
emptySet(),
mapOf(fakeCameraBackend.id to listOf(fakeCameraMetadata))
),
+ cameraMetadata = fakeCameraMetadata,
cameraCoordinator = CameraCoordinatorAdapter(cameraPipe, cameraPipe.cameras()),
callbackMap = CameraCallbackMap(),
requestListener = ComboRequestListener(),
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
index bb2cb3f..cf93534 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -439,6 +439,7 @@
public val VIDEO_RECORD: StreamUseCase = StreamUseCase(3)
public val PREVIEW_VIDEO_STILL: StreamUseCase = StreamUseCase(4)
public val VIDEO_CALL: StreamUseCase = StreamUseCase(5)
+ public val CROPPED_RAW: StreamUseCase = StreamUseCase(6)
}
}
@@ -466,12 +467,12 @@
*/
public fun isValidForHighSpeedOperatingMode(): Boolean {
return this.streamUseCase == null ||
- this.streamUseCase == OutputStream.StreamUseCase.DEFAULT ||
- this.streamUseCase == OutputStream.StreamUseCase.PREVIEW ||
- this.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD ||
+ this.streamUseCase == DEFAULT ||
+ this.streamUseCase == StreamUseCase.PREVIEW ||
+ this.streamUseCase == StreamUseCase.VIDEO_RECORD ||
this.streamUseHint == null ||
- this.streamUseHint == OutputStream.StreamUseHint.DEFAULT ||
- this.streamUseHint == OutputStream.StreamUseHint.VIDEO_RECORD
+ this.streamUseHint == StreamUseHint.DEFAULT ||
+ this.streamUseHint == StreamUseHint.VIDEO_RECORD
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index 028a1eb..5618494 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -22,7 +22,6 @@
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
import androidx.camera.camera2.pipe.config.Camera2ControllerScope
@@ -184,21 +183,13 @@
private val threads: Threads,
private val streamGraph: StreamGraphImpl,
private val graphConfig: CameraGraph.Config,
- private val camera2MetadataProvider: Camera2MetadataProvider
) : CaptureSessionFactory {
override fun create(
cameraDevice: CameraDeviceWrapper,
surfaces: Map<StreamId, Surface>,
captureSessionState: CaptureSessionState
): Map<StreamId, OutputConfigurationWrapper> {
- val outputs =
- buildOutputConfigurations(
- graphConfig,
- streamGraph,
- surfaces,
- camera2MetadataProvider,
- cameraDevice.cameraId
- )
+ val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
if (outputs.all.isEmpty()) {
Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
captureSessionState.onSessionFinalized()
@@ -240,7 +231,6 @@
private val threads: Threads,
private val graphConfig: CameraGraph.Config,
private val streamGraph: StreamGraphImpl,
- private val camera2MetadataProvider: Camera2MetadataProvider
) : CaptureSessionFactory {
override fun create(
cameraDevice: CameraDeviceWrapper,
@@ -259,14 +249,7 @@
else -> graphConfig.sessionMode.mode
}
- val outputs =
- buildOutputConfigurations(
- graphConfig,
- streamGraph,
- surfaces,
- camera2MetadataProvider,
- cameraDevice.cameraId
- )
+ val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
if (outputs.all.isEmpty()) {
Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
captureSessionState.onSessionFinalized()
@@ -315,8 +298,6 @@
graphConfig: CameraGraph.Config,
streamGraph: StreamGraphImpl,
surfaces: Map<StreamId, Surface>,
- camera2MetadataProvider: Camera2MetadataProvider,
- cameraId: CameraId
): OutputConfigurations {
val allOutputs = arrayListOf<OutputConfigurationWrapper>()
val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
@@ -362,8 +343,6 @@
} else {
null
},
- cameraId = cameraId,
- camera2MetadataProvider = camera2MetadataProvider
)
if (output == null) {
Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
@@ -399,8 +378,6 @@
} else {
null
},
- cameraId = cameraId,
- camera2MetadataProvider = camera2MetadataProvider
)
if (output == null) {
Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
@@ -476,14 +453,7 @@
}
}
- val outputs =
- buildOutputConfigurations(
- graphConfig,
- streamGraph,
- surfaces,
- camera2MetadataProvider,
- cameraDevice.cameraId
- )
+ val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
if (outputs.all.isEmpty()) {
Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index 90d3918..c823908 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -161,8 +161,6 @@
surfaceSharing: Boolean = false,
surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
physicalCameraId: CameraId? = null,
- cameraId: CameraId? = null,
- camera2MetadataProvider: Camera2MetadataProvider? = null,
): OutputConfigurationWrapper? {
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
@@ -261,14 +259,9 @@
}
}
- if (streamUseCase != null && cameraId != null && camera2MetadataProvider != null) {
+ if (streamUseCase != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- val cameraMetadata = camera2MetadataProvider.awaitCameraMetadata(cameraId)
- val availableStreamUseCases =
- Api33Compat.getAvailableStreamUseCases(cameraMetadata)
- if (availableStreamUseCases?.contains(streamUseCase.value) == true) {
- Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
- }
+ Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
}
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index 96d36ea..ab6cf33 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -50,6 +50,7 @@
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
+import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
@@ -64,12 +65,14 @@
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
import androidx.camera.camera2.internal.util.TestUtil;
+import androidx.camera.camera2.interop.Camera2Interop;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraXConfig;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageProxy;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
@@ -431,6 +434,92 @@
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void setTorchStrengthLevel_valueUpdated()
+ throws ExecutionException, InterruptedException {
+ assumeTrue(mHasFlashUnit);
+
+ // Arrange
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+ imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+ mCamera = CameraUtil.createCameraAndAttachUseCase(
+ ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+ imageAnalysis);
+ Camera2CameraControlImpl camera2CameraControlImpl =
+ TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+ camera2CameraControlImpl.enableTorch(true).get();
+
+ // Act
+ int maxStrength = mCamera.getCameraInfo().getMaxTorchStrengthLevel();
+ int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
+ // If the default strength is the max, set the strength to 1, otherwise, set to max.
+ int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
+ camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+
+ // Assert: the customized strength is applied
+ Camera2ImplConfig camera2Config = new Camera2ImplConfig(
+ camera2CameraControlImpl.getSessionConfig().getImplementationOptions());
+ assertThat(camera2Config.getCaptureRequestOption(
+ CaptureRequest.FLASH_STRENGTH_LEVEL, -1))
+ .isEqualTo(customizedStrength);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void setTorchStrengthLevel_throwExceptionIfLessThanOne()
+ throws ExecutionException, InterruptedException {
+ assumeTrue(mHasFlashUnit);
+
+ // Arrange
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+ imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+ mCamera = CameraUtil.createCameraAndAttachUseCase(
+ ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+ imageAnalysis);
+ Camera2CameraControlImpl camera2CameraControlImpl =
+ TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+ camera2CameraControlImpl.enableTorch(true).get();
+
+ // Act & Assert
+ try {
+ camera2CameraControlImpl.setTorchStrengthLevelAsync(0).get();
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+ return;
+ }
+
+ fail("setTorchStrength didn't fail with an IllegalArgumentException.");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void setTorchStrengthLevel_throwExceptionIfLargerThanMax()
+ throws ExecutionException, InterruptedException {
+ assumeTrue(mHasFlashUnit);
+
+ // Arrange
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+ imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+ mCamera = CameraUtil.createCameraAndAttachUseCase(
+ ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+ imageAnalysis);
+ Camera2CameraControlImpl camera2CameraControlImpl =
+ TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+ camera2CameraControlImpl.enableTorch(true).get();
+
+ // Act & Assert
+ try {
+ camera2CameraControlImpl.setTorchStrengthLevelAsync(
+ mCamera.getCameraInfo().getMaxTorchStrengthLevel() + 1).get();
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+ return;
+ }
+
+ fail("setTorchStrength didn't fail with an IllegalArgumentException.");
+ }
+
@SdkSuppress(minSdkVersion = 35)
@Test
public void enableLowLightBoost_aeModeSetAndRequestUpdated() throws InterruptedException {
@@ -516,6 +605,57 @@
ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void capture_torchAsFlash_shouldUseDefaultTorchStrength()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ assumeTrue(mHasFlashUnit);
+
+ // Arrange: explicitly set flash type to use torch as flash
+ ImageCapture.Builder imageCaptureBuilder = new ImageCapture.Builder().setFlashType(
+ ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH).setFlashMode(
+ ImageCapture.FLASH_MODE_ON);
+ CameraCaptureSession.CaptureCallback captureCallback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ new Camera2Interop.Extender<>(imageCaptureBuilder).setSessionCaptureCallback(
+ captureCallback);
+ ImageCapture imageCapture = imageCaptureBuilder.build();
+ mCamera = CameraUtil.createCameraAndAttachUseCase(
+ ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+ imageCapture);
+ Camera2CameraControlImpl camera2CameraControlImpl =
+ TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+
+ // Act
+ int maxStrength = mCamera.getCameraInfo().getMaxTorchStrengthLevel();
+ int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
+ // If the default strength is the max, set the strength to 1, otherwise, set to max.
+ int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
+ camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+
+ // Assert: the capture uses default torch strength
+ CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
+ captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ captureConfigBuilder.addSurface(imageCapture.getSessionConfig().getSurfaces().get(0));
+
+ camera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_ON);
+ camera2CameraControlImpl.submitStillCaptureRequests(
+ Arrays.asList(captureConfigBuilder.build()),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH)
+ .get(5, TimeUnit.SECONDS);
+ ArgumentCaptor<CaptureRequest> captureRequestCaptor =
+ ArgumentCaptor.forClass(CaptureRequest.class);
+ verify(captureCallback, timeout(5000).atLeastOnce())
+ .onCaptureCompleted(any(), captureRequestCaptor.capture(), any());
+ List<CaptureRequest> results = captureRequestCaptor.getAllValues();
+ for (CaptureRequest result : results) {
+ // None of the capture capture should be sent with the customized strength.
+ assertThat(result.get(CaptureRequest.FLASH_STRENGTH_LEVEL)).isNotEqualTo(
+ customizedStrength);
+ }
+ }
+
private void captureTest(int captureMode, int flashType)
throws ExecutionException, InterruptedException, TimeoutException {
ImageCapture imageCapture = new ImageCapture.Builder().build();
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index a1b57bc..de994f6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assume.assumeTrue;
import android.content.Context;
+import android.os.Build;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.internal.util.TestUtil;
@@ -63,6 +64,8 @@
private TorchControl mTorchControl;
private CameraUseCaseAdapter mCamera;
+ private Camera2CameraControlImpl mCameraControl;
+ private boolean mIsTorchStrengthSupported;
@Before
public void setUp() {
@@ -83,10 +86,9 @@
// Make ImageAnalysis active.
imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
- Camera2CameraControlImpl cameraControl =
- TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
-
- mTorchControl = cameraControl.getTorchControl();
+ mCameraControl = TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+ mTorchControl = mCameraControl.getTorchControl();
+ mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel() > 1;
}
@After
@@ -113,4 +115,30 @@
// Future should return with no exception
future.get();
}
+
+ @Test(timeout = 5000L)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void setTorchStrengthLevel_futureCompleteWhenTorchIsOnLevel()
+ throws ExecutionException, InterruptedException {
+ assumeTrue(mIsTorchStrengthSupported);
+
+ // Arrange: turn on the torch
+ mTorchControl.enableTorch(true).get();
+
+ // Act & Assert: the future completes
+ mTorchControl.setTorchStrengthLevel(
+ mCamera.getCameraInfo().getMaxTorchStrengthLevel()).get();
+ }
+
+ @Test(timeout = 5000L)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void setTorchStrengthLevel_futureCompleteWhenTorchIsOffLevel()
+ throws ExecutionException, InterruptedException {
+ assumeTrue(mIsTorchStrengthSupported);
+
+ // Arrange: the torch is default off
+ // Act & Assert: the future completes
+ mTorchControl.setTorchStrengthLevel(
+ mCamera.getCameraInfo().getMaxTorchStrengthLevel()).get();
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 6188489..8a2a439 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -34,6 +34,7 @@
import android.util.Rational;
import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.impl.Camera2ImplConfig;
@@ -139,7 +140,10 @@
private ImageCapture.ScreenFlash mScreenFlash;
// use volatile modifier to make these variables in sync in all threads.
- private volatile boolean mIsTorchOn = false;
+ @TorchControl.TorchStateInternal
+ private volatile int mTorchState = TorchControl.OFF;
+ @IntRange(from = 1)
+ private volatile int mTorchStrength;
private volatile boolean mIsLowLightBoostOn = false;
@ImageCapture.FlashMode
private volatile int mFlashMode = FLASH_MODE_OFF;
@@ -205,6 +209,7 @@
this, scheduler, mExecutor, cameraQuirks);
mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
+ mTorchStrength = mCameraCharacteristics.getDefaultTorchStrengthLevel();
mLowLightBoostControl = new LowLightBoostControl(this, mCameraCharacteristics, mExecutor);
if (Build.VERSION.SDK_INT >= 23) {
mZslControl = new ZslControlImpl(mCameraCharacteristics, mExecutor);
@@ -504,6 +509,30 @@
return mExposureControl.setExposureCompensationIndex(exposure);
}
+ @Override
+ public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+ @IntRange(from = 1) int torchStrengthLevel) {
+ if (!isControlInUse()) {
+ return Futures.immediateFailedFuture(
+ new OperationCanceledException("Camera is not active."));
+ }
+ if (torchStrengthLevel < 1
+ || torchStrengthLevel > mCameraCharacteristics.getMaxTorchStrengthLevel()) {
+ return Futures.immediateFailedFuture(new IllegalArgumentException(
+ "The specified torch strength is not within the valid range."));
+ }
+ return Futures.nonCancellationPropagating(mTorchControl.setTorchStrengthLevel(
+ Math.min(torchStrengthLevel, mCameraCharacteristics.getMaxTorchStrengthLevel())));
+ }
+
+ @ExecutedBy("mExecutor")
+ void setTorchStrengthLevelInternal(@IntRange(from = 1) int torchStrengthLevel) {
+ mTorchStrength = torchStrengthLevel;
+ if (isTorchOn()) {
+ updateSessionConfigSynchronous();
+ }
+ }
+
/** {@inheritDoc} */
@Override
public @NonNull ListenableFuture<List<Void>> submitStillCaptureRequests(
@@ -654,14 +683,14 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mExecutor")
- void enableTorchInternal(boolean torch) {
+ void enableTorchInternal(@TorchControl.TorchStateInternal int torchState) {
// When low-light boost is on, any torch related operations will be ignored.
if (mIsLowLightBoostOn) {
return;
}
- mIsTorchOn = torch;
- if (!torch) {
+ mTorchState = torchState;
+ if (torchState == TorchControl.OFF) {
// On some devices, needs to reset the AE/flash state to ensure that the torch can be
// turned off.
resetAeFlashState();
@@ -677,11 +706,11 @@
}
// Forces turn off torch before enabling low-light boost.
- if (lowLightBoost && mIsTorchOn) {
+ if (lowLightBoost && isTorchOn()) {
// On some devices, needs to reset the AE/flash state to ensure that the torch can be
// turned off.
resetAeFlashState();
- mIsTorchOn = false;
+ mTorchState = TorchControl.OFF;
mTorchControl.forceUpdateTorchStateToOff();
}
@@ -706,7 +735,7 @@
@ExecutedBy("mExecutor")
boolean isTorchOn() {
- return mIsTorchOn;
+ return mTorchState != TorchControl.OFF;
}
@ExecutedBy("mExecutor")
@@ -746,9 +775,20 @@
if (mIsLowLightBoostOn) {
aeMode = CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
- } else if (mIsTorchOn) {
+ } else if (isTorchOn()) {
builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH, Config.OptionPriority.REQUIRED);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (mTorchState == TorchControl.ON) {
+ builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_STRENGTH_LEVEL,
+ mTorchStrength, Config.OptionPriority.REQUIRED);
+ } else if (mTorchState == TorchControl.USED_AS_FLASH) {
+ // If torch is used as flash, use the default torch strength instead.
+ builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_STRENGTH_LEVEL,
+ mCameraCharacteristics.getDefaultTorchStrengthLevel(),
+ Config.OptionPriority.REQUIRED);
+ }
+ }
} else {
switch (mFlashMode) {
case FLASH_MODE_OFF:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 81a5d90..aeb7058 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -38,6 +38,7 @@
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
import androidx.annotation.OptIn;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -110,6 +111,8 @@
@GuardedBy("mLock")
private @Nullable RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
@GuardedBy("mLock")
+ private @Nullable RedirectableLiveData<Integer> mRedirectTorchStrengthLiveData = null;
+ @GuardedBy("mLock")
private @Nullable RedirectableLiveData<Integer> mRedirectLowLightBoostStateLiveData = null;
@GuardedBy("mLock")
private @Nullable RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
@@ -161,6 +164,11 @@
mCamera2CameraControlImpl.getTorchControl().getTorchState());
}
+ if (mRedirectTorchStrengthLiveData != null) {
+ mRedirectTorchStrengthLiveData.redirectTo(
+ mCamera2CameraControlImpl.getTorchControl().getTorchStrengthLevel());
+ }
+
if (mRedirectLowLightBoostStateLiveData != null) {
mRedirectLowLightBoostStateLiveData.redirectTo(mCamera2CameraControlImpl
.getLowLightBoostControl().getLowLightBoostState());
@@ -709,4 +717,29 @@
return mPhysicalCameraInfos;
}
+
+ @Override
+ @IntRange(from = 1)
+ public int getMaxTorchStrengthLevel() {
+ return mCameraCharacteristicsCompat.getMaxTorchStrengthLevel();
+ }
+
+ @Override
+ public @NonNull LiveData<Integer> getTorchStrengthLevel() {
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ if (mRedirectTorchStrengthLiveData == null) {
+ mRedirectTorchStrengthLiveData = new RedirectableLiveData<>(
+ mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel());
+ }
+ return mRedirectTorchStrengthLiveData;
+ }
+
+ if (mRedirectTorchStrengthLiveData != null) {
+ return mRedirectTorchStrengthLiveData;
+ }
+
+ return mCamera2CameraControlImpl.getTorchControl().getTorchStrengthLevel();
+ }
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 42a38eb..c05cdc7 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -635,7 +635,8 @@
mIsExecuted = true;
ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
- mCameraControl.getTorchControl().enableTorchInternal(completer, true);
+ mCameraControl.getTorchControl().enableTorchInternal(completer,
+ TorchControl.USED_AS_FLASH);
return "TorchOn";
});
return FutureChain.from(future).transformAsync(
@@ -667,7 +668,7 @@
@Override
public void postCapture() {
if (mIsExecuted) {
- mCameraControl.getTorchControl().enableTorchInternal(null, false);
+ mCameraControl.getTorchControl().enableTorchInternal(null, TorchControl.OFF);
Logger.d(TAG, "Turning off torch");
if (mTriggerAePrecapture) {
mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
@@ -795,7 +796,7 @@
return "EnableTorchInternal";
}
Logger.d(TAG, "ScreenFlashTask#preCapture: enable torch");
- mCameraControl.enableTorchInternal(true);
+ mCameraControl.enableTorchInternal(TorchControl.USED_AS_FLASH);
completer.set(null);
return "EnableTorchInternal";
}),
@@ -828,7 +829,7 @@
public void postCapture() {
Logger.d(TAG, "ScreenFlashTask#postCapture");
if (mUseFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()) {
- mCameraControl.enableTorchInternal(false);
+ mCameraControl.enableTorchInternal(TorchControl.OFF);
}
mCameraControl.getFocusMeteringControl().enableExternalFlashAeMode(false).addListener(
() -> Log.d(TAG, "enableExternalFlashAeMode disabled"), mExecutor
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index d4c634e..7ea9db9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -18,7 +18,11 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.os.Build;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.workaround.FlashAvailabilityChecker;
@@ -38,6 +42,8 @@
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
/**
@@ -52,14 +58,32 @@
private static final String TAG = "TorchControl";
static final int DEFAULT_TORCH_STATE = TorchState.OFF;
+ /** Torch is off. */
+ static final int OFF = 0;
+ /** Torch is turned on explicitly by {@link #enableTorch(boolean)}. */
+ static final int ON = 1;
+ /** Torch is turned on as flash by the capture pipeline. */
+ static final int USED_AS_FLASH = 2;
+
+ /** The internal torch state. */
+ @IntDef({OFF, ON, USED_AS_FLASH})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TorchStateInternal {
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
private final Camera2CameraControlImpl mCamera2CameraControlImpl;
private final MutableLiveData<Integer> mTorchState;
+ private final MutableLiveData<Integer> mTorchStrength;
private final boolean mHasFlashUnit;
@CameraExecutor
private final Executor mExecutor;
private boolean mIsActive;
+ private boolean mIsTorchStrengthSupported;
+ private int mDefaultTorchStrength;
+ private int mTargetTorchStrength;
+ private CallbackToFutureAdapter.Completer<Void> mTorchStrengthCompleter;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -79,7 +103,13 @@
mExecutor = executor;
mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
+ mIsTorchStrengthSupported =
+ mHasFlashUnit && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && cameraCharacteristics.getMaxTorchStrengthLevel() > 1;
+ mDefaultTorchStrength = cameraCharacteristics.getDefaultTorchStrengthLevel();
+ mTargetTorchStrength = mDefaultTorchStrength;
mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
+ mTorchStrength = new MutableLiveData<>(mDefaultTorchStrength);
Camera2CameraControlImpl.CaptureResultListener captureResultListener = captureResult -> {
if (mEnableTorchCompleter != null) {
CaptureRequest captureRequest = captureResult.getRequest();
@@ -92,6 +122,16 @@
mEnableTorchCompleter = null;
}
}
+ if (mIsTorchStrengthSupported
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && mTorchStrengthCompleter != null) {
+ Integer torchStrength = captureResult.get(CaptureResult.FLASH_STRENGTH_LEVEL);
+
+ if (torchStrength != null && torchStrength == mTargetTorchStrength) {
+ mTorchStrengthCompleter.set(null);
+ mTorchStrengthCompleter = null;
+ }
+ }
// Return false to keep getting captureResult.
return false;
};
@@ -114,8 +154,10 @@
if (!isActive) {
if (mTargetTorchEnabled) {
mTargetTorchEnabled = false;
- mCamera2CameraControlImpl.enableTorchInternal(false);
- setLiveDataValue(mTorchState, TorchState.OFF);
+ mTargetTorchStrength = mDefaultTorchStrength;
+ mCamera2CameraControlImpl.enableTorchInternal(OFF);
+ setTorchState(OFF);
+ setLiveDataValue(mTorchStrength, mDefaultTorchStrength);
}
if (mEnableTorchCompleter != null) {
@@ -123,6 +165,12 @@
new OperationCanceledException("Camera is not active."));
mEnableTorchCompleter = null;
}
+
+ if (mTorchStrengthCompleter != null) {
+ mTorchStrengthCompleter.setException(
+ new OperationCanceledException("Camera is not active."));
+ mTorchStrengthCompleter = null;
+ }
}
}
@@ -154,11 +202,11 @@
return Futures.immediateFailedFuture(new IllegalStateException("No flash unit"));
}
- setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
+ @TorchStateInternal int torchState = enabled ? ON : OFF;
+ setTorchState(torchState);
return CallbackToFutureAdapter.getFuture(completer -> {
- mExecutor.execute(
- () -> enableTorchInternal(completer, enabled));
+ mExecutor.execute(() -> enableTorchInternal(completer, torchState));
return "enableTorch: " + enabled;
});
}
@@ -175,9 +223,14 @@
return mTorchState;
}
+ @NonNull LiveData<Integer> getTorchStrengthLevel() {
+ return mTorchStrength;
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mExecutor")
- void enableTorchInternal(@Nullable Completer<Void> completer, boolean enabled) {
+ void enableTorchInternal(@Nullable Completer<Void> completer,
+ @TorchStateInternal int torchState) {
if (!mHasFlashUnit) {
if (completer != null) {
completer.setException(new IllegalStateException("No flash unit"));
@@ -186,7 +239,7 @@
}
if (!mIsActive) {
- setLiveDataValue(mTorchState, TorchState.OFF);
+ setTorchState(OFF);
if (completer != null) {
completer.setException(new OperationCanceledException("Camera is not active."));
}
@@ -201,9 +254,9 @@
return;
}
- mTargetTorchEnabled = enabled;
- mCamera2CameraControlImpl.enableTorchInternal(enabled);
- setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
+ mTargetTorchEnabled = torchState != OFF;
+ mCamera2CameraControlImpl.enableTorchInternal(torchState);
+ setTorchState(torchState);
if (mEnableTorchCompleter != null) {
mEnableTorchCompleter.setException(new OperationCanceledException(
"There is a new enableTorch being set"));
@@ -211,6 +264,54 @@
mEnableTorchCompleter = completer;
}
+ ListenableFuture<Void> setTorchStrengthLevel(@IntRange(from = 1) int torchStrengthLevel) {
+ if (!mIsTorchStrengthSupported) {
+ return Futures.immediateFailedFuture(new UnsupportedOperationException(
+ "Setting torch strength is not supported on the device."));
+ }
+
+ setLiveDataValue(mTorchStrength, torchStrengthLevel);
+
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ mExecutor.execute(
+ () -> setTorchStrengthLevelInternal(completer, torchStrengthLevel));
+ return "setTorchStrength: " + torchStrengthLevel;
+ });
+ }
+
+ @ExecutedBy("mExecutor")
+ void setTorchStrengthLevelInternal(@Nullable Completer<Void> completer,
+ @IntRange(from = 1) int torchStrengthLevel) {
+ if (!mIsTorchStrengthSupported) {
+ if (completer != null) {
+ completer.setException(new UnsupportedOperationException(
+ "Setting torch strength is not supported on the device."));
+ }
+ return;
+ }
+
+ if (!mIsActive) {
+ if (completer != null) {
+ completer.setException(new OperationCanceledException("Camera is not active."));
+ }
+ return;
+ }
+
+ mTargetTorchStrength = torchStrengthLevel;
+ mCamera2CameraControlImpl.setTorchStrengthLevelInternal(torchStrengthLevel);
+ if (!mCamera2CameraControlImpl.isTorchOn() && completer != null) {
+ // Complete the future if the torch is not on. The new strength will be applied next
+ // time it's turned on.
+ completer.set(null);
+ } else {
+ if (mTorchStrengthCompleter != null) {
+ mTorchStrengthCompleter.setException(new OperationCanceledException(
+ "There is a new torch strength being set."));
+ }
+ mTorchStrengthCompleter = completer;
+ }
+ }
+
/**
* Force update the torch state to OFF.
*
@@ -225,7 +326,25 @@
}
mTargetTorchEnabled = false;
- setLiveDataValue(mTorchState, TorchState.OFF);
+ setTorchState(OFF);
+ }
+
+ private void setTorchState(@TorchStateInternal int internalState) {
+ @TorchState.State int state;
+ switch (internalState) {
+ case ON:
+ state = TorchState.ON;
+ break;
+ case USED_AS_FLASH:
+ // If torch is turned on as flash, it's considered off because it's not used for
+ // torch purpose.
+ // Fall-through
+ case OFF:
+ // Fall-through
+ default:
+ state = TorchState.OFF;
+ }
+ setLiveDataValue(mTorchState, state);
}
private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index d92dd68..1f2cc60 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -131,6 +131,28 @@
}
/**
+ * Returns the default torch strength level.
+ */
+ public int getDefaultTorchStrengthLevel() {
+ Integer defaultLevel = null;
+ if (Build.VERSION.SDK_INT >= 35) {
+ defaultLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL);
+ }
+ return defaultLevel == null ? 1 : defaultLevel;
+ }
+
+ /**
+ * Returns the maximum torch strength level.
+ */
+ public int getMaxTorchStrengthLevel() {
+ Integer maxLevel = null;
+ if (Build.VERSION.SDK_INT >= 35) {
+ maxLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL);
+ }
+ return maxLevel == null ? 1 : maxLevel;
+ }
+
+ /**
* Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
* workarounds in it.
*/
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 529c505..e7fcc70 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -137,6 +137,8 @@
@RequiresApi(33)
private static final DynamicRangeProfiles CAMERA0_DYNAMIC_RANGE_PROFILES =
new DynamicRangeProfiles(new long[]{DynamicRangeProfiles.HLG10, 0, 0});
+ private static final int CAMERA0_DEFAULT_TORCH_STRENGTH = 25;
+ private static final int CAMERA0_MAX_TORCH_STRENGTH = 50;
private static final String CAMERA1_ID = "1";
private static final int CAMERA1_SUPPORTED_HARDWARE_LEVEL =
@@ -876,6 +878,44 @@
assertThat(supportedDynamicRanges).containsExactly(SDR, HLG_10_BIT);
}
+ @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void apiVersionMet_torchStrengthPropagateCorrectly()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
+ mCameraManagerCompat);
+ int strength = 30;
+ when(mMockTorchControl.getTorchStrengthLevel()).thenReturn(new MutableLiveData<>(strength));
+ cameraInfo.linkWithCameraControl(mMockCameraControl);
+
+ assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(strength);
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void apiVersionMet_canReturnDefaultTorchStrength()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(
+ CAMERA0_DEFAULT_TORCH_STRENGTH);
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void apiVersionMet_canReturnMaxTorchStrength()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(CAMERA0_MAX_TORCH_STRENGTH);
+ }
+
@Config(minSdk = 33)
@Test
public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
@@ -902,6 +942,17 @@
assertThat(supportedDynamicRanges).containsExactly(SDR);
}
+ @Config(maxSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM - 1)
+ @Test
+ public void apiVersionNotMet_returnMaxTorchStrengthOne()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(1);
+ }
+
@Config(maxSdk = 32)
@Test
public void apiVersionNotMet_queryHdrDynamicRangeNotSupported()
@@ -1096,6 +1147,10 @@
CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
}
);
+ shadowCharacteristics0.set(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL,
+ CAMERA0_DEFAULT_TORCH_STRENGTH);
+ shadowCharacteristics0.set(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL,
+ CAMERA0_MAX_TORCH_STRENGTH);
}
// Mock the request capability
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
index 39b8743..87ddf67 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
@@ -353,8 +353,8 @@
return screenFlash
}
- override fun enableTorchInternal(torch: Boolean) {
- isTorchEnabled = torch
+ override fun enableTorchInternal(torchState: Int) {
+ isTorchEnabled = torchState != TorchControl.OFF
}
override fun addCaptureResultListener(listener: CaptureResultListener) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index 6b25593..09718fb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -17,6 +17,7 @@
package androidx.camera.core;
import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -233,6 +234,29 @@
@NonNull ListenableFuture<Integer> setExposureCompensationIndex(int value);
/**
+ * Sets torch strength level.
+ *
+ * <p>The torch strength level only applies on the case that torch is turned on by
+ * {@link #enableTorch(boolean)} and doesn't affect other usages of the flash unit.
+ *
+ * <p>Use the value returned by {@link CameraInfo#getMaxTorchStrengthLevel()} to set the maximum
+ * level the device can provide and use {@code 1} to set the minimum level. If a level
+ * greater than the maximum value or less than {@code 1} is set, the returned
+ * {@link ListenableFuture} will fail with an {@link IllegalArgumentException} and it won't
+ * modify the torch strength.
+ *
+ * @param torchStrengthLevel The desired torch strength level.
+ * @return a {@link ListenableFuture} that is completed when the torch strength has been
+ * applied.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ default @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+ @IntRange(from = 1) int torchStrengthLevel) {
+ return Futures.immediateFailedFuture(new UnsupportedOperationException(
+ "Setting torch strength is not supported on the device."));
+ }
+
+ /**
* An exception representing a failure that the operation is canceled which might be caused by
* a new value is set or camera is closed.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index efbb3c3..91b2f00 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -23,6 +23,7 @@
import android.view.Surface;
import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.StringDef;
@@ -425,6 +426,29 @@
return Collections.emptySet();
}
+ /**
+ * Returns the maximum torch strength level.
+ *
+ * @return The maximum strength level. If the device doesn't support configuring torch
+ * strength, returns {@code 1}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @IntRange(from = 1)
+ default int getMaxTorchStrengthLevel() {
+ return 1;
+ }
+
+ /**
+ * Returns the {@link LiveData} of the torch strength level.
+ *
+ * <p>The value of the {@link LiveData} will be the default torch strength level of this
+ * device if {@link CameraControl#setTorchStrengthLevelAsync(int)} hasn't been called.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default @NonNull LiveData<Integer> getTorchStrengthLevel() {
+ return new MutableLiveData<>(1);
+ }
+
@StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
IMPLEMENTATION_TYPE_CAMERA2_LEGACY, IMPLEMENTATION_TYPE_CAMERA2,
IMPLEMENTATION_TYPE_FAKE})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java b/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
index c7da97c..23f50af 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
@@ -32,8 +32,7 @@
private TorchState() {
}
- /**
- */
+ /** The camera flash torch state. */
@IntDef({OFF, ON})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
index cf17fc7..b39ebca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
@@ -18,6 +18,7 @@
import android.graphics.Rect;
+import androidx.annotation.IntRange;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
@@ -83,6 +84,12 @@
}
@Override
+ public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+ @IntRange(from = 1) int torchStrengthLevel) {
+ return mCameraControlInternal.setTorchStrengthLevelAsync(torchStrengthLevel);
+ }
+
+ @Override
@ImageCapture.FlashMode
public int getFlashMode() {
return mCameraControlInternal.getFlashMode();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index fd752fd..10d896a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -73,6 +73,16 @@
}
@Override
+ public @NonNull LiveData<Integer> getTorchStrengthLevel() {
+ return mCameraInfoInternal.getTorchStrengthLevel();
+ }
+
+ @Override
+ public int getMaxTorchStrengthLevel() {
+ return mCameraInfoInternal.getMaxTorchStrengthLevel();
+ }
+
+ @Override
public boolean isLowLightBoostSupported() {
return mCameraInfoInternal.isLowLightBoostSupported();
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index a93558e..befe1c0 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -342,6 +342,7 @@
videoCapture.setViewPortCropRect(cropRect)
checkAndBindUseCases(preview, videoCapture)
+ val calculatedCropRect = videoCapture.cropRect!!
// TODO(b/264936115): In stream sharing (VirtualCameraAdapter), children's ViewPortCropRect
// is ignored and override to the parent size, the cropRect is also rotated. Skip the test
@@ -352,7 +353,7 @@
val result = recordingSession.createRecording(recorder = recorder).recordAndVerify()
// Verify.
- val resolution = rectToSize(videoCapture.cropRect!!)
+ val resolution = rectToSize(calculatedCropRect)
verifyVideoResolution(
context,
result.file,
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt
new file mode 100644
index 0000000..a83099b
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Preview
+@Composable
+fun EllipsizeRedrawDemo() {
+ val transition = rememberInfiniteTransition("padding")
+ val padding = transition.animateFloat(0f, 50f, infiniteRepeatable(tween(10000)))
+ Box(
+ modifier =
+ Modifier.padding(top = padding.value.dp)
+ .fillMaxSize()
+ .border(1.dp, Color.Blue)
+ .padding(24.dp)
+ .border(1.dp, Color.Cyan),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ BasicText("If b/389707025 fixed, the following should display with no animation")
+
+ // reset everything at 49
+ key(padding.value.toInt() / 49) {
+ BasicText(
+ text = "I will definitely fill the screen!".repeat(10),
+ modifier = Modifier.border(1.dp, Color.Red),
+ style =
+ TextStyle(
+ textAlign = TextAlign.Center,
+ letterSpacing = 1.sp,
+ lineHeight = 24.sp,
+ lineHeightStyle =
+ LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Center,
+ trim = LineHeightStyle.Trim.None,
+ mode = LineHeightStyle.Mode.Fixed
+ ),
+ ),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ color = { Color.Black }
+ )
+ BasicText(
+ text = "I will definitely fill the screen!".repeat(10),
+ modifier = Modifier.border(1.dp, Color.Red),
+ style =
+ TextStyle(
+ textAlign = TextAlign.Center,
+ letterSpacing = 1.sp,
+ lineHeight = 24.sp,
+ lineHeightStyle =
+ LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Center,
+ trim = LineHeightStyle.Trim.None,
+ mode = LineHeightStyle.Mode.Fixed
+ ),
+ ),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ color = { Color.Black }
+ )
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index fe61539..22d952e2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -92,6 +92,9 @@
ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
ComposableDemo("Get last character after clip") {
LastClippedCharacterDemo()
+ },
+ ComposableDemo("Ellipses plays well with redraw") {
+ EllipsizeRedrawDemo()
}
)
),
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
index 855071a..bdc4452 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
@@ -18,8 +18,17 @@
import android.os.Build
import androidx.compose.foundation.GOLDEN_FOUNDATION
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -30,6 +39,10 @@
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -50,6 +63,50 @@
private val textTag = "text"
@Test
+ fun basicTextEllipsisCentered_leadingMarginCorrect_doesntMarch_b389707025() {
+ val padding = mutableStateOf(0.dp)
+ rule.setContent {
+ Box(
+ modifier =
+ Modifier.padding(top = padding.value).fillMaxSize().border(1.dp, Color.Blue),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ BasicText(
+ text = "I will definitely fill the screen!".repeat(10),
+ modifier = Modifier.border(1.dp, Color.Red).testTag(textTag),
+ style =
+ TextStyle(
+ textAlign = TextAlign.Center,
+ letterSpacing = 1.sp,
+ lineHeight = 24.sp,
+ lineHeightStyle =
+ LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Center,
+ trim = LineHeightStyle.Trim.None,
+ mode = LineHeightStyle.Mode.Fixed
+ ),
+ ),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ color = { Color.Black }
+ )
+ }
+ }
+ }
+
+ repeat(2) {
+ // these repaints cause the reproduction of b/389707025
+ rule.waitForIdle()
+ padding.value = padding.value + 1.dp
+ }
+ rule
+ .onNodeWithTag(textTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "leadingMarginForEllipsis")
+ }
+
+ @Test
fun multiStyleText_setFontWeight() {
rule.setContent {
BasicText(
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index ee62807..a3501ba 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1036,10 +1036,10 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingToolbarState FloatingToolbarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingToolbarState rememberFloatingToolbarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index ee62807..a3501ba 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1036,10 +1036,10 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingToolbarState FloatingToolbarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingToolbarState rememberFloatingToolbarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index c025935..dce4bf4 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -111,6 +111,7 @@
implementation("androidx.compose.material:material-icons-core:1.7.5")
implementation(project(":test:screenshot:screenshot"))
implementation(project(":core:core"))
+ implementation("androidx.compose.ui:ui-tooling:1.4.1")
implementation(libs.espressoCore)
implementation(libs.testRules)
implementation(libs.testRunner)
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 be06c8a..6d3b4e5 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
@@ -40,6 +40,8 @@
import androidx.compose.material3.samples.ButtonSample
import androidx.compose.material3.samples.ButtonWithIconSample
import androidx.compose.material3.samples.CardSample
+import androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
+import androidx.compose.material3.samples.CenteredVerticalFloatingToolbarWithFabSample
import androidx.compose.material3.samples.CheckboxSample
import androidx.compose.material3.samples.CheckboxWithTextSample
import androidx.compose.material3.samples.ChipGroupReflowSample
@@ -888,11 +890,11 @@
HorizontalFloatingToolbarWithFabSample()
},
Example(
- name = "VerticalFloatingToolbarWithFabSample",
+ name = "CenteredHorizontalFloatingToolbarWithFabSample",
description = FloatingToolbarsExampleDescription,
sourceUrl = FloatingToolbarsExampleSourceUrl,
) {
- VerticalFloatingToolbarWithFabSample()
+ CenteredHorizontalFloatingToolbarWithFabSample()
},
Example(
name = "HorizontalFloatingToolbarAsScaffoldFabSample",
@@ -901,6 +903,20 @@
) {
HorizontalFloatingToolbarAsScaffoldFabSample()
},
+ Example(
+ name = "VerticalFloatingToolbarWithFabSample",
+ description = FloatingToolbarsExampleDescription,
+ sourceUrl = FloatingToolbarsExampleSourceUrl,
+ ) {
+ VerticalFloatingToolbarWithFabSample()
+ },
+ Example(
+ name = "CenteredVerticalFloatingToolbarWithFabSample",
+ description = FloatingToolbarsExampleDescription,
+ sourceUrl = FloatingToolbarsExampleSourceUrl,
+ ) {
+ CenteredVerticalFloatingToolbarWithFabSample()
+ },
)
private const val ExtendedFABExampleDescription = "Extended FAB examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
index ed11f6b..ccaa941 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
@@ -61,8 +61,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment.Companion.BottomCenter
-import androidx.compose.ui.Alignment.Companion.CenterEnd
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -107,7 +106,7 @@
}
}
HorizontalFloatingToolbar(
- modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+ modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
expanded = expanded || isTouchExplorationEnabled,
leadingContent = { leadingContent() },
trailingContent = { trailingContent() },
@@ -157,7 +156,7 @@
}
}
HorizontalFloatingToolbar(
- modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+ modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
expanded = true,
leadingContent = { leadingContent() },
trailingContent = { trailingContent() },
@@ -214,7 +213,7 @@
}
}
VerticalFloatingToolbar(
- modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+ modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
expanded = expanded || isTouchExplorationEnabled,
leadingContent = { leadingContent() },
trailingContent = { trailingContent() },
@@ -264,7 +263,7 @@
}
}
VerticalFloatingToolbar(
- modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+ modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
expanded = true,
leadingContent = { leadingContent() },
trailingContent = { trailingContent() },
@@ -330,7 +329,9 @@
Icon(Icons.Filled.Add, "Localized description")
}
},
- modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+ modifier =
+ Modifier.align(Alignment.BottomEnd)
+ .offset(x = -ScreenOffset, y = -ScreenOffset),
colors = vibrantColors,
content = {
IconButton(onClick = { /* doSomething() */ }) {
@@ -355,6 +356,62 @@
@Preview
@Sampled
@Composable
+fun CenteredHorizontalFloatingToolbarWithFabSample() {
+ val context = LocalContext.current
+ val isTouchExplorationEnabled = remember {
+ val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+ am.isEnabled && am.isTouchExplorationEnabled
+ }
+ val exitAlwaysScrollBehavior =
+ FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
+ val vibrantColors = FloatingToolbarDefaults.vibrantFloatingToolbarColors()
+ Scaffold(modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior)) { innerPadding ->
+ Box(Modifier.padding(innerPadding)) {
+ Column(
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(text = remember { LoremIpsum().values.first() })
+ }
+ HorizontalFloatingToolbar(
+ // Always expanded as the toolbar is bottom-centered. We will use a
+ // FloatingToolbarScrollBehavior to hide both the toolbar and its FAB on scroll.
+ expanded = true,
+ floatingActionButton = {
+ // Match the FAB to the vibrantColors. See also StandardFloatingActionButton.
+ FloatingToolbarDefaults.VibrantFloatingActionButton(
+ onClick = { /* doSomething() */ },
+ ) {
+ Icon(Icons.Filled.Add, "Localized description")
+ }
+ },
+ modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
+ colors = vibrantColors,
+ scrollBehavior = if (!isTouchExplorationEnabled) exitAlwaysScrollBehavior else null,
+ content = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Person, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.MoreVert, contentDescription = "Localized description")
+ }
+ },
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
fun VerticalFloatingToolbarWithFabSample() {
val context = LocalContext.current
val isTouchExplorationEnabled = remember {
@@ -396,7 +453,9 @@
Icon(Icons.Filled.Add, "Localized description")
}
},
- modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+ modifier =
+ Modifier.align(Alignment.BottomEnd)
+ .offset(x = -ScreenOffset, y = -ScreenOffset),
colors = vibrantColors,
content = {
IconButton(onClick = { /* doSomething() */ }) {
@@ -421,6 +480,62 @@
@Preview
@Sampled
@Composable
+fun CenteredVerticalFloatingToolbarWithFabSample() {
+ val context = LocalContext.current
+ val isTouchExplorationEnabled = remember {
+ val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+ am.isEnabled && am.isTouchExplorationEnabled
+ }
+ val exitAlwaysScrollBehavior =
+ FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+ val vibrantColors = FloatingToolbarDefaults.vibrantFloatingToolbarColors()
+ Scaffold(modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior)) { innerPadding ->
+ Box(Modifier.padding(innerPadding)) {
+ Column(
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(text = remember { LoremIpsum().values.first() })
+ }
+ VerticalFloatingToolbar(
+ // Always expanded as the toolbar is right-centered. We will use a
+ // FloatingToolbarScrollBehavior to hide both the toolbar and its FAB on scroll.
+ expanded = true,
+ floatingActionButton = {
+ // Match the FAB to the vibrantColors. See also StandardFloatingActionButton.
+ FloatingToolbarDefaults.VibrantFloatingActionButton(
+ onClick = { /* doSomething() */ },
+ ) {
+ Icon(Icons.Filled.Add, "Localized description")
+ }
+ },
+ modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
+ colors = vibrantColors,
+ scrollBehavior = if (!isTouchExplorationEnabled) exitAlwaysScrollBehavior else null,
+ content = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Person, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.MoreVert, contentDescription = "Localized description")
+ }
+ },
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
fun HorizontalFloatingToolbarAsScaffoldFabSample() {
val context = LocalContext.current
val isTouchExplorationEnabled = remember {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
index b9b0ac3..de628ad 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
@@ -26,25 +26,32 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertCountEquals
@@ -64,6 +71,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -92,7 +100,7 @@
val scrollHeightOffsetDp = 20.dp
var scrollHeightOffsetPx = 0f
var containerSizePx = 0f
- val screenOffsetDp = FloatingToolbarDefaults.ScreenOffset
+ val screenOffsetDp = ScreenOffset
var screenOffsetPx = 0f
rule.setMaterialContent(lightColorScheme()) {
@@ -143,7 +151,7 @@
val scrollHeightOffsetDp = 20.dp
var scrollHeightOffsetPx = 0f
var containerSizePx = 0f
- val screenOffsetDp = FloatingToolbarDefaults.ScreenOffset
+ val screenOffsetDp = ScreenOffset
var screenOffsetPx = 0f
rule.setMaterialContent(lightColorScheme()) {
@@ -918,6 +926,76 @@
rule.runOnIdle { assertThat(expanded).isEqualTo(false) }
}
+ @Test
+ fun verticalFloatingToolbar_scrollBehavior() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scrollBehavior =
+ FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior).testTag(mainLayoutTag)) {
+ innerPadding ->
+ Box(Modifier.padding(innerPadding)) {
+ Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+ Text(text = remember { LoremIpsum().values.first() })
+ }
+ VerticalFloatingToolbar(
+ expanded = true,
+ floatingActionButton = { ToolbarFab() },
+ modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
+ scrollBehavior = scrollBehavior,
+ ) {
+ ToolbarContent()
+ }
+ }
+ }
+ }
+
+ // Check that the FAB and a sample from the toolbar content are displayed.
+ rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsDisplayed()
+ rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsDisplayed()
+
+ // Swipe the content up to collapse the FloatingToolbar.
+ rule.onNodeWithTag(mainLayoutTag).performTouchInput { swipeUp(bottom, bottom - 1000) }
+ rule.waitForIdle()
+ // Check that the FAB and a sample from the toolbar content are not displayed.
+ rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsNotDisplayed()
+ rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsNotDisplayed()
+ }
+
+ @Test
+ fun horizontalFloatingToolbar_scrollBehavior() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scrollBehavior =
+ FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+ Scaffold(modifier = Modifier.nestedScroll(scrollBehavior).testTag(mainLayoutTag)) {
+ innerPadding ->
+ Box(Modifier.padding(innerPadding)) {
+ Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+ Text(text = remember { LoremIpsum().values.first() })
+ }
+ HorizontalFloatingToolbar(
+ expanded = true,
+ floatingActionButton = { ToolbarFab() },
+ modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
+ scrollBehavior = scrollBehavior,
+ ) {
+ ToolbarContent()
+ }
+ }
+ }
+ }
+
+ // Check that the FAB and a sample from the toolbar content are displayed.
+ rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsDisplayed()
+ rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsDisplayed()
+
+ // Swipe the content up to collapse the FloatingToolbar.
+ rule.onNodeWithTag(mainLayoutTag).performTouchInput { swipeUp(bottom, bottom - 1000) }
+ rule.waitForIdle()
+ // Check that the FAB and a sample from the toolbar content are not displayed.
+ rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsNotDisplayed()
+ rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsNotDisplayed()
+ }
+
@Composable
private fun VerticalNestedScrollTestContent(
onExpanded: () -> Unit,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
index e0e61a2..6ac3dec 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
@@ -51,12 +51,15 @@
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.FloatingToolbarDefaults.horizontalEnterTransition
import androidx.compose.material3.FloatingToolbarDefaults.horizontalExitTransition
+import androidx.compose.material3.FloatingToolbarDefaults.standardFloatingToolbarColors
import androidx.compose.material3.FloatingToolbarDefaults.verticalEnterTransition
import androidx.compose.material3.FloatingToolbarDefaults.verticalExitTransition
+import androidx.compose.material3.FloatingToolbarDefaults.vibrantFloatingToolbarColors
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Start
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Top
+import androidx.compose.material3.FloatingToolbarState.Companion.Saver
import androidx.compose.material3.tokens.ColorSchemeKeyTokens
import androidx.compose.material3.tokens.ElevationTokens
import androidx.compose.material3.tokens.FabBaselineTokens
@@ -262,11 +265,22 @@
* other content, and even in a `Scaffold`'s floating action button slot. Its [expanded] flag
* controls the visibility of the actions with a slide animations.
*
- * Note that if your app uses a `Snackbar`, it's best to position the toolbar in a `Scaffold`'s FAB
- * slot. This ensures the `Snackbar` appears above the toolbar, preventing any visual overlap or
- * interference.
+ * In case the toolbar is aligned to the right or the left of the screen, you may apply a
+ * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
+ * state when scrolling occurs, as this sample shows:
*
* @sample androidx.compose.material3.samples.HorizontalFloatingToolbarWithFabSample
+ *
+ * In case the toolbar is positioned along a center edge of the screen (like top or bottom center),
+ * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
+ * order to hide or show the entire component, as this sample shows:
+ *
+ * @sample androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
+ *
+ * Note that if your app uses a `Snackbar`, it's best to position the toolbar in a `Scaffold`'s FAB
+ * slot. This ensures the `Snackbar` appears above the toolbar, preventing any visual overlap or
+ * interference. See this sample:
+ *
* @sample androidx.compose.material3.samples.HorizontalFloatingToolbarAsScaffoldFabSample
* @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
* and the toolbar content are organized horizontally. Otherwise, only the FAB is visible.
@@ -279,6 +293,12 @@
* [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
* [floatingActionButton] for more information on the right FAB to use for proper styling.
* @param contentPadding the padding applied to the content of this floating toolbar.
+ * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
+ * automatically react to scrolling. If your toolbar is positioned along a center edge of the
+ * screen (like top or bottom center), it's best to use this scroll behavior to make the entire
+ * toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
+ * off-center, which may occur in this case when using the [expanded] flag to simply expand or
+ * collapse the toolbar.
* @param shape the shape used for this floating toolbar content.
* @param floatingActionButtonPosition the position of the floating toolbar's floating action
* button. By default, the FAB is placed at the end of the toolbar (i.e. aligned to the right in
@@ -296,6 +316,7 @@
modifier: Modifier = Modifier,
colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
+ scrollBehavior: FloatingToolbarScrollBehavior? = null,
shape: Shape = FloatingToolbarDefaults.ContainerShape,
floatingActionButtonPosition: FloatingToolbarHorizontalFabPosition =
FloatingToolbarHorizontalFabPosition.End,
@@ -308,6 +329,7 @@
colors = colors,
toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
toolbarContentPadding = contentPadding,
+ scrollBehavior = scrollBehavior,
toolbarShape = shape,
animationSpec = animationSpec,
fab = floatingActionButton,
@@ -469,7 +491,17 @@
* other content, and its [expanded] flag controls the visibility of the actions with a slide
* animations.
*
+ * In case the toolbar is aligned to the top or the bottom of the screen, you may apply a
+ * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
+ * state when scrolling occurs, as this sample shows:
+ *
* @sample androidx.compose.material3.samples.VerticalFloatingToolbarWithFabSample
+ *
+ * In case the toolbar is positioned along a center edge of the screen (like left or right center),
+ * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
+ * order to hide or show the entire component, as this sample shows:
+ *
+ * @sample androidx.compose.material3.samples.CenteredVerticalFloatingToolbarWithFabSample
* @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
* and the toolbar content are organized vertically. Otherwise, only the FAB is visible.
* @param floatingActionButton a floating action button to be displayed by the toolbar. It's
@@ -481,6 +513,12 @@
* [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
* [floatingActionButton] for more information on the right FAB to use for proper styling.
* @param contentPadding the padding applied to the content of this floating toolbar.
+ * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
+ * automatically react to scrolling. If your toolbar is positioned along a center edge of the
+ * screen (like left or right center), it's best to use this scroll behavior to make the entire
+ * toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
+ * off-center, which may occur in this case when using the [expanded] flag to simply expand or
+ * collapse the toolbar.
* @param shape the shape used for this floating toolbar content.
* @param floatingActionButtonPosition the position of the floating toolbar's floating action
* button. By default, the FAB is placed at the bottom of the toolbar (i.e. aligned to the
@@ -498,6 +536,7 @@
modifier: Modifier = Modifier,
colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
+ scrollBehavior: FloatingToolbarScrollBehavior? = null,
shape: Shape = FloatingToolbarDefaults.ContainerShape,
floatingActionButtonPosition: FloatingToolbarVerticalFabPosition =
FloatingToolbarVerticalFabPosition.Bottom,
@@ -510,6 +549,7 @@
colors = colors,
toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
toolbarContentPadding = contentPadding,
+ scrollBehavior = scrollBehavior,
toolbarShape = shape,
animationSpec = animationSpec,
fab = floatingActionButton,
@@ -1489,6 +1529,7 @@
colors: FloatingToolbarColors,
toolbarToFabGap: Dp,
toolbarContentPadding: PaddingValues,
+ scrollBehavior: FloatingToolbarScrollBehavior?,
toolbarShape: Shape,
animationSpec: FiniteAnimationSpec<Float>,
fab: @Composable () -> Unit,
@@ -1514,7 +1555,12 @@
fab()
},
modifier =
- modifier.defaultMinSize(minHeight = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+ modifier
+ .defaultMinSize(minHeight = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+ .then(
+ scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
+ ?: Modifier
+ )
) { measurables, constraints ->
val toolbarMeasurable = measurables[0]
val fabMeasurable = measurables[1]
@@ -1596,6 +1642,7 @@
colors: FloatingToolbarColors,
toolbarToFabGap: Dp,
toolbarContentPadding: PaddingValues,
+ scrollBehavior: FloatingToolbarScrollBehavior?,
toolbarShape: Shape,
animationSpec: FiniteAnimationSpec<Float>,
fab: @Composable () -> Unit,
@@ -1621,7 +1668,12 @@
fab()
},
modifier =
- modifier.defaultMinSize(minWidth = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+ modifier
+ .defaultMinSize(minWidth = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+ .then(
+ scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
+ ?: Modifier
+ )
) { measurables, constraints ->
val toolbarMeasurable = measurables[0]
val fabMeasurable = measurables[1]
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index 5226452..f11dc72 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -190,8 +190,7 @@
import kotlinx.coroutines.launch
/**
- * <a href="https://m3.material.io/components/time-pickers/overview" class="external"
- * target="_blank">Material Design time picker</a>.
+ * [Material Design time picker](https://m3.material.io/components/time-pickers/overview)
*
* Time pickers help users select and set a specific time.
*
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
index a851fb5..7c54156 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
@@ -17,7 +17,12 @@
package androidx.compose.ui.text
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.style.TextOverflow
@@ -30,6 +35,7 @@
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -76,6 +82,49 @@
}
@Test
+ fun drawParagraphIndentsCorrectly_whenPaintedRepeatedly() {
+ val width1 = charWidth * 3
+ val subject =
+ Paragraph(
+ text = ltrChar.repeat(repeatCount),
+ style =
+ TextStyle(
+ textAlign = TextAlign.Center,
+ letterSpacing = 1.sp,
+ lineHeight = 24.sp,
+ lineHeightStyle =
+ LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Center,
+ trim = LineHeightStyle.Trim.None,
+ mode = LineHeightStyle.Mode.Fixed
+ ),
+ ),
+ maxLines = lastLine + 1,
+ overflow = TextOverflow.Ellipsis,
+ constraints = Constraints(maxWidth = width1),
+ density = Density(density = 1f),
+ fontFamilyResolver = UncachedFontFamilyResolver(getInstrumentation().context)
+ )
+
+ val width = subject.width.ceilToInt()
+ val height = subject.height.ceilToInt()
+ val bitmap = ImageBitmap(width, height)
+ val canvas = Canvas(bitmap)
+ val paint = Paint()
+ paint.color = Color.Black
+ canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+
+ subject.paint(canvas, Color.Blue)
+ val initialPixels = bitmap.dumpFirstCharPixels()
+ canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+
+ subject.paint(canvas, Color.Blue)
+ val finalPixels = bitmap.dumpFirstCharPixels()
+
+ assertThat(initialPixels).isEqualTo(finalPixels)
+ }
+
+ @Test
fun getLineLeftAndGetLineRight_Rtl() {
val paragraph = paragraph(rtlChar.repeat(repeatCount))
for (line in 0 until paragraph.lineCount) {
@@ -353,4 +402,10 @@
UncachedFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
)
}
+
+ private fun ImageBitmap.dumpFirstCharPixels(): IntArray {
+ val out = IntArray(charWidth * charWidth)
+ readPixels(out, width = charWidth, height = charWidth)
+ return out
+ }
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
index ba2d38b..13a0595 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
@@ -113,7 +113,7 @@
spanStyles,
listOf(),
fontFamilyResolver,
- Density(1f)
+ Density(1f),
)
}
}
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index 49d12ab..f1880be 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -67,6 +67,7 @@
import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_CHARACTER
import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_WORD
import androidx.compose.ui.text.android.TextLayout
+import androidx.compose.ui.text.android.hasSpan
import androidx.compose.ui.text.android.selection.getWordEnd
import androidx.compose.ui.text.android.selection.getWordStart
import androidx.compose.ui.text.android.style.IndentationFixSpan
@@ -689,11 +690,13 @@
textAlign != TextAlign.Justify)
}
-@OptIn(InternalPlatformTextApi::class)
+// this _will_ be called multiple times on the same ParagraphIntrinsics
private fun CharSequence.attachIndentationFixSpan(): CharSequence {
if (isEmpty()) return this
- val spannable = if (this is Spannable) this else SpannableString(this)
- spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
+ val spannable = this as? Spannable ?: SpannableString(this)
+ if (!spannable.hasSpan(IndentationFixSpan::class.java)) {
+ spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
+ }
return spannable
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt
new file mode 100644
index 0000000..6dc2a1c
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.benchmark.autofill
+
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.os.LocaleList
+import android.os.Parcel
+import android.view.View
+import android.view.ViewStructure
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+
+/**
+ * A fake implementation of [ViewStructure] to use in tests.
+ *
+ * @param virtualId An ID that is unique for each viewStructure node in the viewStructure tree.
+ * @param packageName The package name of the app (Used as an autofill heuristic).
+ * @param typeName The type name of the view's identifier, or null if there is none.
+ * @param entryName The entry name of the view's identifier, or null if there is none.
+ * @param children A list of [ViewStructure]s that are children of the current [ViewStructure].
+ * @param bounds The bounds (Dimensions) of the component represented by this [ViewStructure].
+ * @param autofillId The [autofillId] for the parent component. The same autofillId is used for
+ * other child components.
+ * @param autofillType The data type. Can be one of the following: [View.AUTOFILL_TYPE_DATE],
+ * [View.AUTOFILL_TYPE_LIST], [View.AUTOFILL_TYPE_TEXT], [View.AUTOFILL_TYPE_TOGGLE] or
+ * [View.AUTOFILL_TYPE_NONE].
+ * @param autofillHints The autofill hint. If this value not specified, we use heuristics to
+ * determine what data to use while performing autofill.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal data class FakeViewStructure(
+ var virtualId: Int = 0,
+ var packageName: String? = null,
+ var typeName: String? = null,
+ var entryName: String? = null,
+ var children: MutableList<FakeViewStructure> = mutableListOf(),
+ var bounds: Rect? = null,
+ private val autofillId: AutofillId? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) generateAutofillId() else null,
+ internal var autofillType: Int = View.AUTOFILL_TYPE_NONE,
+ internal var autofillHints: Array<out String> = arrayOf()
+) : ViewStructure() {
+
+ private var activated: Boolean = false
+ private var alpha: Float = 1f
+ private var autofillOptions: Array<CharSequence>? = null
+ private var autofillValue: AutofillValue? = null
+ private var className: String? = null
+ private var contentDescription: CharSequence? = null
+ private var dataIsSensitive: Boolean = false
+ private var elevation: Float = 0f
+ private var extras: Bundle = Bundle()
+ private var hint: CharSequence? = null
+ private var htmlInfo: HtmlInfo? = null
+ private var inputType: Int = 0
+ private var isEnabled: Boolean = true
+ private var isAccessibilityFocused: Boolean = false
+ private var isCheckable: Boolean = false
+ private var isChecked: Boolean = false
+ private var isClickable: Boolean = true
+ private var isContextClickable: Boolean = false
+ private var isFocused: Boolean = false
+ private var isFocusable: Boolean = false
+ private var isLongClickable: Boolean = false
+ private var isOpaque: Boolean = false
+ private var selected: Boolean = false
+ private var text: CharSequence = ""
+ private var textLines: IntArray? = null
+ private var transformation: Matrix? = null
+ private var visibility: Int = View.VISIBLE
+ private var webDomain: String? = null
+
+ internal companion object {
+ @GuardedBy("this") private var previousId = 0
+ private val NO_SESSION = 0
+
+ // Android API level 26 introduced Autofill. Prior to API level 26, no autofill ID will be
+ // provided.
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Synchronized
+ private fun generateAutofillId(): AutofillId {
+ var autofillId: AutofillId? = null
+ useParcel { parcel ->
+ parcel.writeInt(++previousId) // View Id.
+ parcel.writeInt(NO_SESSION) // Flag.
+ parcel.setDataPosition(0)
+ autofillId = AutofillId.CREATOR.createFromParcel(parcel)
+ }
+ return autofillId ?: error("Could not generate autofill id")
+ }
+ }
+
+ override fun getChildCount() = children.count()
+
+ override fun addChildCount(childCount: Int): Int {
+ repeat(childCount) { children.add(FakeViewStructure(autofillId = autofillId)) }
+ return children.count() - childCount
+ }
+
+ override fun newChild(index: Int): FakeViewStructure {
+ if (index >= children.count()) error("Call addChildCount() before calling newChild()")
+ return children[index]
+ }
+
+ override fun getAutofillId() = autofillId
+
+ override fun setAutofillId(rootId: AutofillId, virtualId: Int) {
+ this.virtualId = virtualId
+ }
+
+ override fun setId(
+ virtualId: Int,
+ packageName: String?,
+ typeName: String?,
+ entryName: String?
+ ) {
+ this.virtualId = virtualId
+ this.packageName = packageName
+ this.typeName = typeName
+ this.entryName = entryName
+ }
+
+ override fun setAutofillType(autofillType: Int) {
+ this.autofillType = autofillType
+ }
+
+ override fun setAutofillHints(autofillHints: Array<out String>?) {
+ autofillHints?.let { this.autofillHints = it }
+ }
+
+ override fun setDimens(left: Int, top: Int, x: Int, y: Int, width: Int, height: Int) {
+ this.bounds = Rect(left, top, width - left, height - top)
+ }
+
+ override fun equals(other: Any?) =
+ other is FakeViewStructure &&
+ other.virtualId == virtualId &&
+ other.packageName == packageName &&
+ other.typeName == typeName &&
+ other.entryName == entryName &&
+ other.autofillType == autofillType &&
+ other.autofillHints.contentEquals(autofillHints) &&
+ other.bounds.contentEquals(bounds) &&
+ other.activated == activated &&
+ other.alpha == alpha &&
+ other.autofillOptions.contentEquals(autofillOptions) &&
+ other.autofillValue == autofillValue &&
+ other.className == className &&
+ other.children.count() == children.count() &&
+ other.contentDescription == contentDescription &&
+ other.dataIsSensitive == dataIsSensitive &&
+ other.elevation == elevation &&
+ other.hint == hint &&
+ other.htmlInfo == htmlInfo &&
+ other.inputType == inputType &&
+ other.isEnabled == isEnabled &&
+ other.isCheckable == isCheckable &&
+ other.isChecked == isChecked &&
+ other.isClickable == isClickable &&
+ other.isContextClickable == isContextClickable &&
+ other.isAccessibilityFocused == isAccessibilityFocused &&
+ other.isFocused == isFocused &&
+ other.isLongClickable == isLongClickable &&
+ other.isOpaque == isOpaque &&
+ other.isFocusable == isFocusable &&
+ other.selected == selected &&
+ other.text == text &&
+ other.textLines.contentEquals(textLines) &&
+ other.transformation == transformation &&
+ other.visibility == visibility &&
+ other.webDomain == webDomain
+
+ override fun hashCode() = super.hashCode()
+
+ override fun getExtras() = extras
+
+ override fun getHint() = hint ?: ""
+
+ override fun getText() = text
+
+ override fun hasExtras() = !extras.isEmpty
+
+ override fun setActivated(p0: Boolean) {
+ activated = p0
+ }
+
+ override fun setAccessibilityFocused(p0: Boolean) {
+ isAccessibilityFocused = p0
+ }
+
+ override fun setAlpha(p0: Float) {
+ alpha = p0
+ }
+
+ override fun setAutofillOptions(p0: Array<CharSequence>?) {
+ autofillOptions = p0
+ }
+
+ override fun setAutofillValue(p0: AutofillValue?) {
+ autofillValue = p0
+ }
+
+ override fun setCheckable(p0: Boolean) {
+ isCheckable = p0
+ }
+
+ override fun setChecked(p0: Boolean) {
+ isChecked = p0
+ }
+
+ override fun setClassName(p0: String?) {
+ className = p0
+ }
+
+ override fun setClickable(p0: Boolean) {
+ isClickable = p0
+ }
+
+ override fun setContentDescription(p0: CharSequence?) {
+ contentDescription = p0
+ }
+
+ override fun setContextClickable(p0: Boolean) {
+ isContextClickable = p0
+ }
+
+ override fun setDataIsSensitive(p0: Boolean) {
+ dataIsSensitive = p0
+ }
+
+ override fun setElevation(p0: Float) {
+ elevation = p0
+ }
+
+ override fun setEnabled(p0: Boolean) {
+ isEnabled = p0
+ }
+
+ override fun setFocusable(p0: Boolean) {
+ isFocusable = p0
+ }
+
+ override fun setFocused(p0: Boolean) {
+ isFocused = p0
+ }
+
+ override fun setHtmlInfo(p0: HtmlInfo) {
+ htmlInfo = p0
+ }
+
+ override fun setHint(p0: CharSequence?) {
+ hint = p0
+ }
+
+ override fun setInputType(p0: Int) {
+ inputType = p0
+ }
+
+ override fun setLongClickable(p0: Boolean) {
+ isLongClickable = p0
+ }
+
+ override fun setOpaque(p0: Boolean) {
+ isOpaque = p0
+ }
+
+ override fun setSelected(p0: Boolean) {
+ selected = p0
+ }
+
+ override fun setText(p0: CharSequence?) {
+ p0?.let { text = it }
+ }
+
+ override fun setText(p0: CharSequence?, p1: Int, p2: Int) {
+ p0?.let { text = it.subSequence(p1, p2) }
+ }
+
+ override fun setTextLines(p0: IntArray?, p1: IntArray?) {
+ textLines = p0
+ }
+
+ override fun setTransformation(p0: Matrix?) {
+ transformation = p0
+ }
+
+ override fun setVisibility(p0: Int) {
+ visibility = p0
+ }
+
+ override fun setWebDomain(p0: String?) {
+ webDomain = p0
+ }
+
+ // Unimplemented methods.
+ override fun asyncCommit() {
+ TODO("not implemented")
+ }
+
+ override fun asyncNewChild(p0: Int): ViewStructure {
+ TODO("not implemented")
+ }
+
+ override fun getTextSelectionEnd(): Int {
+ TODO("not implemented")
+ }
+
+ override fun getTextSelectionStart(): Int {
+ TODO("not implemented")
+ }
+
+ override fun newHtmlInfoBuilder(p0: String): HtmlInfo.Builder {
+ TODO("not implemented")
+ }
+
+ override fun setAutofillId(p0: AutofillId) {
+ TODO("not implemented")
+ }
+
+ override fun setChildCount(p0: Int) {
+ TODO("not implemented")
+ }
+
+ override fun setLocaleList(p0: LocaleList?) {
+ TODO("not implemented")
+ }
+
+ override fun setTextStyle(p0: Float, p1: Int, p2: Int, p3: Int) {
+ TODO("not implemented")
+ }
+}
+
+private fun Rect?.contentEquals(other: Rect?) =
+ when {
+ (other == null && this == null) -> true
+ (other == null || this == null) -> false
+ else ->
+ other.left == left && other.right == right && other.bottom == bottom && other.top == top
+ }
+
+/** Obtains a parcel and then recycles it correctly whether an exception is thrown or not. */
+private fun useParcel(block: (Parcel) -> Unit) {
+ var parcel: Parcel? = null
+ try {
+ parcel = Parcel.obtain()
+ block(parcel)
+ } finally {
+ parcel?.recycle()
+ }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt
new file mode 100644
index 0000000..56e3231
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.benchmark.autofill
+
+import android.view.View
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.MicrobenchmarkConfig
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.doFramesUntilNoChangesPending
+import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalView
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 26)
+@RunWith(Parameterized::class)
+class PopulateAutofillStructBenchmark(private val isAutofillEnabled: Boolean) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = isSemanticAutofillEnabled
+ private lateinit var ownerView: View
+
+ @OptIn(ExperimentalBenchmarkConfigApi::class)
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule(MicrobenchmarkConfig(traceAppTagEnabled = true))
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "Autofill enabled = {0}")
+ fun data(): Collection<Array<Any>> {
+ val testCases = mutableListOf<Array<Any>>()
+ // Add a `false` parameter here and run locally to compare Autofill off vs on
+ for (isAutofillEnabled in listOf(true)) {
+ testCases.add(arrayOf(isAutofillEnabled))
+ }
+ return testCases
+ }
+ }
+
+ @Test
+ fun populateViewStructureBenchmark_textScreen() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ isSemanticAutofillEnabled = isAutofillEnabled
+ measurePopulateViewStructureRepeatedOnUiThread { AutofillTextScreen() }
+ @OptIn(ExperimentalComposeUiApi::class)
+ isSemanticAutofillEnabled = previousFlagValue
+ }
+
+ @Test
+ fun populateViewStructureBenchmark_autofillScreen() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ isSemanticAutofillEnabled = isAutofillEnabled
+ measurePopulateViewStructureRepeatedOnUiThread { AutofillScreen() }
+ @OptIn(ExperimentalComposeUiApi::class)
+ isSemanticAutofillEnabled = previousFlagValue
+ }
+
+ private fun measurePopulateViewStructureRepeatedOnUiThread(content: @Composable () -> Unit) {
+ benchmarkRule.runBenchmarkFor(
+ givenTestCase = {
+ object : ComposeTestCase {
+ @Composable
+ override fun Content() {
+ ownerView = LocalView.current
+ content()
+ }
+ }
+ }
+ ) {
+ benchmarkRule.runOnUiThread { doFramesUntilNoChangesPending() }
+ benchmarkRule.measureRepeatedOnUiThread {
+ ownerView.onProvideAutofillVirtualStructure(FakeViewStructure(), 0)
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
index 898c1b2..3ec6174 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
@@ -18,10 +18,18 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Button
+import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentDataType
import androidx.compose.ui.autofill.ContentType
@@ -29,6 +37,7 @@
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
@Composable
internal fun RemovableAutofillText(state: MutableState<Boolean>) {
@@ -180,6 +189,76 @@
}
}
+@Composable
+internal fun AutofillTextScreen() {
+ Column {
+ TextField(
+ value = data.firstName,
+ onValueChange = {},
+ label = { Text("Enter first name here: ") },
+ modifier =
+ Modifier.semantics {
+ contentType = ContentType.PersonFirstName
+ contentDataType = ContentDataType.Text
+ }
+ )
+ TextField(
+ value = data.lastName,
+ onValueChange = {},
+ label = { Text("Enter last name here: ") },
+ modifier =
+ Modifier.semantics {
+ contentType = ContentType.PersonLastName
+ contentDataType = ContentDataType.Text
+ }
+ )
+ TextField(
+ value = data.firstName,
+ onValueChange = {},
+ label = { Text("Enter first name here: ") },
+ modifier =
+ Modifier.semantics {
+ contentType = ContentType.PersonFirstName
+ contentDataType = ContentDataType.Text
+ }
+ )
+ TextField(
+ value = data.lastName,
+ onValueChange = {},
+ label = { Text("Enter last name here: ") },
+ modifier =
+ Modifier.semantics {
+ contentType = ContentType.PersonLastName
+ contentDataType = ContentDataType.Text
+ }
+ )
+ }
+}
+
+@Composable
+internal fun AutofillScreen() {
+ Scaffold(
+ content = { padding ->
+ Column(modifier = Modifier.fillMaxSize().padding(padding)) {
+ // Navigation Button backwards
+ Button(onClick = {}, modifier = Modifier.align(Alignment.Start)) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Go to route.")
+ }
+
+ // Navigation Button forwards
+ Button(onClick = {}, modifier = Modifier.align(Alignment.Start)) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Go to next screen")
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ AutofillTextScreen()
+ }
+ }
+ )
+}
+
internal data class PersonData(
var title: String = "",
var firstName: String = "",
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt
new file mode 100644
index 0000000..e1be2f7
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.compose.ui.focus
+
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.requireSemanticsInfo
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FocusListenerTest {
+ @get:Rule val rule = createComposeRule()
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ // When we clear focus on Pre P devices, request focus is called even when we are
+ // in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ private val initialFocusAfterClearFocus = SDK_INT < Build.VERSION_CODES.P
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
+
+ @Test
+ fun nothingFocused() {
+ // Arrange.
+ val listener = TestFocusListener()
+ rule.setContent(listener) { Box(Modifier.size(10.dp).focusable()) }
+
+ // Assert.
+ rule.runOnIdle { assertThat(listener.events).isEmpty() }
+ }
+
+ @Test
+ fun firstItemFocused() {
+ // Arrange.
+ val listener = TestFocusListener()
+ rule.setContent(listener) { Box(Modifier.size(10.dp).testTag("item").focusable()) }
+ val itemId = rule.onNodeWithTag("item").semanticsId()
+
+ // Act.
+ rule.onNodeWithTag("item").requestFocus()
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(listener).isEqualTo(TestFocusListener(mutableListOf(Pair(null, itemId))))
+ }
+ }
+
+ @Test
+ fun firstItemUnFocused() {
+ // Arrange.
+ val listener = TestFocusListener()
+ lateinit var focusManager: FocusManager
+ rule.setContent(listener) {
+ focusManager = LocalFocusManager.current
+ Box(Modifier.size(10.dp).testTag("item").focusable())
+ }
+ val itemId = rule.onNodeWithTag("item").semanticsId()
+ rule.onNodeWithTag("item").requestFocus()
+ rule.runOnIdle { listener.reset() }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(listener)
+ .isEqualTo(
+ TestFocusListener(
+ if (initialFocusAfterClearFocus) {
+ mutableListOf(Pair(itemId, null), Pair(null, itemId))
+ } else {
+ mutableListOf(Pair(itemId, null))
+ }
+ )
+ )
+ }
+ }
+
+ @Test
+ fun secondItemFocused() {
+ // Arrange.
+ val listener = TestFocusListener()
+ rule.setContent(listener) {
+ Column {
+ Box(Modifier.size(10.dp).testTag("item1").focusable())
+ Box(Modifier.size(10.dp).testTag("item2").focusable())
+ }
+ }
+ val item1Id = rule.onNodeWithTag("item1").semanticsId()
+ val item2Id = rule.onNodeWithTag("item2").semanticsId()
+ rule.onNodeWithTag("item1").requestFocus()
+ rule.runOnIdle { listener.reset() }
+
+ // Act.
+ rule.onNodeWithTag("item2").requestFocus()
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(listener)
+ .isEqualTo(
+ TestFocusListener(mutableListOf(Pair(item1Id, null), Pair(null, item2Id)))
+ )
+ }
+ }
+
+ @Test
+ fun secondItemUnFocused() {
+ // Arrange.
+ val listener = TestFocusListener()
+ lateinit var focusManager: FocusManager
+ rule.setContent(listener) {
+ focusManager = LocalFocusManager.current
+ Column {
+ Box(Modifier.size(10.dp).testTag("item1").focusable())
+ Box(Modifier.size(10.dp).testTag("item2").focusable())
+ }
+ }
+ val item1Id = rule.onNodeWithTag("item1").semanticsId()
+ val item2Id = rule.onNodeWithTag("item2").semanticsId()
+ rule.onNodeWithTag("item1").requestFocus()
+ rule.onNodeWithTag("item2").requestFocus()
+ rule.runOnIdle { listener.reset() }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(listener)
+ .isEqualTo(
+ TestFocusListener(
+ if (initialFocusAfterClearFocus) {
+ mutableListOf(Pair(item2Id, null), Pair(null, item1Id))
+ } else {
+ mutableListOf(Pair(item2Id, null))
+ }
+ )
+ )
+ }
+ }
+
+ private data class TestFocusListener(
+ val events: MutableList<Pair<Int?, Int?>> = mutableListOf<Pair<Int?, Int?>>()
+ ) : FocusListener {
+ override fun onFocusChanged(
+ previous: FocusTargetModifierNode?,
+ current: FocusTargetModifierNode?
+ ) {
+ events +=
+ Pair(
+ previous?.requireSemanticsInfo()?.semanticsId,
+ current?.requireSemanticsInfo()?.semanticsId
+ )
+ }
+
+ fun reset() {
+ events.clear()
+ }
+ }
+
+ private fun ComposeContentTestRule.setContent(
+ focusListener: FocusListener,
+ content: @Composable (() -> Unit)
+ ) {
+ setContent {
+ val focusOwner = LocalFocusManager.current as FocusOwner
+ DisposableEffect(focusOwner, focusListener) {
+ focusOwner.listeners += focusListener
+ onDispose { focusOwner.listeners -= focusListener }
+ }
+ content()
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index f6a524e..69f31cc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -27,7 +27,12 @@
import androidx.annotation.RequiresApi
import androidx.collection.MutableIntSet
import androidx.collection.mutableObjectListOf
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.focus.FocusListener
+import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.node.requireSemanticsInfo
import androidx.compose.ui.platform.coreshims.ViewCompatShims
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsConfiguration
@@ -54,7 +59,7 @@
private val view: View,
private val rectManager: RectManager,
private val packageName: String,
-) : AutofillManager(), SemanticsListener {
+) : AutofillManager(), SemanticsListener, FocusListener {
private var reusableRect = Rect()
private var rootAutofillId: AutofillId
@@ -85,6 +90,26 @@
}
}
+ override fun onFocusChanged(
+ previous: FocusTargetModifierNode?,
+ current: FocusTargetModifierNode?
+ ) {
+ previous?.requireSemanticsInfo()?.let {
+ if (it.semanticsConfiguration?.isAutofillable() == true) {
+ platformAutofillManager.notifyViewExited(view, it.semanticsId)
+ }
+ }
+ current?.requireSemanticsInfo()?.let {
+ if (it.semanticsConfiguration?.isAutofillable() == true) {
+ val semanticsId = it.semanticsId
+ rectManager.rects.withRect(semanticsId) { l, t, r, b ->
+ platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
+ }
+ previouslyFocusedId = semanticsId
+ }
+ }
+ }
+
/** Send events to the autofill service in response to semantics changes. */
override fun onSemanticsChanged(
semanticsInfo: SemanticsInfo,
@@ -109,22 +134,18 @@
}
// Check Focus.
- // TODO: Instead of saving the focused item here, add some internal API to focusManager
- // so that this could be more efficient.
- val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
- val currFocus = config?.getOrNull(SemanticsProperties.Focused)
- if (previousFocus != true && currFocus == true && config.isAutofillable()) {
- previouslyFocusedId = semanticsId
- rectManager.rects.withRect(semanticsId) { left, top, right, bottom ->
- platformAutofillManager.notifyViewEntered(
- view,
- semanticsId,
- Rect(left, top, right, bottom)
- )
+ if (@OptIn(ExperimentalComposeUiApi::class) !ComposeUiFlags.isTrackFocusEnabled) {
+ val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
+ val currFocus = config?.getOrNull(SemanticsProperties.Focused)
+ if (previousFocus != true && currFocus == true && config.isAutofillable()) {
+ previouslyFocusedId = semanticsId
+ rectManager.rects.withRect(semanticsId) { l, t, r, b ->
+ platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
+ }
}
- }
- if (previousFocus == true && currFocus != true && prevConfig.isAutofillable()) {
- platformAutofillManager.notifyViewExited(view, semanticsId)
+ if (previousFocus == true && currFocus != true && prevConfig.isAutofillable()) {
+ platformAutofillManager.notifyViewExited(view, semanticsId)
+ }
}
// Update currentlyDisplayedIDs if relevance to Autocommit has changed.
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 15f5213..c059f5d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -2037,7 +2037,10 @@
viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(this)
- if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners += it }
+ _autofillManager?.let {
+ focusOwner.listeners += it
+ semanticsOwner.listeners += it
+ }
}
override fun onDetachedFromWindow() {
@@ -2062,7 +2065,10 @@
viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
- if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners -= it }
+ _autofillManager?.let {
+ semanticsOwner.listeners -= it
+ focusOwner.listeners -= it
+ }
}
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt
new file mode 100644
index 0000000..139f0db
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.compose.ui.focus
+
+/** Listener that can be used to observe focus changes. */
+internal interface FocusListener {
+ /**
+ * Called when the focus changes.
+ *
+ * @param previous The previous focus target node.
+ * @param current The new focus target node.
+ */
+ fun onFocusChanged(previous: FocusTargetModifierNode?, current: FocusTargetModifierNode?)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index ae474794..ae030e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.focus
+import androidx.collection.MutableObjectList
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.KeyEvent
@@ -147,6 +148,9 @@
/** Schedule the owner to be invalidated after onApplyChanges. */
fun scheduleInvalidationForOwner()
+ /** Listeners that will be notified when the active item changes. */
+ val listeners: MutableObjectList<FocusListener>
+
/** The focus state of the root focus node. */
val rootState: FocusState
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 07a8cdf..01e0582 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.focus
import androidx.collection.MutableLongSet
+import androidx.collection.MutableObjectList
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -465,15 +466,19 @@
override val rootState: FocusState
get() = rootFocusNode.focusState
+ override val listeners: MutableObjectList<FocusListener> = MutableObjectList(1)
+
override var activeFocusTargetNode: FocusTargetNode? = null
set(value) {
val previousValue = field
field = value
if (value == null || previousValue !== value) isFocusCaptured = false
+ if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
+ listeners.forEach { it.onFocusChanged(previousValue, value) }
+ }
}
override var isFocusCaptured: Boolean = false
- get() = field
set(value) {
requirePrecondition(!value || activeFocusTargetNode != null) {
"Cannot capture focus when the active focus target node is unset"
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index d0ada4b..08505aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.semantics.SemanticsInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -325,6 +326,8 @@
}
.layoutNode
+internal fun DelegatableNode.requireSemanticsInfo(): SemanticsInfo = requireLayoutNode()
+
internal fun DelegatableNode.requireOwner(): Owner =
checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
diff --git a/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..3026d4b
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt
@@ -0,0 +1,101 @@
+// Signature format: 4.0
+package androidx.coordinatorlayout.widget {
+
+ public class CoordinatorLayout extends android.view.ViewGroup implements androidx.core.view.NestedScrollingParent2 androidx.core.view.NestedScrollingParent3 {
+ ctor public CoordinatorLayout(android.content.Context);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?, @AttrRes int);
+ method public void dispatchDependentViewsChanged(android.view.View);
+ method public boolean doViewsOverlap(android.view.View, android.view.View);
+ method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
+ method public java.util.List<android.view.View!> getDependencies(android.view.View);
+ method public java.util.List<android.view.View!> getDependents(android.view.View);
+ method public android.graphics.drawable.Drawable? getStatusBarBackground();
+ method public boolean isPointInChildBounds(android.view.View, int, int);
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onDraw(android.graphics.Canvas);
+ method public void onLayoutChild(android.view.View, int);
+ method public void onMeasureChild(android.view.View, int, int, int, int);
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int, int[]);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
+ method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+ method public void setStatusBarBackgroundColor(@ColorInt int);
+ method public void setStatusBarBackgroundResource(@DrawableRes int);
+ }
+
+ public static interface CoordinatorLayout.AttachedBehavior {
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior getBehavior();
+ }
+
+ public abstract static class CoordinatorLayout.Behavior<V extends android.view.View> {
+ ctor public CoordinatorLayout.Behavior();
+ ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet?);
+ method public boolean blocksInteractionBelow(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method public boolean getInsetDodgeRect(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect);
+ method @ColorInt public int getScrimColor(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method @FloatRange(from=0, to=1) public float getScrimOpacity(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method public static Object? getTag(android.view.View);
+ method public boolean layoutDependsOn(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public androidx.core.view.WindowInsetsCompat onApplyWindowInsets(androidx.coordinatorlayout.widget.CoordinatorLayout, V, androidx.core.view.WindowInsetsCompat);
+ method public void onAttachedToLayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+ method public boolean onDependentViewChanged(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDependentViewRemoved(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDetachedFromLayoutParams();
+ method public boolean onInterceptTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public boolean onLayoutChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int);
+ method public boolean onMeasureChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int, int, int, int);
+ method public boolean onNestedFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+ method public boolean onNestedPreFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float);
+ method @Deprecated public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+ method public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
+ method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+ method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
+ method public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int, int[]);
+ method @Deprecated public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+ method public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+ method public boolean onRequestChildRectangleOnScreen(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+ method public void onRestoreInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.os.Parcelable);
+ method public android.os.Parcelable? onSaveInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method @Deprecated public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+ method public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+ method @Deprecated public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int);
+ method public boolean onTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public static void setTag(android.view.View, Object?);
+ }
+
+ @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface CoordinatorLayout.DefaultBehavior {
+ method @Deprecated public abstract Class<? extends androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior!> value();
+ }
+
+ public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(int, int);
+ method @IdRes public int getAnchorId();
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior? getBehavior();
+ method public void setAnchorId(@IdRes int);
+ method public void setBehavior(androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior?);
+ field public int anchorGravity;
+ field public int dodgeInsetEdges;
+ field public int gravity;
+ field public int insetEdge;
+ field public int keyline;
+ }
+
+ protected static class CoordinatorLayout.SavedState extends androidx.customview.view.AbsSavedState {
+ ctor public CoordinatorLayout.SavedState(android.os.Parcel!, ClassLoader!);
+ ctor public CoordinatorLayout.SavedState(android.os.Parcelable!);
+ field public static final android.os.Parcelable.Creator<androidx.coordinatorlayout.widget.CoordinatorLayout.SavedState!>! CREATOR;
+ }
+
+}
+
diff --git a/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..6af58e16
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+attr keylines
+attr layout_anchor
+attr layout_anchorGravity
+attr layout_behavior
+attr layout_dodgeInsetEdges
+attr layout_insetEdge
+attr layout_keyline
+attr statusBarBackground
+style Widget_Support_CoordinatorLayout
diff --git a/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..22ecfc8
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,105 @@
+// Signature format: 4.0
+package androidx.coordinatorlayout.widget {
+
+ public class CoordinatorLayout extends android.view.ViewGroup implements androidx.core.view.NestedScrollingParent2 androidx.core.view.NestedScrollingParent3 {
+ ctor public CoordinatorLayout(android.content.Context);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?, @AttrRes int);
+ method public void dispatchDependentViewsChanged(android.view.View);
+ method public boolean doViewsOverlap(android.view.View, android.view.View);
+ method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
+ method public java.util.List<android.view.View!> getDependencies(android.view.View);
+ method public java.util.List<android.view.View!> getDependents(android.view.View);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.core.view.WindowInsetsCompat? getLastWindowInsets();
+ method public android.graphics.drawable.Drawable? getStatusBarBackground();
+ method public boolean isPointInChildBounds(android.view.View, int, int);
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onDraw(android.graphics.Canvas);
+ method public void onLayoutChild(android.view.View, int);
+ method public void onMeasureChild(android.view.View, int, int, int, int);
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int, int[]);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
+ method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+ method public void setStatusBarBackgroundColor(@ColorInt int);
+ method public void setStatusBarBackgroundResource(@DrawableRes int);
+ }
+
+ public static interface CoordinatorLayout.AttachedBehavior {
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior getBehavior();
+ }
+
+ public abstract static class CoordinatorLayout.Behavior<V extends android.view.View> {
+ ctor public CoordinatorLayout.Behavior();
+ ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet?);
+ method public boolean blocksInteractionBelow(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method public boolean getInsetDodgeRect(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect);
+ method @ColorInt public int getScrimColor(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method @FloatRange(from=0, to=1) public float getScrimOpacity(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method public static Object? getTag(android.view.View);
+ method public boolean layoutDependsOn(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public androidx.core.view.WindowInsetsCompat onApplyWindowInsets(androidx.coordinatorlayout.widget.CoordinatorLayout, V, androidx.core.view.WindowInsetsCompat);
+ method public void onAttachedToLayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+ method public boolean onDependentViewChanged(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDependentViewRemoved(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDetachedFromLayoutParams();
+ method public boolean onInterceptTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public boolean onLayoutChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int);
+ method public boolean onMeasureChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int, int, int, int);
+ method public boolean onNestedFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+ method public boolean onNestedPreFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float);
+ method @Deprecated public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+ method public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[], @androidx.core.view.ViewCompat.NestedScrollType int);
+ method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+ method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int);
+ method public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int, int[]);
+ method @Deprecated public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int);
+ method public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int, @androidx.core.view.ViewCompat.NestedScrollType int);
+ method public boolean onRequestChildRectangleOnScreen(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+ method public void onRestoreInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.os.Parcelable);
+ method public android.os.Parcelable? onSaveInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+ method @Deprecated public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int);
+ method public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int, @androidx.core.view.ViewCompat.NestedScrollType int);
+ method @Deprecated public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+ method public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, @androidx.core.view.ViewCompat.NestedScrollType int);
+ method public boolean onTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public static void setTag(android.view.View, Object?);
+ }
+
+ @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface CoordinatorLayout.DefaultBehavior {
+ method @Deprecated public abstract Class<? extends androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior!> value();
+ }
+
+ @IntDef({0x0, 0x1, 0x2}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CoordinatorLayout.DispatchChangeEvent {
+ }
+
+ public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(int, int);
+ method @IdRes public int getAnchorId();
+ method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior? getBehavior();
+ method public void setAnchorId(@IdRes int);
+ method public void setBehavior(androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior?);
+ field public int anchorGravity;
+ field public int dodgeInsetEdges;
+ field public int gravity;
+ field public int insetEdge;
+ field public int keyline;
+ }
+
+ protected static class CoordinatorLayout.SavedState extends androidx.customview.view.AbsSavedState {
+ ctor public CoordinatorLayout.SavedState(android.os.Parcel!, ClassLoader!);
+ ctor public CoordinatorLayout.SavedState(android.os.Parcelable!);
+ field public static final android.os.Parcelable.Creator<androidx.coordinatorlayout.widget.CoordinatorLayout.SavedState!>! CREATOR;
+ }
+
+}
+
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
index cf59415..e9af1a6 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
@@ -395,7 +395,7 @@
}
@Test
- fun getResult_recordNeededForCalculation_returnsMapWithResult() {
+ fun getResult_recordContributingToAggregation_returnsListWithResult() {
val aggregator =
ResultGroupedByPeriodAggregator(
createLocalTimeRange(
@@ -423,7 +423,34 @@
}
@Test
- fun getResult_recordOutOfBounds_returnsEmptyMap() {
+ fun getResult_recordNotContributingToAggregation_returnsEmptyList() {
+ val aggregator =
+ ResultGroupedByPeriodAggregator(
+ createLocalTimeRange(
+ TimeRangeFilter.after(
+ Instant.ofEpochMilli(100).toLocalTimeWithDefaultZoneFallback(ZoneOffset.UTC)
+ )
+ ),
+ bucketPeriod = Period.ofDays(1)
+ ) {
+ TransFatTotalAggregationProcessor(it)
+ }
+
+ aggregator.filterAndAggregate(
+ NutritionRecord(
+ startTime = Instant.ofEpochMilli(100),
+ endTime = Instant.ofEpochMilli(1000),
+ startZoneOffset = ZoneOffset.UTC,
+ endZoneOffset = ZoneOffset.UTC,
+ metadata = Metadata(dataOrigin = DataOrigin("some.package")),
+ )
+ )
+
+ assertThat(aggregator.getResult()).isEmpty()
+ }
+
+ @Test
+ fun getResult_recordOutOfBounds_returnsEmptyList() {
val aggregator =
ResultGroupedByPeriodAggregator(
LocalTimeRange(
diff --git a/libraryversions.toml b/libraryversions.toml
index 8988755..11546e0 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -31,7 +31,7 @@
CONSTRAINTLAYOUT_COMPOSE = "1.2.0-alpha01"
CONSTRAINTLAYOUT_CORE = "1.2.0-alpha01"
CONTENTPAGER = "1.1.0-alpha01"
-COORDINATORLAYOUT = "1.3.0-alpha03"
+COORDINATORLAYOUT = "1.3.0-beta01"
CORE = "1.16.0-alpha02"
CORE_ANIMATION = "1.0.0"
CORE_ANIMATION_TESTING = "1.0.0"
diff --git a/navigation3/navigation3/api/current.txt b/navigation3/navigation3/api/current.txt
index bbb7d86..ec7e433 100644
--- a/navigation3/navigation3/api/current.txt
+++ b/navigation3/navigation3/api/current.txt
@@ -1,16 +1,6 @@
// Signature format: 4.0
package androidx.navigation3 {
- public final class AnimatedNavDisplay {
- method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
- method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
- field public static final androidx.navigation3.AnimatedNavDisplay INSTANCE;
- }
-
- public final class AnimatedNavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void AnimatedNavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
- }
-
public interface NavContentWrapper {
method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
@@ -18,11 +8,12 @@
public final class NavDisplay {
method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
+ method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
field public static final androidx.navigation3.NavDisplay INSTANCE;
}
public final class NavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+ method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
}
public final class NavRecord<T> {
@@ -36,7 +27,8 @@
}
public final class NavWrapperManager {
- ctor public NavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+ ctor public NavWrapperManager();
+ ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
}
diff --git a/navigation3/navigation3/api/restricted_current.txt b/navigation3/navigation3/api/restricted_current.txt
index bbb7d86..ec7e433 100644
--- a/navigation3/navigation3/api/restricted_current.txt
+++ b/navigation3/navigation3/api/restricted_current.txt
@@ -1,16 +1,6 @@
// Signature format: 4.0
package androidx.navigation3 {
- public final class AnimatedNavDisplay {
- method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
- method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
- field public static final androidx.navigation3.AnimatedNavDisplay INSTANCE;
- }
-
- public final class AnimatedNavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void AnimatedNavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
- }
-
public interface NavContentWrapper {
method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
@@ -18,11 +8,12 @@
public final class NavDisplay {
method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
+ method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
field public static final androidx.navigation3.NavDisplay INSTANCE;
}
public final class NavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+ method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
}
public final class NavRecord<T> {
@@ -36,7 +27,8 @@
}
public final class NavWrapperManager {
- ctor public NavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+ ctor public NavWrapperManager();
+ ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
}
diff --git a/navigation3/navigation3/build.gradle b/navigation3/navigation3/build.gradle
index 796ed34..74aa38e 100644
--- a/navigation3/navigation3/build.gradle
+++ b/navigation3/navigation3/build.gradle
@@ -67,11 +67,10 @@
androidMain {
dependsOn(jvmMain)
dependencies {
+ api("androidx.compose.animation:animation:1.7.5")
+ api("androidx.compose.ui:ui:1.7.5")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.annotation:annotation:1.8.0")
- implementation("androidx.compose.animation:animation:1.7.5")
- implementation("androidx.compose.foundation:foundation:1.7.5")
- implementation("androidx.compose.ui:ui:1.7.5")
}
}
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
index bc706ddb..fa6ff47 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
@@ -24,7 +24,6 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper
-import androidx.navigation3.AnimatedNavDisplay
import androidx.navigation3.NavDisplay
import androidx.navigation3.NavRecord
import androidx.navigation3.SavedStateNavContentWrapper
@@ -32,9 +31,13 @@
import androidx.navigation3.recordProvider
import androidx.navigation3.rememberNavWrapperManager
+class ProfileViewModel : ViewModel() {
+ val name = "no user"
+}
+
@Sampled
@Composable
-fun BasicNav() {
+fun BaseNav() {
val backStack = rememberMutableStateListOf(Profile)
val manager =
rememberNavWrapperManager(
@@ -46,54 +49,14 @@
onBack = { backStack.removeLast() },
recordProvider =
recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
- record<Profile> {
- val viewModel = viewModel<ProfileViewModel>()
- Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
- }
- record<Scrollable> { Scrollable({ backStack.add(it) }) { backStack.removeLast() } }
- record<Dialog>(featureMap = NavDisplay.isDialog(true)) {
- DialogContent { backStack.removeLast() }
- }
- record<Dashboard> { dashboardArgs ->
- val userId = dashboardArgs.userId
- Dashboard(userId, onBack = { backStack.removeLast() })
- }
- }
- )
-}
-
-class ProfileViewModel : ViewModel() {
- val name = "no user"
-}
-
-@Sampled
-@Composable
-fun AnimatedNav() {
- val backStack = rememberMutableStateListOf(Profile)
- val manager =
- rememberNavWrapperManager(
- listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
- )
- AnimatedNavDisplay(
- backstack = backStack,
- wrapperManager = manager,
- onBack = { backStack.removeLast() },
- recordProvider =
- recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
record<Profile>(
- AnimatedNavDisplay.transition(
- slideInHorizontally { it },
- slideOutHorizontally { it }
- )
+ NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) {
val viewModel = viewModel<ProfileViewModel>()
Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
}
record<Scrollable>(
- AnimatedNavDisplay.transition(
- slideInHorizontally { it },
- slideOutHorizontally { it }
- )
+ NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) {
Scrollable({ backStack.add(it) }) { backStack.removeLast() }
}
@@ -101,10 +64,7 @@
DialogContent { backStack.removeLast() }
}
record<Dashboard>(
- AnimatedNavDisplay.transition(
- slideInHorizontally { it },
- slideOutHorizontally { it }
- )
+ NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) { dashboardArgs ->
val userId = dashboardArgs.userId
Dashboard(userId, onBack = { backStack.removeLast() })
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
similarity index 91%
rename from navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt
rename to navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
index 1ff756b..d7da926 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.kruth.assertThat
-import androidx.navigation3.AnimatedNavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
+import androidx.navigation3.NavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import kotlin.test.Test
@@ -36,7 +36,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-class AnimatedNavDisplayTest {
+class AnimatedTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
@@ -47,8 +47,7 @@
composeTestRule.setContent {
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- AnimatedNavDisplay(backstack, wrapperManager = manager) {
+ NavDisplay(backstack) {
when (it) {
first -> NavRecord(first) { Text(first) }
second -> NavRecord(second) { Text(second) }
@@ -91,8 +90,7 @@
composeTestRule.setContent {
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- AnimatedNavDisplay(backstack, wrapperManager = manager) {
+ NavDisplay(backstack) {
when (it) {
first ->
NavRecord(
@@ -104,7 +102,7 @@
NavRecord(
second,
featureMap =
- AnimatedNavDisplay.transition(
+ NavDisplay.transition(
enter = fadeIn(tween(customDuration)),
exit = fadeOut(tween(customDuration))
)
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
index a36372a8..fd5749a 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
@@ -45,10 +45,7 @@
@Test
fun testContentShown() {
composeTestRule.setContent {
- val manager = rememberNavWrapperManager(emptyList())
- NavDisplay(backstack = mutableStateListOf(first), wrapperManager = manager) {
- NavRecord(first) { Text(first) }
- }
+ NavDisplay(backstack = mutableStateListOf(first)) { NavRecord(first) { Text(first) } }
}
assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
@@ -59,8 +56,7 @@
lateinit var backstack: MutableList<Any>
composeTestRule.setContent {
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- NavDisplay(backstack = backstack, wrapperManager = manager) {
+ NavDisplay(backstack = backstack) {
when (it) {
first -> NavRecord(first) { Text(first) }
second -> NavRecord(second) { Text(second) }
@@ -82,15 +78,7 @@
lateinit var backstack: MutableList<Any>
composeTestRule.setContent {
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- NavDisplay(
- backstack = backstack,
- wrapperManager = manager,
- onBack = {
- // removeLast requires API 35
- backstack.removeAt(backstack.size - 1)
- }
- ) {
+ NavDisplay(backstack = backstack) {
when (it) {
first -> NavRecord(first) { Text(first) }
second -> NavRecord(second, NavDisplay.isDialog(true)) { Text(second) }
@@ -115,15 +103,7 @@
composeTestRule.setContent {
onBackDispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- NavDisplay(
- backstack = backstack,
- wrapperManager = manager,
- onBack = {
- // removeLast requires API 35
- backstack.removeAt(backstack.size - 1)
- }
- ) {
+ NavDisplay(backstack = backstack) {
when (it) {
first -> NavRecord(first) { Text(first) }
second -> NavRecord(second) { Text(second) }
@@ -150,8 +130,7 @@
lateinit var backstack: MutableList<Any>
composeTestRule.setContent {
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(emptyList())
- NavDisplay(backstack = backstack, wrapperManager = manager) {
+ NavDisplay(backstack = backstack) {
when (it) {
first -> NavRecord(first) { numberOnScreen1 = rememberSaveable { increment++ } }
second -> NavRecord(second) {}
@@ -226,7 +205,6 @@
backStack2 = remember { mutableStateListOf(second) }
backStack3 = remember { mutableStateListOf(third) }
state = remember { mutableStateOf(1) }
- val manager = rememberNavWrapperManager(emptyList())
NavDisplay(
backstack =
when (state.value) {
@@ -234,7 +212,6 @@
2 -> backStack2
else -> backStack3
},
- wrapperManager = manager,
recordProvider =
recordProvider {
record(first) { Text(first) }
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt
deleted file mode 100644
index c1f4817..0000000
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.navigation3
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Dialog
-import androidx.navigation3.AnimatedNavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
-
-/** Object that indicates the features that can be handled by the [AnimatedNavDisplay] */
-public object AnimatedNavDisplay {
- /**
- * Function to be called on the [NavRecord.featureMap] to notify the [AnimatedNavDisplay] that
- * the content should be animated using the provided transitions.
- */
- public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
- if (enter == null || exit == null) emptyMap()
- else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
-
- /**
- * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
- * content should be displayed inside of a [Dialog]
- */
- public fun isDialog(boolean: Boolean): Map<String, Any> =
- if (!boolean) emptyMap() else mapOf(DIALOG_KEY to true)
-
- internal const val ENTER_TRANSITION_KEY = "enterTransition"
- internal const val EXIT_TRANSITION_KEY = "exitTransition"
- internal const val DIALOG_KEY = "dialog"
- internal const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700
-}
-
-/**
- * Display for Composable content that displays a single pane of content at a time, but can move
- * that content in and out with customized transitions.
- *
- * The AnimatedNavDisplay displays the content associated with the last key on the back stack in
- * most circumstances. If that content wants to be displayed as a dialog, as communicated by adding
- * [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
- * the second to last key is a displayed in the background.
- *
- * @param backstack the collection of keys that represents the state that needs to be handled
- * @param wrapperManager the manager that combines all of the [NavContentWrapper]s
- * @param modifier the modifier to be applied to the layout.
- * @param contentAlignment The [Alignment] of the [AnimatedContent]
- * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
- * individually for each [NavRecord] by passing in the record's transitions through
- * [NavRecord.featureMap].
- * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
- * individually for each [NavRecord] by passing in the record's transitions through
- * [NavRecord.featureMap].
- * @param onBack a callback for handling system back presses
- * @param recordProvider lambda used to construct each possible [NavRecord]
- * @sample androidx.navigation3.samples.AnimatedNav
- */
-@Composable
-public fun <T : Any> AnimatedNavDisplay(
- backstack: List<T>,
- wrapperManager: NavWrapperManager,
- modifier: Modifier = Modifier,
- contentAlignment: Alignment = Alignment.TopStart,
- sizeTransform: SizeTransform? = null,
- enterTransition: EnterTransition =
- fadeIn(
- animationSpec =
- tween(
- DEFAULT_TRANSITION_DURATION_MILLISECOND,
- )
- ),
- exitTransition: ExitTransition =
- fadeOut(
- animationSpec =
- tween(
- DEFAULT_TRANSITION_DURATION_MILLISECOND,
- )
- ),
- onBack: () -> Unit = {},
- recordProvider: (key: T) -> NavRecord<out T>
-) {
- BackHandler(backstack.size > 1, onBack)
- wrapperManager.PrepareBackStack(backStack = backstack)
- val key = backstack.last()
- val record = recordProvider.invoke(key)
-
- // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
- val finalEnterTransition =
- record.featureMap[AnimatedNavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition
- ?: enterTransition
- val finalExitTransition =
- record.featureMap[AnimatedNavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition
- ?: exitTransition
-
- // if there is a dialog, we should create a transition with the next to last entry instead.
- val transition =
- if (record.featureMap[AnimatedNavDisplay.DIALOG_KEY] == true) {
- if (backstack.size > 1) {
- val previousKey = backstack[backstack.size - 2]
- updateTransition(targetState = previousKey, label = previousKey.toString())
- } else {
- null
- }
- } else {
- updateTransition(targetState = key, label = key.toString())
- }
-
- transition?.AnimatedContent(
- modifier = modifier,
- transitionSpec = {
- ContentTransform(
- targetContentEnter = finalEnterTransition,
- initialContentExit = finalExitTransition,
- sizeTransform = sizeTransform
- )
- },
- contentAlignment = contentAlignment
- ) { innerKey ->
- wrapperManager.ContentForRecord(recordProvider.invoke(innerKey))
- }
-
- if (record.featureMap[AnimatedNavDisplay.DIALOG_KEY] == true) {
- Dialog(onBack) { wrapperManager.ContentForRecord(record) }
- }
-}
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
index ae0ce29..cdcff77 100644
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
+++ b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
@@ -13,62 +13,138 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.navigation3
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Box
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog
+import androidx.navigation3.NavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
/** Object that indicates the features that can be handled by the [NavDisplay] */
public object NavDisplay {
/**
* Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+ * content should be animated using the provided transitions.
+ */
+ public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
+ if (enter == null || exit == null) emptyMap()
+ else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
+
+ /**
+ * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
* content should be displayed inside of a [Dialog]
*/
public fun isDialog(boolean: Boolean): Map<String, Any> =
if (!boolean) emptyMap() else mapOf(DIALOG_KEY to true)
+ internal const val ENTER_TRANSITION_KEY = "enterTransition"
+ internal const val EXIT_TRANSITION_KEY = "exitTransition"
internal const val DIALOG_KEY = "dialog"
+ internal const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700
}
/**
- * Simple display for Composable content that displays a single pane of content at a time.
+ * Display for Composable content that displays a single pane of content at a time, but can move
+ * that content in and out with customized transitions.
*
* The NavDisplay displays the content associated with the last key on the back stack in most
* circumstances. If that content wants to be displayed as a dialog, as communicated by adding
* [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
* the second to last key is a displayed in the background.
*
- * @param modifier the modifier to be applied to the layout.
* @param backstack the collection of keys that represents the state that needs to be handled
* @param wrapperManager the manager that combines all of the [NavContentWrapper]s
+ * @param modifier the modifier to be applied to the layout.
+ * @param contentAlignment The [Alignment] of the [AnimatedContent]
+ * * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
+ * * individually for each [NavRecord] by passing in the record's transitions through
+ * * [NavRecord.featureMap].
+ * * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
+ * * individually for each [NavRecord] by passing in the record's transitions through
+ * * [NavRecord.featureMap].
+ *
* @param onBack a callback for handling system back presses
* @param recordProvider lambda used to construct each possible [NavRecord]
- * @sample androidx.navigation3.samples.BasicNav
+ * @sample androidx.navigation3.samples.BaseNav
*/
@Composable
public fun <T : Any> NavDisplay(
backstack: List<T>,
- wrapperManager: NavWrapperManager,
modifier: Modifier = Modifier,
- onBack: () -> Unit = {},
+ wrapperManager: NavWrapperManager = rememberNavWrapperManager(emptyList()),
+ contentAlignment: Alignment = Alignment.TopStart,
+ sizeTransform: SizeTransform? = null,
+ enterTransition: EnterTransition =
+ fadeIn(
+ animationSpec =
+ tween(
+ DEFAULT_TRANSITION_DURATION_MILLISECOND,
+ )
+ ),
+ exitTransition: ExitTransition =
+ fadeOut(
+ animationSpec =
+ tween(
+ DEFAULT_TRANSITION_DURATION_MILLISECOND,
+ )
+ ),
+ onBack: () -> Unit = { if (backstack is MutableList) backstack.removeAt(backstack.size - 1) },
recordProvider: (key: T) -> NavRecord<out T>
) {
BackHandler(backstack.size > 1, onBack)
wrapperManager.PrepareBackStack(backStack = backstack)
val key = backstack.last()
val record = recordProvider.invoke(key)
- if (record.featureMap[NavDisplay.DIALOG_KEY] == true) {
- if (backstack.size > 1) {
- val previousKey = backstack[backstack.size - 2]
- val lastRecord = recordProvider.invoke(previousKey)
- Box(modifier = modifier) { wrapperManager.ContentForRecord(lastRecord) }
+
+ // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
+ val finalEnterTransition =
+ record.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
+ val finalExitTransition =
+ record.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
+
+ val isDialog = record.featureMap[NavDisplay.DIALOG_KEY] == true
+
+ // if there is a dialog, we should create a transition with the next to last entry instead.
+ val transition =
+ if (isDialog) {
+ if (backstack.size > 1) {
+ val previousKey = backstack[backstack.size - 2]
+ val previousRecord = recordProvider.invoke(previousKey)
+ updateTransition(targetState = previousRecord, label = previousKey.toString())
+ } else {
+ null
+ }
+ } else {
+ updateTransition(targetState = record, label = key.toString())
}
+
+ transition?.AnimatedContent(
+ modifier = modifier,
+ transitionSpec = {
+ ContentTransform(
+ targetContentEnter = finalEnterTransition,
+ initialContentExit = finalExitTransition,
+ sizeTransform = sizeTransform
+ )
+ },
+ contentAlignment = contentAlignment,
+ contentKey = { it.key }
+ ) { innerRecord ->
+ wrapperManager.ContentForRecord(innerRecord)
+ }
+
+ if (isDialog) {
Dialog(onBack) { wrapperManager.ContentForRecord(record) }
- } else {
- Box(modifier = modifier) { wrapperManager.ContentForRecord(record) }
}
}
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
index 114ac13..4a137df 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
@@ -36,7 +36,7 @@
*
* @param navContentWrappers the [NavContentWrapper]s that are providing data to the content
*/
-public class NavWrapperManager(navContentWrappers: List<NavContentWrapper>) {
+public class NavWrapperManager(navContentWrappers: List<NavContentWrapper> = emptyList()) {
/**
* Final list of wrappers. This always adds a [SaveableStateNavContentWrapper] by default, as it
* is required. It then filters out any duplicates to ensure there is always one instance of any
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
index cab7204..9c6443f 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
@@ -72,7 +72,12 @@
DisposableEffect(key1 = key) {
refCount[key] = refCount.getOrDefault(key, 0).plus(1)
onDispose {
- if (refCount[key] == 0) {
+ // We need to check to make sure that the refcount has been cleared here because
+ // when we are using animations, if the entire back stack is changed, we will
+ // execute the onDispose above that clears all of the counts before we finish the
+ // transition and run this onDispose so our count will already be gone and we
+ // should just remove the state.
+ if (!refCount.contains(key) || refCount[key] == 0) {
savedStateHolder?.removeState(key)
} else {
refCount[key] =
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
index 27466007..0c1973b 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
@@ -179,9 +179,20 @@
/** The currently selected PDF content, as [Selection] */
public val currentSelection: Selection?
get() {
- return selectionStateManager?.selectionModel?.selection
+ return selectionStateManager?.selectionModel?.value?.selection
}
+ /** Listener interface to receive updates when the [currentSelection] changes */
+ public interface OnSelectionChangedListener {
+ /** Called when the [Selection] has changed */
+ public fun onSelectionChanged(
+ previousSelection: Selection?,
+ newSelection: Selection?,
+ )
+ }
+
+ private var onSelectionChangedListeners = mutableListOf<OnSelectionChangedListener>()
+
/**
* The [CoroutineScope] used to make suspending calls to [PdfDocument]. The size of the fixed
* thread pool is arbitrary and subject to tuning.
@@ -192,8 +203,8 @@
private var pageLayoutManager: PageLayoutManager? = null
private var pageManager: PageManager? = null
private var visiblePagesCollector: Job? = null
- private var dimensionsCollector: Job? = null
- private var invalidationCollector: Job? = null
+ private var layoutInfoCollector: Job? = null
+ private var pageSignalCollector: Job? = null
private var selectionStateCollector: Job? = null
private var deferredScrollPage: Int? = null
@@ -301,6 +312,33 @@
}
}
+ /**
+ * Adds the specified listener to the list of listeners that will be notified of selection
+ * change events.
+ *
+ * @param listener listener to notify when selection change events occur
+ * @see removeOnSelectionChangedListener
+ */
+ public fun addOnSelectionChangedListener(listener: OnSelectionChangedListener) {
+ onSelectionChangedListeners.add(listener)
+ }
+
+ /**
+ * Removes the specified listener from the list of listeners that will be notified of selection
+ * change events.
+ *
+ * @param listener listener to remove
+ */
+ public fun removeOnSelectionChangedListener(listener: OnSelectionChangedListener) {
+ onSelectionChangedListeners.remove(listener)
+ }
+
+ private fun dispatchSelectionChanged(old: Selection?, new: Selection?) {
+ for (listener in onSelectionChangedListeners) {
+ listener.onSelectionChanged(old, new)
+ }
+ }
+
private fun gotoPage(pageNum: Int) {
checkMainThread()
val localPageLayoutManager =
@@ -386,7 +424,7 @@
for (i in visiblePages.lower..visiblePages.upper) {
val pageLoc = localPaginationManager.getPageLocation(i, getVisibleAreaInContentCoords())
pageManager?.drawPage(i, canvas, pageLoc)
- selectionModel?.let {
+ selectionModel?.value?.let {
selectionRenderer.drawSelectionOnPage(
model = it,
pageNum = i,
@@ -492,7 +530,7 @@
}
state.documentUri = pdfDocument?.uri
state.paginationModel = pageLayoutManager?.paginationModel
- state.selectionModel = selectionStateManager?.selectionModel
+ state.selectionModel = selectionStateManager?.selectionModel?.value
return state
}
@@ -638,32 +676,23 @@
CoroutineScope(HandlerCompat.createAsync(handler.looper).asCoroutineDispatcher())
pageLayoutManager?.let { manager ->
// Don't let two copies of this run concurrently
- val dimensionsToJoin = dimensionsCollector?.apply { cancel() }
- dimensionsCollector =
+ val layoutInfoToJoin = layoutInfoCollector?.apply { cancel() }
+ layoutInfoCollector =
mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
- manager.dimensions.collect {
- // Prevent 2 copies from running concurrently
- dimensionsToJoin?.join()
- onPageDimensionsReceived(it.first, it.second)
+ // Prevent 2 copies from running concurrently
+ layoutInfoToJoin?.join()
+ launch {
+ manager.dimensions.collect { onPageDimensionsReceived(it.first, it.second) }
}
- }
- // Don't let two copies of this run concurrently
- val visiblePagesToJoin = visiblePagesCollector?.apply { cancel() }
- visiblePagesCollector =
- mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
- manager.visiblePages.collect {
- // Prevent 2 copies from running concurrently
- visiblePagesToJoin?.join()
- onVisiblePagesChanged()
- }
+ launch { manager.visiblePages.collect { onVisiblePagesChanged() } }
}
}
pageManager?.let { manager ->
- val invalidationToJoin = invalidationCollector?.apply { cancel() }
- invalidationCollector =
+ val pageSignalsToJoin = pageSignalCollector?.apply { cancel() }
+ pageSignalCollector =
mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
// Prevent 2 copies from running concurrently
- invalidationToJoin?.join()
+ pageSignalsToJoin?.join()
launch { manager.invalidationSignalFlow.collect { invalidate() } }
launch {
manager.pageTextReadyFlow.collect { pageNum ->
@@ -678,15 +707,22 @@
mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
// Prevent 2 copies from running concurrently
selectionToJoin?.join()
- manager.selectionUiSignalBus.collect { onSelectionUiSignal(it) }
+ launch { manager.selectionUiSignalBus.collect { onSelectionUiSignal(it) } }
+ var prevSelection = currentSelection
+ launch {
+ manager.selectionModel.collect { newModel ->
+ dispatchSelectionChanged(prevSelection, newModel?.selection)
+ prevSelection = newModel?.selection
+ }
+ }
}
}
}
private fun stopCollectingData() {
- dimensionsCollector?.cancel()
+ layoutInfoCollector?.cancel()
visiblePagesCollector?.cancel()
- invalidationCollector?.cancel()
+ pageSignalCollector?.cancel()
selectionStateCollector?.cancel()
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
index 0ca14c12..ee3f374 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
@@ -62,6 +62,23 @@
endBoundary.writeToParcel(dest, flags)
}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is SelectionModel) return false
+
+ if (other.selection != selection) return false
+ if (other.startBoundary != startBoundary) return false
+ if (other.endBoundary != endBoundary) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = selection.hashCode()
+ result = 31 * result + startBoundary.hashCode()
+ result = 31 * result + endBoundary.hashCode()
+ return result
+ }
+
companion object {
/** Produces a [SelectionModel] from a single [PageSelection] on a single page */
// TODO(b/386398335) Add support for creating a SelectionModel from selections on 2 pages
@@ -135,6 +152,21 @@
dest.writeBoolean(isRtl)
}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is UiSelectionBoundary) return false
+
+ if (other.location != this.location) return false
+ if (other.isRtl != this.isRtl) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = location.hashCode()
+ result = 31 * result + isRtl.hashCode()
+ return result
+ }
+
companion object CREATOR : Parcelable.Creator<UiSelectionBoundary> {
override fun createFromParcel(parcel: Parcel): UiSelectionBoundary {
return UiSelectionBoundary(parcel)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
index 72ec107..3f6cdd7 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
@@ -28,7 +28,10 @@
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Owns and updates all mutable state related to content selection in [PdfView] */
@@ -39,8 +42,10 @@
initialSelection: SelectionModel? = null,
) {
/** The current [Selection] */
- var selectionModel: SelectionModel? = initialSelection
- @VisibleForTesting internal set
+ @VisibleForTesting val _selectionModel = MutableStateFlow<SelectionModel?>(initialSelection)
+
+ val selectionModel: StateFlow<SelectionModel?>
+ get() = _selectionModel
/** Replay at few values in case of an UI signal issued while [PdfView] is not collecting */
private val _selectionUiSignalBus = MutableSharedFlow<SelectionUiSignal>(replay = 3)
@@ -95,11 +100,12 @@
setSelectionJob = null
_selectionUiSignalBus.tryEmit(SelectionUiSignal.ToggleActionMode(show = false))
_selectionUiSignalBus.tryEmit(SelectionUiSignal.Invalidate)
- selectionModel = null
+ // tryEmit will always succeed for StateFlow
+ _selectionModel.tryEmit(null)
}
fun maybeShowActionMode() {
- if (selectionModel != null) {
+ if (selectionModel.value != null) {
_selectionUiSignalBus.tryEmit(SelectionUiSignal.ToggleActionMode(show = true))
}
}
@@ -124,7 +130,7 @@
}
private fun maybeHandleActionDown(location: PdfPoint, currentZoom: Float): Boolean {
- val currentSelection = selectionModel ?: return false
+ val currentSelection = selectionModel.value ?: return false
val start = currentSelection.startBoundary.location
val end = currentSelection.endBoundary.location
val touchTargetContentSize = handleTouchTargetSizePx / currentZoom
@@ -234,7 +240,9 @@
end.pagePoint
)
if (newSelection != null && newSelection.hasBounds) {
- selectionModel = SelectionModel.fromSinglePageSelection(newSelection)
+ _selectionModel.update {
+ SelectionModel.fromSinglePageSelection(newSelection)
+ }
_selectionUiSignalBus.tryEmit(SelectionUiSignal.Invalidate)
// Show the action mode if the user is not actively dragging the handles
if (draggingState == null) {
diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
index b1f4dd3..7980a55 100644
--- a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
+++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
@@ -29,6 +29,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -99,7 +100,7 @@
selectionStateManager.maybeSelectWordAtPoint(selectionPoint)
testDispatcher.scheduler.runCurrent()
- val selectionModel = selectionStateManager.selectionModel
+ val selectionModel = selectionStateManager.selectionModel.value
assertThat(selectionModel).isNotNull()
assertThat(selectionModel?.selection).isInstanceOf(TextSelection::class.java)
val selection = requireNotNull(selectionModel?.selection as TextSelection)
@@ -146,7 +147,7 @@
selectionStateManager.maybeSelectWordAtPoint(selectionPoint2)
testDispatcher.scheduler.runCurrent()
- val selectionModel = selectionStateManager.selectionModel
+ val selectionModel = selectionStateManager.selectionModel.value
assertThat(selectionModel).isNotNull()
assertThat(selectionModel?.selection).isInstanceOf(TextSelection::class.java)
val selection = requireNotNull(selectionModel?.selection as TextSelection)
@@ -184,7 +185,7 @@
assertThat(selectionStateManager.selectionModel).isNotNull()
selectionStateManager.clearSelection()
- assertThat(selectionStateManager.selectionModel).isNull()
+ assertThat(selectionStateManager.selectionModel.value).isNull()
// We only care about the final 2 signals that should occur as a result of cancellation
// hide action mode
assertThat(uiSignals[uiSignals.size - 2])
@@ -201,18 +202,18 @@
// Start a selection and don't finish it (i.e. no runCurrent)
selectionStateManager.maybeSelectWordAtPoint(selectionPoint)
- assertThat(selectionStateManager.selectionModel).isNull()
+ assertThat(selectionStateManager.selectionModel.value).isNull()
// Clear selection, flush the scheduler, and make sure selection remains null (i.e. the work
// enqueued by our initial selection doesn't finish and supersede the cleared state)
selectionStateManager.clearSelection()
testDispatcher.scheduler.runCurrent()
- assertThat(selectionStateManager.selectionModel).isNull()
+ assertThat(selectionStateManager.selectionModel.value).isNull()
}
@Test
fun maybeDragHandle_actionDownOutsideHandle_returnFalse() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
assertThat(
selectionStateManager.maybeDragSelectionHandle(
@@ -226,7 +227,7 @@
@Test
fun maybeDragHandle_actionDownInsideStartHandle_returnTrue() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// Chose a point inside the start handle touch target (below and behind the start position)
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -246,7 +247,7 @@
@Test
fun maybeDragHandle_actionDownInsideEndHandle_returnTrue() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// Chose a point inside the end handle touch target (below and ahead the end position)
val insideEndHandle =
PointF(initialSelectionForDragging.endBoundary.location.pagePoint).apply {
@@ -266,7 +267,7 @@
@Test
fun maybeDragHandle_actionMove_updateSelection() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// "Grab" the start handle
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -295,7 +296,7 @@
// Make sure the selection is updated appropriately
testDispatcher.scheduler.runCurrent()
- val selection = selectionStateManager.selectionModel?.selection
+ val selection = selectionStateManager.selectionModel.value?.selection
assertThat(selection).isInstanceOf(TextSelection::class.java)
val expectedStartLoc = initialSelectionForDragging.endBoundary.location.pagePoint
val expectedEndLoc =
@@ -308,7 +309,7 @@
@Test
fun maybeDragHandle_actionMoveOutsidePage_returnTrue() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// "Grab" the start handle
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -337,7 +338,7 @@
@Test
fun maybeDragHandle_actionMoveWithoutActionDown_returnFalse() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// Chose a point inside the start handle touch target (below and behind the start position)
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -358,7 +359,7 @@
@Test
fun maybeDragHandle_actionUpWithoutActionDown_returnFalse() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// Chose a point inside the start handle touch target (below and behind the start position)
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -379,7 +380,7 @@
@Test
fun maybeDragHandle_actionUp_returnTrueAndStopHandlingEvents() {
- selectionStateManager.selectionModel = initialSelectionForDragging
+ selectionStateManager._selectionModel.update { initialSelectionForDragging }
// Chose a point inside the start handle touch target (below and behind the start position)
val insideStartHandle =
PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
diff --git a/preference/preference/src/main/res/layout/preference_list_fragment.xml b/preference/preference/src/main/res/layout/preference_list_fragment.xml
index ad03481..c2ae115 100644
--- a/preference/preference/src/main/res/layout/preference_list_fragment.xml
+++ b/preference/preference/src/main/res/layout/preference_list_fragment.xml
@@ -19,7 +19,6 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="NewApi"
android:orientation="vertical"
- android:fitsSystemWindows="true"
android:layout_height="match_parent"
android:layout_width="match_parent" >
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
index 37efc13..2460822 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
@@ -142,7 +142,7 @@
.containsExactlyElementsIn(
items.createExpected(
// Paging 3 implementation loads starting from initial key
- fromIndex = 98,
+ fromIndex = 91,
toIndex = 100
)
)
@@ -579,7 +579,7 @@
.containsExactlyElementsIn(
items.createExpected(
// Paging 3 implementation loads starting from initial key
- fromIndex = 98,
+ fromIndex = 91,
toIndex = 100
)
)
diff --git a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index 0cd7d50..e72e6c8 100644
--- a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -247,8 +247,9 @@
dao.addAllItems(ITEMS_LIST)
val result = pager.refresh(initialKey = 40) as LoadResult.Page
- // initial loadSize = 15, but limited by id < 50, should only load items 40 - 50
- assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(40, 50))
+ // initial loadSize = 15, but limited by id < 50, should treat 50 as end and
+ // load items 35 - 50
+ assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(35, 50))
// should have 50 items fulfilling condition of id < 50 (TestItem id 0 - 49)
assertThat(pagingSource.itemCount).isEqualTo(50)
}
@@ -575,6 +576,36 @@
assertThat(result.nextKey).isEqualTo(null)
}
+ @Test
+ fun load_refreshKeyOnLastPage() = runPagingSourceTest { pager, _ ->
+ dao.addAllItems(ITEMS_LIST)
+ pager.refresh(initialKey = 70)
+ dao.deleteTestItems(80, 100)
+
+ // assume user was viewing last item of the refresh load with anchorPosition = 85,
+ // initialLoadSize = 15. This mimics how getRefreshKey() calculates refresh key.
+ val refreshKey = 85 - (15 / 2)
+
+ val pagingSource2 = LimitOffsetPagingSourceImpl(database)
+ val pager2 = TestPager(CONFIG, pagingSource2)
+ val result = pager2.refresh(initialKey = refreshKey) as LoadResult.Page
+
+ // database should only have 80 items left. Refresh key should be moved back at this point
+ // to ensure a full load. (greater than item count - loadSize after deletion)
+ assertThat(pagingSource2.itemCount).isEqualTo(80)
+ // ensure that paging source can handle invalid refresh key properly
+ // should load last page with items 65 - 80
+ assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(65, 80))
+
+ // should account for updated item count to return correct itemsBefore, itemsAfter,
+ // prevKey, nextKey
+ assertThat(result.itemsBefore).isEqualTo(65)
+ assertThat(result.itemsAfter).isEqualTo(0)
+ // no append can be triggered
+ assertThat(result.prevKey).isEqualTo(65)
+ assertThat(result.nextKey).isEqualTo(null)
+ }
+
/**
* Tests the behavior if user was viewing items in the top of the database and those items were
* deleted.
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
index d08a210..d700b6c 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -83,7 +83,7 @@
}
is Append -> key
is Refresh ->
- if (key >= itemCount) {
+ if (key >= itemCount - params.loadSize) {
maxOf(0, itemCount - params.loadSize)
} else {
key
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 108bd57..455fe21 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -95,9 +95,6 @@
/** Kernel component providing kernel version as VersionedSpl. */
public const val COMPONENT_KERNEL: String = "KERNEL"
- /** WebView component providing default WebView provider version as VersionedSpl. */
- internal const val COMPONENT_WEBVIEW: String = "WEBVIEW"
-
/**
* Vendor component providing ro.vendor.build.security_patch property value as DateBasedSpl.
*/
@@ -117,7 +114,6 @@
COMPONENT_SYSTEM_MODULES,
COMPONENT_KERNEL,
COMPONENT_VENDOR,
- COMPONENT_WEBVIEW,
]
)
internal annotation class Component
@@ -572,9 +568,6 @@
DateBasedSecurityPatchLevel.fromString(vendorSpl)
}
-
- // TODO(musashi): Add support for webview package
- COMPONENT_WEBVIEW -> TODO()
else -> throw IllegalArgumentException("Unknown component: $component")
}
}
@@ -614,9 +607,6 @@
)
}
COMPONENT_KERNEL -> getPublishedKernelVersions()
-
- // TODO(musashi): Add support for webview package
- COMPONENT_WEBVIEW -> TODO()
else -> throw IllegalArgumentException("Unknown component: $component")
}
}
@@ -740,8 +730,7 @@
// These components are expected to use DateBasedSpl
DateBasedSecurityPatchLevel.fromString(securityPatchLevel)
}
- COMPONENT_KERNEL,
- COMPONENT_WEBVIEW -> {
+ COMPONENT_KERNEL -> {
// These components are expected to use VersionedSpl
VersionedSecurityPatchLevel.fromString(securityPatchLevel)
}
@@ -767,13 +756,10 @@
COMPONENT_SYSTEM_MODULES,
COMPONENT_VENDOR,
COMPONENT_KERNEL,
- COMPONENT_WEBVIEW
)
components.forEach { component ->
if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) return@forEach
- // TODO(musashi): Unblock once support for WebView is present.
- if (component == COMPONENT_WEBVIEW) return@forEach
val deviceSpl =
try {
getDeviceSecurityPatchLevel(component)
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index 28a6576..440fd0f6 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -622,7 +622,7 @@
fun testGetPatchedCves_ThrowsExceptionForInvalidComponent() {
val spl = SecurityPatchState.DateBasedSecurityPatchLevel.fromString("2023-01-01")
- securityState.getPatchedCves(SecurityPatchState.COMPONENT_WEBVIEW, spl)
+ securityState.getPatchedCves(SecurityPatchState.COMPONENT_KERNEL, spl)
}
@Test
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
index 0c304d1..a80f168 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
@@ -34,7 +34,7 @@
import androidx.wear.compose.material3.DatePickerType
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+import androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
import androidx.wear.compose.material3.samples.DatePickerSample
import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
import java.time.LocalDate
@@ -47,7 +47,7 @@
ComposableDemo("Date Month-Day-Year") { DatePickerDemo(DatePickerType.MonthDayYear) },
ComposableDemo("Date Day-Month-Year") { DatePickerDemo(DatePickerType.DayMonthYear) },
ComposableDemo("Date System date format") { DatePickerSample() },
- ComposableDemo("Date Range") { DatePickerMinDateMaxDateSample() },
+ ComposableDemo("Future only") { DatePickerFutureOnlySample() },
ComposableDemo("Past only") { DatePickerPastOnlyDemo() },
)
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index a82e444..20ff2d3 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -40,7 +40,9 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.integration.demos.common.AdaptiveScreen
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Card
@@ -54,18 +56,21 @@
import androidx.wear.compose.material3.TextButtonDefaults
import androidx.wear.compose.material3.samples.icons.CheckIcon
+fun listOfLabels(): List<String> {
+ return listOf(
+ "Hi",
+ "Hello World",
+ "Hello world again?",
+ "More content as we add stuff",
+ "I don't know if this will fit now, testing",
+ "Really long text that it's going to take multiple lines",
+ "And now we are really pushing it because the screen is really small",
+ )
+}
+
@Composable
fun EdgeButtonBelowLazyColumnDemo() {
- val labels =
- listOf(
- "Hi",
- "Hello World",
- "Hello world again?",
- "More content as we add stuff",
- "I don't know if this will fit now, testing",
- "Really long text that it's going to take multiple lines",
- "And now we are really pushing it because the screen is really small",
- )
+ val labels = listOfLabels()
val selectedLabel = remember { mutableIntStateOf(0) }
AdaptiveScreen {
val state = rememberLazyListState()
@@ -104,16 +109,7 @@
@Composable
fun EdgeButtonBelowScalingLazyColumnDemo() {
- val labels =
- listOf(
- "Hi",
- "Hello World",
- "Hello world again?",
- "More content as we add stuff",
- "I don't know if this will fit now, testing",
- "Really long text that it's going to take multiple lines",
- "And now we are really pushing it because the screen is really small",
- )
+ val labels = listOfLabels()
val selectedLabel = remember { mutableIntStateOf(0) }
AdaptiveScreen {
@@ -151,6 +147,45 @@
}
}
+@Composable
+fun EdgeButtonBelowTransformingLazyColumnDemo() {
+ val labels = listOfLabels()
+ val selectedLabel = remember { mutableIntStateOf(0) }
+ AdaptiveScreen {
+ val state = rememberTransformingLazyColumnState()
+ ScreenScaffold(
+ scrollState = state,
+ contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
+ edgeButton = {
+ EdgeButton(
+ onClick = {},
+ buttonSize = EdgeButtonSize.Large,
+ colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
+ ) {
+ Text(labels[selectedLabel.intValue], color = Color.White)
+ }
+ }
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = state,
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ contentPadding = contentPadding,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ items(labels.size) {
+ Card(
+ onClick = { selectedLabel.intValue = it },
+ modifier = Modifier.fillMaxWidth(0.9f)
+ ) {
+ Text(labels[it])
+ }
+ }
+ }
+ }
+ }
+}
+
@Suppress("PrimitiveInCollection")
@Composable
fun EdgeButtonMultiDemo() {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index b4828a7..8a93dff 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -104,6 +104,9 @@
ComposableDemo("Edge Button Below SLC") {
EdgeButtonBelowScalingLazyColumnDemo()
},
+ ComposableDemo("Edge Button Below TLC") {
+ EdgeButtonBelowTransformingLazyColumnDemo()
+ },
)
),
Material3DemoCategory(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
index a412d5a..5cda3e5 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
@@ -17,13 +17,21 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
@@ -280,18 +288,39 @@
@Sampled
@Composable
fun OutlinedCompactButtonSample(modifier: Modifier = Modifier) {
- CompactButton(
- onClick = { /* Do something */ },
- colors = ButtonDefaults.outlinedButtonColors(),
- border = ButtonDefaults.outlinedButtonBorder(enabled = true),
- modifier = modifier,
- ) {
- Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
- Spacer(Modifier.width(ButtonDefaults.IconSpacing))
- Icon(
- Icons.Filled.ArrowDropDown,
- contentDescription = "Expand",
- modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
- )
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ var expanded by remember { mutableStateOf(false) }
+ if (expanded) {
+ Text("A multiline string showing two lines")
+ } else {
+ Text("One line text")
+ }
+ Spacer(Modifier.height(ButtonDefaults.IconSpacing))
+ CompactButton(
+ onClick = { expanded = !expanded },
+ colors = ButtonDefaults.outlinedButtonColors(),
+ border = ButtonDefaults.outlinedButtonBorder(enabled = true),
+ modifier = modifier,
+ ) {
+ if (expanded) {
+ Text("Show Less", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ } else {
+ Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ Spacer(Modifier.width(ButtonDefaults.IconSpacing))
+ if (expanded) {
+ Icon(
+ Icons.Filled.KeyboardArrowUp,
+ contentDescription = "Collapse",
+ modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
+ )
+ } else {
+ Icon(
+ Icons.Filled.KeyboardArrowDown,
+ contentDescription = "Expand",
+ modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
+ )
+ }
+ }
}
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
index 4c2ae26..6557f67 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
@@ -17,12 +17,8 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
@@ -33,7 +29,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.DatePicker
import androidx.wear.compose.material3.DatePickerType
@@ -106,14 +101,13 @@
@Sampled
@Composable
-fun DatePickerMinDateMaxDateSample() {
- var showDatePicker by remember { mutableStateOf(false) }
- var datePickerDate by remember { mutableStateOf(LocalDate.of(2024, 9, 2)) }
+fun DatePickerFutureOnlySample() {
+ val currentDate = LocalDate.now()
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
val formatter =
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(LocalConfiguration.current.locales[0])
- val minDate = LocalDate.of(2022, 10, 30)
- val maxDate = LocalDate.of(2025, 2, 4)
if (showDatePicker) {
DatePicker(
initialDate = datePickerDate, // Initialize with last picked date on reopen
@@ -121,18 +115,14 @@
datePickerDate = it
showDatePicker = false
},
- minValidDate = minDate,
- maxValidDate = maxDate,
- datePickerType = DatePickerType.YearMonthDay
+ datePickerType = DatePickerType.YearMonthDay,
+ minValidDate = currentDate
)
} else {
- Column(
+ Box(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
+ contentAlignment = Alignment.Center,
) {
- Text(text = "${minDate.format(formatter)} ~ ${maxDate.format(formatter)}")
- Spacer(modifier = Modifier.height(6.dp))
Button(
onClick = { showDatePicker = true },
label = { Text("Selected Date") },
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
index 0f074a7..e92bce3 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
@@ -35,7 +35,7 @@
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.wear.compose.material3.internal.Strings
-import androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+import androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
import androidx.wear.compose.material3.samples.DatePickerSample
import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
import com.google.common.truth.Truth.assertThat
@@ -70,7 +70,7 @@
rule.setContentWithTheme {
DatePickerSample()
DatePickerYearMonthDaySample()
- DatePickerMinDateMaxDateSample()
+ DatePickerFutureOnlySample()
}
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 5a3f11e..b3a9562 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -915,7 +915,8 @@
* @sample androidx.wear.compose.material3.samples.FilledTonalCompactButtonSample
*
* Example of a [CompactButton] with an icon and label and with
- * [ButtonDefaults.outlinedButtonBorder] and [ButtonDefaults.outlinedButtonColors]
+ * [ButtonDefaults.outlinedButtonBorder] and [ButtonDefaults.outlinedButtonColors]. The example
+ * includes a [Text] that expands and collapses when the [CompactButton] is clicked.
*
* @sample androidx.wear.compose.material3.samples.OutlinedCompactButtonSample
*
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
index af2130d..2879d30 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
@@ -84,9 +84,9 @@
*
* @sample androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
*
- * Example of a [DatePicker] with minDate and maxDate:
+ * Example of a [DatePicker] with a minDate:
*
- * @sample androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+ * @sample androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
* @param initialDate The initial value to be displayed in the DatePicker.
* @param onDatePicked The callback that is called when the user confirms the date selection. It
* provides the selected date as [LocalDate]
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index a38ede8..3298f37 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId = "androidx.wear.compose.integration.demos"
minSdk = 30
- versionCode = 62
- versionName = "1.62"
+ versionCode = 63
+ versionName = "1.63"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 4db7d12..8942afc 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -224,10 +224,6 @@
method public androidx.wear.protolayout.material3.GraphicDataCardStyle largeGraphicDataCardStyle();
}
- public final class HelpersKt {
- method public static androidx.wear.protolayout.types.LayoutColor withOpacity(androidx.wear.protolayout.types.LayoutColor, @FloatRange(from=0.0, to=1.0) float ratio);
- }
-
public final class IconButtonStyle {
field public static final androidx.wear.protolayout.material3.IconButtonStyle.Companion Companion;
}
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 4db7d12..8942afc 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -224,10 +224,6 @@
method public androidx.wear.protolayout.material3.GraphicDataCardStyle largeGraphicDataCardStyle();
}
- public final class HelpersKt {
- method public static androidx.wear.protolayout.types.LayoutColor withOpacity(androidx.wear.protolayout.types.LayoutColor, @FloatRange(from=0.0, to=1.0) float ratio);
- }
-
public final class IconButtonStyle {
field public static final androidx.wear.protolayout.material3.IconButtonStyle.Companion Companion;
}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index c5fce5f..951a192 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -62,7 +62,7 @@
* @param time An optional slot for displaying the time relevant to the contents of the card,
* expected to be a short piece of text. Uses [CardColors.timeColor] color by default.
* @param height The height of this card. It's highly recommended to set this to [expand] or
- * [weight]
+ * [weight].
* @param shape Defines the card's shape, in other words the corner radius for this card.
* @param colors The colors to be used for a background and inner content of this card. If the
* background image is also specified, the image will be laid out on top of the background color.
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index a95093c..8f080b0 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -116,7 +116,7 @@
* Note that this only looks at the static value of the [LayoutColor], any dynamic value will be
* ignored.
*/
-public fun LayoutColor.withOpacity(@FloatRange(from = 0.0, to = 1.0) ratio: Float): LayoutColor {
+internal fun LayoutColor.withOpacity(@FloatRange(from = 0.0, to = 1.0) ratio: Float): LayoutColor {
// From androidx.core.graphics.ColorUtils
require(!(ratio < 0 || ratio > 1)) { "setOpacityForColor ratio must be between 0 and 1." }
val fullyOpaque = 255
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
index d35b071..d77f5e3 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
@@ -26,7 +26,7 @@
import java.util.Objects
/**
- * Adds content description to be read by Talkback.
+ * Adds content description to be read by accessibility services.
*
* @param staticValue The static content description. This value will be used if [dynamicValue] is
* null, or if can't be resolved.
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 1eb7d18..0bfb95e 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -18,13 +18,11 @@
import android.annotation.SuppressLint;
import android.net.Uri;
-import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.core.os.CancellationSignal;
-import androidx.webkit.PrerenderException;
import androidx.webkit.PrerenderOperationCallback;
import androidx.webkit.Profile;
import androidx.webkit.SpeculativeLoadingParameters;
@@ -203,19 +201,6 @@
@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
@NonNull PrerenderOperationCallback callback) {
-
- ValueCallback<Void> activationCallback = (value) -> {
- // value will always be null.
- callback.onPrerenderActivated();
- };
- ValueCallback<Throwable> errorCallback = (throwable) -> {
- callback.onError(new PrerenderException("Prerender operation failed", throwable));
- };
- mImpl.prerenderUrl(
- url,
- cancellationSignal,
- activationCallback,
- errorCallback);
}
/**
@@ -228,22 +213,5 @@
@Nullable CancellationSignal cancellationSignal,
@NonNull SpeculativeLoadingParameters params,
@NonNull PrerenderOperationCallback callback) {
-
- InvocationHandler paramsBoundaryInterface =
- BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
- new SpeculativeLoadingParametersAdapter(params));
- ValueCallback<Void> activationCallback = (value) -> {
- // value will always be null.
- callback.onPrerenderActivated();
- };
- ValueCallback<Throwable> errorCallback = (throwable) -> {
- callback.onError(new PrerenderException("Prerender operation failed", throwable));
- };
- mImpl.prerenderUrl(
- url,
- cancellationSignal,
- paramsBoundaryInterface,
- activationCallback,
- errorCallback);
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
index 1dcfeee..d17ea2d 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -319,14 +319,47 @@
}
}
- /**
- * Tests the presentation flow on to a rear facing display works as expected. Similar to
- * [testPresentRearDisplayArea], but starts the presentation with a new instance of
- * [WindowAreaControllerImpl].
- */
@RequiresApi(Build.VERSION_CODES.Q)
@Test
- fun testPresentRearDisplayAreaWithNewController(): Unit =
+ fun testRearDisplayPresentationModeSessionEndedError(): Unit =
+ testScope.runTest {
+ assumeAtLeastVendorApiLevel(minVendorApiLevel)
+ val extensionComponent = FakeWindowAreaComponent()
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
+
+ extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
+ extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
+ val windowAreaInfo: WindowAreaInfo? =
+ async {
+ return@async controller.windowAreaInfos.first().firstOrNull {
+ it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
+ }
+ }
+ .await()
+
+ assertNotNull(windowAreaInfo)
+ assertTrue {
+ windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA).status ==
+ WINDOW_AREA_STATUS_UNAVAILABLE
+ }
+
+ val callback = TestWindowAreaPresentationSessionCallback()
+ activityScenario.scenario.onActivity { testActivity ->
+ controller.presentContentOnWindowArea(
+ windowAreaInfo.token,
+ testActivity,
+ Runnable::run,
+ callback
+ )
+ assert(!callback.sessionActive)
+ assert(callback.sessionError != null)
+ assert(callback.sessionError is IllegalStateException)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ @Test
+ fun testPresentContentWithNewControllerThrowsException(): Unit =
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
@@ -360,117 +393,7 @@
Runnable::run,
callback
)
- assert(callback.sessionActive)
- assert(!callback.contentVisible)
- callback.presentation?.setContentView(TextView(testActivity))
- assert(callback.contentVisible)
- assert(callback.sessionActive)
-
- callback.presentation?.close()
- assert(!callback.contentVisible)
- assert(!callback.sessionActive)
- }
- }
-
- /**
- * Tests the presentation flow on to a rear facing display works as expected. Similar to
- * [testTransferToRearFacingWindowArea], but starts the presentation with a new instance of
- * [WindowAreaControllerImpl].
- */
- @RequiresApi(Build.VERSION_CODES.Q)
- @Test
- fun testTransferToRearDisplayAreaWithNewController(): Unit =
- testScope.runTest {
- assumeAtLeastVendorApiLevel(minVendorApiLevel)
- val extensions = FakeWindowAreaComponent()
- val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
- extensions.currentRearDisplayStatus = STATUS_AVAILABLE
- val callback = TestWindowAreaSessionCallback()
- val windowAreaInfo =
- async {
- return@async controller.windowAreaInfos.first().firstOrNull {
- it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
- }
- }
- .await()
-
- assertNotNull(windowAreaInfo)
- assertEquals(
- windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA).status,
- WINDOW_AREA_STATUS_AVAILABLE
- )
-
- activityScenario.scenario.onActivity { testActivity ->
- testActivity.resetLayoutCounter()
- testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- testActivity.waitForLayout()
- }
-
- // Create a new controller to start the transfer.
- val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
-
- activityScenario.scenario.onActivity { testActivity ->
- assert(
- testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- )
- testActivity.resetLayoutCounter()
- controller2.transferActivityToWindowArea(
- windowAreaInfo.token,
- testActivity,
- Runnable::run,
- callback
- )
- }
-
- activityScenario.scenario.onActivity { testActivity ->
- assert(
- testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
- )
- assert(callback.currentSession != null)
- testActivity.resetLayoutCounter()
- callback.endSession()
- }
- activityScenario.scenario.onActivity { testActivity ->
- assert(
- testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- )
- assert(callback.currentSession == null)
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- @Test
- fun testRearDisplayPresentationModeSessionEndedError(): Unit =
- testScope.runTest {
- assumeAtLeastVendorApiLevel(minVendorApiLevel)
- val extensionComponent = FakeWindowAreaComponent()
- val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
-
- extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
- extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
- val windowAreaInfo: WindowAreaInfo? =
- async {
- return@async controller.windowAreaInfos.first().firstOrNull {
- it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
- }
- }
- .await()
-
- assertNotNull(windowAreaInfo)
- assertTrue {
- windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA).status ==
- WINDOW_AREA_STATUS_UNAVAILABLE
- }
-
- val callback = TestWindowAreaPresentationSessionCallback()
- activityScenario.scenario.onActivity { testActivity ->
- controller.presentContentOnWindowArea(
- windowAreaInfo.token,
- testActivity,
- Runnable::run,
- callback
- )
assert(!callback.sessionActive)
assert(callback.sessionError != null)
assert(callback.sessionError is IllegalStateException)
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
index 3b88357..04864cc 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -130,37 +130,44 @@
private val TAG = WindowAreaController::class.simpleName
private var decorator: WindowAreaControllerDecorator = EmptyDecorator
+ private var windowAreaController: WindowAreaController? = null
/** Provides an instance of [WindowAreaController]. */
@JvmName("getOrCreate")
@JvmStatic
fun getOrCreate(): WindowAreaController {
- val windowAreaComponentExtensions =
- try {
- this::class.java.classLoader?.let {
- SafeWindowAreaComponentProvider(it).windowAreaComponent
- }
- } catch (t: Throwable) {
- if (BuildConfig.verificationMode == VerificationMode.LOG) {
- Log.d(TAG, "Failed to load WindowExtensions")
- }
- null
- }
+ return if (windowAreaController != null) windowAreaController!!
+ else {
- val deviceSupported =
- Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
- windowAreaComponentExtensions != null &&
- ExtensionsUtil.safeVendorApiLevel >= 3
+ val windowAreaComponentExtensions =
+ try {
+ this::class.java.classLoader?.let {
+ SafeWindowAreaComponentProvider(it).windowAreaComponent
+ }
+ } catch (t: Throwable) {
+ if (BuildConfig.verificationMode == VerificationMode.LOG) {
+ Log.d(TAG, "Failed to load WindowExtensions")
+ }
+ null
+ }
- val controller =
- if (deviceSupported) {
- WindowAreaControllerImpl(
- windowAreaComponent = windowAreaComponentExtensions!!,
- )
- } else {
- EmptyWindowAreaControllerImpl()
- }
- return decorator.decorate(controller)
+ val deviceSupported =
+ Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
+ windowAreaComponentExtensions != null &&
+ ExtensionsUtil.safeVendorApiLevel >= 3
+
+ val controller =
+ if (deviceSupported) {
+ WindowAreaControllerImpl(
+ windowAreaComponent = windowAreaComponentExtensions!!,
+ )
+ } else {
+ EmptyWindowAreaControllerImpl()
+ }
+ val decoratedController = decorator.decorate(controller)
+ windowAreaController = decoratedController
+ decoratedController
+ }
}
@JvmStatic
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
index 445cb5d..5e97ca3 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -41,13 +41,9 @@
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.reflection.Consumer2
import java.util.concurrent.Executor
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
/**
* Implementation of WindowAreaController for devices that do implement the WindowAreaComponent on
@@ -208,18 +204,7 @@
return
}
- if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_UNKNOWN) {
- Log.d(TAG, "Force updating currentRearDisplayModeStatus")
- // currentRearDisplayModeStatus may be null if the client has not queried
- // WindowAreaController.windowAreaInfos using this instance. In this case, we query
- // it for a single value to force update currentRearDisplayModeStatus.
- CoroutineScope(executor.asCoroutineDispatcher()).launch {
- windowAreaInfos.first()
- startRearDisplayMode(activity, executor, windowAreaSessionCallback)
- }
- } else {
- startRearDisplayMode(activity, executor, windowAreaSessionCallback)
- }
+ startRearDisplayMode(activity, executor, windowAreaSessionCallback)
}
override fun presentContentOnWindowArea(
@@ -237,26 +222,7 @@
return
}
- if (currentRearDisplayPresentationStatus == WINDOW_AREA_STATUS_UNKNOWN) {
- Log.d(TAG, "Force updating currentRearDisplayPresentationStatus")
- // currentRearDisplayModeStatus may be null if the client has not queried
- // WindowAreaController.windowAreaInfos using this instance. In this case, we query
- // it for a single value to force update currentRearDisplayPresentationStatus.
- CoroutineScope(executor.asCoroutineDispatcher()).launch {
- windowAreaInfos.first()
- startRearDisplayPresentationMode(
- activity,
- executor,
- windowAreaPresentationSessionCallback
- )
- }
- } else {
- startRearDisplayPresentationMode(
- activity,
- executor,
- windowAreaPresentationSessionCallback
- )
- }
+ startRearDisplayPresentationMode(activity, executor, windowAreaPresentationSessionCallback)
}
private fun startRearDisplayMode(
diff --git a/xr/compose/material3/material3/api/current.txt b/xr/compose/material3/material3/api/current.txt
index 98fb6c4..0dc4412 100644
--- a/xr/compose/material3/material3/api/current.txt
+++ b/xr/compose/material3/material3/api/current.txt
@@ -39,6 +39,10 @@
property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.compose.material3.VerticalOrbiterProperties?> LocalNavigationRailOrbiterProperties;
}
+ public final class ThreePaneScaffoldKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void ThreePaneScaffold(androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder, kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane, optional kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane, kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane);
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public final class VerticalOrbiterProperties {
ctor public VerticalOrbiterProperties(androidx.xr.compose.spatial.EdgeOffset offset, int position, androidx.compose.ui.Alignment.Vertical alignment, androidx.xr.compose.spatial.OrbiterSettings settings, androidx.xr.compose.subspace.layout.SpatialShape shape);
method public androidx.xr.compose.material3.VerticalOrbiterProperties copy(optional androidx.xr.compose.spatial.EdgeOffset? offset, optional androidx.xr.compose.spatial.OrbiterEdge.Vertical? position, optional androidx.compose.ui.Alignment.Vertical? alignment, optional androidx.xr.compose.spatial.OrbiterSettings? settings, optional androidx.xr.compose.subspace.layout.SpatialShape? shape);
diff --git a/xr/compose/material3/material3/api/restricted_current.txt b/xr/compose/material3/material3/api/restricted_current.txt
index 98fb6c4..0dc4412 100644
--- a/xr/compose/material3/material3/api/restricted_current.txt
+++ b/xr/compose/material3/material3/api/restricted_current.txt
@@ -39,6 +39,10 @@
property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.compose.material3.VerticalOrbiterProperties?> LocalNavigationRailOrbiterProperties;
}
+ public final class ThreePaneScaffoldKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void ThreePaneScaffold(androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder, kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane, optional kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane, kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane);
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public final class VerticalOrbiterProperties {
ctor public VerticalOrbiterProperties(androidx.xr.compose.spatial.EdgeOffset offset, int position, androidx.compose.ui.Alignment.Vertical alignment, androidx.xr.compose.spatial.OrbiterSettings settings, androidx.xr.compose.subspace.layout.SpatialShape shape);
method public androidx.xr.compose.material3.VerticalOrbiterProperties copy(optional androidx.xr.compose.spatial.EdgeOffset? offset, optional androidx.xr.compose.spatial.OrbiterEdge.Vertical? position, optional androidx.compose.ui.Alignment.Vertical? alignment, optional androidx.xr.compose.spatial.OrbiterSettings? settings, optional androidx.xr.compose.subspace.layout.SpatialShape? shape);
diff --git a/xr/compose/material3/material3/build.gradle b/xr/compose/material3/material3/build.gradle
index abb0d4c..0cad526 100644
--- a/xr/compose/material3/material3/build.gradle
+++ b/xr/compose/material3/material3/build.gradle
@@ -36,6 +36,7 @@
dependencies {
api(libs.kotlinStdlib)
// Add dependencies here
+ implementation(project(":compose:ui:ui-unit"))
implementation(project(":compose:material3:adaptive:adaptive"))
implementation(project(":compose:material3:adaptive:adaptive-layout"))
implementation(project(":compose:material3:material3"))
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
index 6862955..4411de4 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
@@ -21,6 +21,7 @@
import androidx.compose.material3.LocalNavigationRailComponentOverride
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
import androidx.compose.material3.adaptive.layout.LocalAnimatedPaneOverride
+import androidx.compose.material3.adaptive.layout.LocalThreePaneScaffoldOverride
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidedValue
@@ -61,6 +62,7 @@
)
}
if (context.shouldOverrideComponent(XrComponentOverride.ThreePaneScaffold)) {
+ add(LocalThreePaneScaffoldOverride provides XrThreePaneScaffoldOverride)
add(LocalAnimatedPaneOverride provides XrAnimatedPaneOverride)
}
}
@@ -127,5 +129,10 @@
@Composable
override fun XrComponentOverrideEnablerContext.shouldOverrideComponent(
component: XrComponentOverride
- ): Boolean = isSpatializationEnabled
+ ): Boolean =
+ when (component) {
+ // TODO(b/388825260): Allow enabling ThreePaneScaffold once all edge-cases are fixed
+ XrComponentOverride.ThreePaneScaffold -> false
+ else -> isSpatializationEnabled
+ }
}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
new file mode 100644
index 0000000..ca1ae92
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.compose.material3
+
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
+import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
+import androidx.compose.material3.adaptive.layout.PaneScaffoldParentData
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideContext
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.spatial.Subspace
+import androidx.xr.compose.subspace.SpatialLayoutSpacer
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.fillMaxHeight
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.width
+import kotlin.math.roundToInt
+
+/**
+ * A pane scaffold composable that can display up to three panes in the order that
+ * [ThreePaneScaffoldHorizontalOrder] specifies, and allocate margins and spacers according to
+ * [PaneScaffoldDirective].
+ *
+ * [ThreePaneScaffold] is the base composable functions of adaptive programming. Developers can
+ * freely pipeline the relevant adaptive signals and use them as input of the scaffold function to
+ * render the final adaptive layout.
+ *
+ * @param modifier The modifier to be applied to the layout.
+ * @param scaffoldDirective The top-level directives about how the scaffold should arrange its
+ * panes.
+ * @param paneOrder The horizontal order of the panes from start to end in the scaffold.
+ * @param secondaryPane The content of the secondary pane that has a priority lower then the primary
+ * pane but higher than the tertiary pane.
+ * @param tertiaryPane The content of the tertiary pane that has the lowest priority.
+ * @param primaryPane The content of the primary pane that has the highest priority.
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@ExperimentalMaterial3XrApi
+@Composable
+public fun ThreePaneScaffold(
+ modifier: SubspaceModifier,
+ scaffoldDirective: PaneScaffoldDirective,
+ paneOrder: ThreePaneScaffoldHorizontalOrder,
+ secondaryPane: @Composable () -> Unit,
+ tertiaryPane: (@Composable () -> Unit)? = null,
+ primaryPane: @Composable () -> Unit
+) {
+ Subspace {
+ SpatialRow(modifier = modifier.height(XrThreePaneScaffoldTokens.PanelHeight)) {
+ var drawSpacer = false // Only draws spacers after the first pane is drawn
+ paneOrder.each { role ->
+ when (role) {
+ ThreePaneScaffoldRole.Primary -> {
+ Panel(
+ scaffoldDirective,
+ XrThreePaneScaffoldTokens.PrimaryPanePanelWidth,
+ drawSpacer,
+ primaryPane
+ )
+ drawSpacer = true
+ }
+ ThreePaneScaffoldRole.Secondary -> {
+ Panel(
+ scaffoldDirective,
+ XrThreePaneScaffoldTokens.SecondaryPanePanelWidth,
+ drawSpacer,
+ secondaryPane
+ )
+ drawSpacer = true
+ }
+ ThreePaneScaffoldRole.Tertiary ->
+ if (tertiaryPane != null) {
+ Panel(
+ scaffoldDirective,
+ XrThreePaneScaffoldTokens.TertiaryPanePanelWidth,
+ drawSpacer,
+ tertiaryPane
+ )
+ drawSpacer = true
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+private fun Panel(
+ scaffoldDirective: PaneScaffoldDirective,
+ defaultPreferredWidth: Dp,
+ drawSpacer: Boolean,
+ content: @Composable () -> Unit
+) {
+ if (drawSpacer) {
+ SpatialLayoutSpacer(SubspaceModifier.width(scaffoldDirective.horizontalPartitionSpacerSize))
+ }
+
+ SpatialPanel(SubspaceModifier.width(defaultPreferredWidth).fillMaxHeight()) {
+ Layout(content) { measurables, constraints ->
+ val measurable = measurables[0]
+ val parentData = measurable.parentData as? PaneScaffoldParentData
+ val widthFloat = parentData?.preferredWidth ?: defaultPreferredWidth
+ val width = widthFloat.toPx().roundToInt()
+ val height = constraints.maxHeight
+ return@Layout layout(width, height) {
+ measurable.measure(Constraints.fixed(width, height)).place(0, 0)
+ }
+ }
+ }
+}
+
+/**
+ * [ThreePaneScaffoldOverride] that uses the XR-specific [ThreePaneScaffold].
+ *
+ * Note that when using this override, any madifiers passed in to the 2D composable are ignored.
+ */
+@ExperimentalMaterial3XrApi
+@OptIn(
+ ExperimentalMaterial3AdaptiveApi::class,
+ ExperimentalMaterial3AdaptiveComponentOverrideApi::class
+)
+internal object XrThreePaneScaffoldOverride : ThreePaneScaffoldOverride {
+ @Composable
+ override fun ThreePaneScaffoldOverrideContext.ThreePaneScaffold() {
+ ThreePaneScaffold(
+ modifier = SubspaceModifier,
+ scaffoldDirective = scaffoldDirective.copy(maxHorizontalPartitions = 3),
+ paneOrder = paneOrder,
+ primaryPane = primaryPane,
+ secondaryPane = secondaryPane,
+ tertiaryPane = tertiaryPane,
+ )
+ }
+}
+
+// TODO(conradchen): Confirm the values with design
+private object XrThreePaneScaffoldTokens {
+ val PanelHeight = 1024.dp
+ val PrimaryPanePanelWidth = 800.dp
+ val SecondaryPanePanelWidth = 412.dp
+ val TertiaryPanePanelWidth = 412.dp
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private inline fun ThreePaneScaffoldHorizontalOrder.each(action: (ThreePaneScaffoldRole) -> Unit) {
+ action(get(0))
+ action(get(1))
+ action(get(2))
+}