Merge "Properly set JavaCompile jvmTarget for KMP libraries using withJava" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 9fc459b..a5dda1a 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -18,7 +18,7 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.testing.TestNavigationEventCallback
import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ActivityScenario
@@ -573,9 +573,9 @@
val callback = CountingOnBackPressedCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.eventDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = DirectNavigationEventInput()
+ dispatcher.eventDispatcher.addInput(input)
+ input.handleOnCompleted()
assertWithMessage("Count should be incremented after dispatchOnCompleted")
.that(callback.count)
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt b/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
index dd1a301..4eb5c69 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
@@ -88,7 +88,7 @@
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEventDispatcher
import androidx.navigationevent.NavigationEventDispatcherOwner
import androidx.navigationevent.setViewTreeNavigationEventDispatcherOwner
@@ -241,12 +241,12 @@
private var dispatchingOnMultiWindowModeChanged = false
private var dispatchingOnPictureInPictureModeChanged = false
- // Input from `ComponentActivity.onBackPressed()`, which can get called when API < 33 or
+ // Inputs from `ComponentActivity.onBackPressed()`, which can get called when API < 33 or
// when `android:enableOnBackInvokedCallback` is `false`.
- private val onBackPressedInputHandler: DirectNavigationEventInputHandler by lazy {
- val inputHandler = DirectNavigationEventInputHandler()
- navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler
+ private val onBackPressedInput: DirectNavigationEventInput by lazy {
+ val input = DirectNavigationEventInput()
+ navigationEventDispatcher.addInput(input)
+ input
}
/**
@@ -595,7 +595,7 @@
to one or more {@link OnBackPressedCallback} objects."""
)
override fun onBackPressed() {
- onBackPressedInputHandler.handleOnCompleted()
+ onBackPressedInput.handleOnCompleted()
}
/**
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
index 5ecc60d..6ab4f9b 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
@@ -28,7 +28,7 @@
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEventDispatcher
import androidx.navigationevent.NavigationEventDispatcherOwner
import androidx.navigationevent.setViewTreeNavigationEventDispatcherOwner
@@ -61,10 +61,10 @@
// Input from for `ComponentDialog.onBackPressed()`, which can get called when API < 33 or
// when `android:enableOnBackInvokedCallback` is `false`.
- private val onBackPressedInputHandler: DirectNavigationEventInputHandler by lazy {
- val inputHandler = DirectNavigationEventInputHandler()
- navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler
+ private val onBackPressedInput: DirectNavigationEventInput by lazy {
+ val input = DirectNavigationEventInput()
+ navigationEventDispatcher.addInput(input)
+ input
}
override fun onSaveInstanceState(): Bundle {
@@ -125,7 +125,7 @@
to one or more {@link OnBackPressedCallback} objects."""
)
override fun onBackPressed() {
- onBackPressedInputHandler.handleOnCompleted()
+ onBackPressedInput.handleOnCompleted()
}
override fun setContentView(layoutResID: Int) {
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
index 4b217af..483b56a 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
@@ -24,11 +24,11 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEventCallback
import androidx.navigationevent.NavigationEventDispatcher
-import androidx.navigationevent.NavigationEventInputHandler
-import androidx.navigationevent.OnBackInvokedInputHandler
+import androidx.navigationevent.NavigationEventInput
+import androidx.navigationevent.OnBackInvokedInput
/**
* Dispatcher that can be used to register [OnBackPressedCallback] instances for handling the
@@ -79,8 +79,8 @@
// This is to implement `OnBackPressedDispatcher.onHasEnabledCallbacksChanged`, which
// can be set through OnBackPressedDispatcher's public constructor.
onHasEnabledCallbacksChanged?.let { callback ->
- dispatcher.addInputHandler(
- object : NavigationEventInputHandler() {
+ dispatcher.addInput(
+ object : NavigationEventInput() {
override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
callback.accept(hasEnabledCallbacks)
}
@@ -90,10 +90,10 @@
dispatcher
}
- private val manualDispatchInputHandler by lazy {
- val inputHandler = DirectNavigationEventInputHandler()
- eventDispatcher.addInputHandler(inputHandler)
- inputHandler
+ private val directInput by lazy {
+ val input = DirectNavigationEventInput()
+ eventDispatcher.addInput(input)
+ input
}
@JvmOverloads
@@ -106,8 +106,8 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) {
- val inputHandler = OnBackInvokedInputHandler(invoker)
- eventDispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ eventDispatcher.addInput(input)
}
/**
@@ -214,13 +214,13 @@
@VisibleForTesting
@MainThread
fun dispatchOnBackStarted(backEvent: BackEventCompat) {
- manualDispatchInputHandler.handleOnStarted(backEvent.toNavigationEvent())
+ directInput.handleOnStarted(backEvent.toNavigationEvent())
}
@VisibleForTesting
@MainThread
fun dispatchOnBackProgressed(backEvent: BackEventCompat) {
- manualDispatchInputHandler.handleOnProgressed(backEvent.toNavigationEvent())
+ directInput.handleOnProgressed(backEvent.toNavigationEvent())
}
/**
@@ -233,13 +233,13 @@
*/
@MainThread
fun onBackPressed() {
- manualDispatchInputHandler.handleOnCompleted()
+ directInput.handleOnCompleted()
}
@VisibleForTesting
@MainThread
fun dispatchOnBackCancelled() {
- manualDispatchInputHandler.handleOnCancelled()
+ directInput.handleOnCancelled()
}
}
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 2f3c84a..9b0c083 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -39,25 +39,9 @@
dependsOn(commonMain)
}
- wasmJsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- jsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType !in [KotlinPlatformType.jvm, KotlinPlatformType.common]) {
- target.compilations["main"].defaultSourceSet {
- dependsOn(nonJvmMain)
- }
+ target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
}
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
index dcf6837..6dddf79 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
@@ -35,6 +35,8 @@
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName
+import kotlin.String
+import kotlin.text.ifEmpty
// TODO(b/410764334): Re-evaluate the abstraction layer.
/** Represents a class annotated with [androidx.appfunctions.AppFunctionSerializable]. */
@@ -110,8 +112,8 @@
) ?: false
}
- /** A description of the AppFunctionSerializable class and its intended use. */
- val description: String by lazy {
+ /** The docstring of the annotated class. */
+ internal val docstring: String by lazy {
if (isDescribedByKdoc) {
appFunctionSerializableClass.docString.orEmpty()
} else {
@@ -119,6 +121,11 @@
}
}
+ /** A description of the AppFunctionSerializable class and its intended use. */
+ open fun getDescription(sharedDataTypeDescriptionMap: Map<String, String> = mapOf()): String {
+ return docstring.ifEmpty { sharedDataTypeDescriptionMap[jvmQualifiedName] ?: "" }
+ }
+
/** All the [KSDeclaration] from the AppFunctionSerializable. */
val declarations: Sequence<KSDeclaration> by lazy { appFunctionSerializableClass.declarations }
@@ -255,7 +262,9 @@
}
/** Returns the annotated class's properties as defined in its primary constructor. */
- open fun getProperties(): List<AppFunctionPropertyDeclaration> {
+ open fun getProperties(
+ sharedDataTypeDescriptionMap: Map<String, String> = emptyMap()
+ ): List<AppFunctionPropertyDeclaration> {
val primaryConstructorProperties =
checkNotNull(appFunctionSerializableClass.primaryConstructor).parameters
@@ -267,9 +276,10 @@
return primaryConstructorProperties.mapNotNull { valueParameter ->
allProperties[valueParameter.name?.asString()]?.let {
AppFunctionPropertyDeclaration(
- it,
- isDescribedByKdoc,
+ property = it,
+ isDescribedByKdoc = isDescribedByKdoc,
isRequired = !valueParameter.hasDefault,
+ sharedDataTypeDescriptionMap = sharedDataTypeDescriptionMap,
)
}
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
index 6ea5de1..0b614a0 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
@@ -30,7 +30,9 @@
return this
}
- override fun getProperties(): List<AppFunctionPropertyDeclaration> {
+ override fun getProperties(
+ sharedDataTypeDescriptionMap: Map<String, String>
+ ): List<AppFunctionPropertyDeclaration> {
return classDeclaration
.getAllProperties()
.map {
@@ -40,6 +42,7 @@
// Property from interface is always required as there is no existing API
// to tell if the interface property has default value or not.
isRequired = true,
+ sharedDataTypeDescriptionMap,
)
}
.toList()
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
index da771c6..51711e3 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
@@ -76,7 +76,7 @@
* The JVM qualified name of the parametrized class being annotated with
* AppFunctionSerializable, without the parameterized type information
*/
- val unparameterizedJvmQualifiedName: String by lazy { super.jvmQualifiedName }
+ private val unparameterizedJvmQualifiedName: String by lazy { super.jvmQualifiedName }
override val factoryVariableName: String by lazy {
val variableName = jvmClassName.replace("$", "").replaceFirstChar { it -> it.lowercase() }
@@ -92,14 +92,22 @@
"${variableName}${typeArgumentSuffix}Factory"
}
+ override fun getDescription(sharedDataTypeDescriptionMap: Map<String, String>): String {
+ return docstring.ifEmpty {
+ sharedDataTypeDescriptionMap[unparameterizedJvmQualifiedName] ?: ""
+ }
+ }
+
/**
* Returns the annotated class's properties as defined in its primary constructor.
*
* When the property is generic type, it will try to resolve the actual type reference from
* [arguments].
*/
- override fun getProperties(): List<AppFunctionPropertyDeclaration> {
- return super.getProperties().map { propertyDeclaration ->
+ override fun getProperties(
+ sharedDataTypeDescriptionMap: Map<String, String>
+ ): List<AppFunctionPropertyDeclaration> {
+ return super.getProperties(sharedDataTypeDescriptionMap).map { propertyDeclaration ->
val valueTypeDeclaration = propertyDeclaration.type.resolve().declaration
if (valueTypeDeclaration is KSTypeParameter) {
val actualType =
@@ -114,6 +122,7 @@
description = propertyDeclaration.description,
isRequired = propertyDeclaration.isRequired,
propertyAnnotations = propertyDeclaration.propertyAnnotations,
+ qualifiedName = propertyDeclaration.qualifiedName,
)
} else {
propertyDeclaration
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
index 483ccf3..f840b4c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
@@ -270,7 +270,7 @@
addSerializableTypeMetadataToSharedDataTypeMap(
annotatedAppFunctionSerializable,
annotatedAppFunctionSerializable
- .getProperties()
+ .getProperties(sharedDataTypeDescriptionMap)
.associateBy { checkNotNull(it.name).toString() }
.toMutableMap(),
sharedDataTypeMap,
@@ -294,7 +294,7 @@
addSerializableTypeMetadataToSharedDataTypeMap(
annotatedAppFunctionSerializable,
annotatedAppFunctionSerializable
- .getProperties()
+ .getProperties(sharedDataTypeDescriptionMap)
.associateBy { checkNotNull(it.name).toString() }
.toMutableMap(),
sharedDataTypeMap,
@@ -323,7 +323,7 @@
addSerializableTypeMetadataToSharedDataTypeMap(
targetSerializableProxy,
targetSerializableProxy
- .getProperties()
+ .getProperties(sharedDataTypeDescriptionMap)
.associateBy { checkNotNull(it.name).toString() }
.toMutableMap(),
sharedDataTypeMap,
@@ -349,7 +349,7 @@
addSerializableTypeMetadataToSharedDataTypeMap(
targetSerializableProxy,
targetSerializableProxy
- .getProperties()
+ .getProperties(sharedDataTypeDescriptionMap)
.associateBy { checkNotNull(it.name).toString() }
.toMutableMap(),
sharedDataTypeMap,
@@ -419,14 +419,7 @@
seenDataTypeQualifiers.add(serializableTypeQualifiedName)
val serializableDescription =
- when {
- appFunctionSerializableType.description.isNotEmpty() ->
- appFunctionSerializableType.description
- appFunctionSerializableType is AnnotatedParameterizedAppFunctionSerializable ->
- sharedDataTypeDescriptionMap[
- appFunctionSerializableType.unparameterizedJvmQualifiedName] ?: ""
- else -> sharedDataTypeDescriptionMap[serializableTypeQualifiedName] ?: ""
- }
+ appFunctionSerializableType.getDescription(sharedDataTypeDescriptionMap)
val superTypesWithSerializableAnnotation =
appFunctionSerializableType.findSuperTypesWithSerializableAnnotation()
@@ -441,7 +434,7 @@
serializableTypeQualifiedName,
buildObjectTypeMetadataForObjectParameters(
serializableTypeQualifiedName,
- appFunctionSerializableType.getProperties(),
+ appFunctionSerializableType.getProperties(sharedDataTypeDescriptionMap),
unvisitedSerializableProperties,
sharedDataTypeMap,
seenDataTypeQualifiers,
@@ -492,6 +485,7 @@
// no existing API to tell if the interface property has
// default value or not.
isRequired = true,
+ sharedDataTypeDescriptionMap,
)
}
.toList(),
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
index 7857a16..7ed061c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
@@ -29,22 +29,27 @@
val description: String,
val isRequired: Boolean,
val propertyAnnotations: Sequence<KSAnnotation> = emptySequence(),
+ val qualifiedName: String,
) {
/** Creates an [AppFunctionPropertyDeclaration] from [KSPropertyDeclaration]. */
constructor(
property: KSPropertyDeclaration,
isDescribedByKdoc: Boolean,
isRequired: Boolean,
+ sharedDataTypeDescriptionMap: Map<String, String>,
) : this(
checkNotNull(property.simpleName).asString(),
property.type,
if (isDescribedByKdoc) {
- property.docString.orEmpty()
+ property.docString?.ifEmpty {
+ sharedDataTypeDescriptionMap[property.getQualifiedName()]
+ } ?: ""
} else {
""
},
isRequired,
property.annotations,
+ property.getQualifiedName(),
)
/** Indicates whether the [type] is a generic type or not. */
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
index 87a83c68..9993a166c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
@@ -21,6 +21,7 @@
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSName
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
@@ -223,6 +224,14 @@
return resolve().toTypeName(args)
}
+fun KSPropertyDeclaration.getQualifiedName(): String {
+ val qualifier =
+ qualifiedName?.getQualifier()
+ ?: throw ProcessingException("Unable to resolve the qualified name", this)
+ val simpleName = simpleName.asString()
+ return "$qualifier#$simpleName"
+}
+
internal fun TypeName.ignoreNullable(): TypeName {
return copy(nullable = false)
}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
index 4871068..458d8c0 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
@@ -107,7 +107,7 @@
qualifiedName = annotatedSerializable.jvmQualifiedName,
docString =
if (annotatedSerializable.isDescribedByKdoc) {
- annotatedSerializable.description
+ annotatedSerializable.getDescription()
} else {
""
},
@@ -116,8 +116,7 @@
for (property in annotatedSerializable.getProperties()) {
add(
AppFunctionComponent(
- qualifiedName =
- "${annotatedSerializable.jvmQualifiedName}#${property.name}",
+ qualifiedName = property.qualifiedName,
docString = property.description,
)
)
diff --git a/appfunctions/appfunctions-testing/build.gradle b/appfunctions/appfunctions-testing/build.gradle
index 2bd3a6f..89fec6c 100644
--- a/appfunctions/appfunctions-testing/build.gradle
+++ b/appfunctions/appfunctions-testing/build.gradle
@@ -36,6 +36,7 @@
implementation(project(":appfunctions:appfunctions"))
implementation(project(":appfunctions:appfunctions-service"))
implementation("junit:junit:4.13.2")
+ implementation(libs.robolectric)
// Internal dependencies
implementation("androidx.annotation:annotation:1.9.0")
diff --git a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
index 864542d..8bfc485 100644
--- a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
+++ b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
@@ -26,6 +26,7 @@
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+import org.robolectric.shadows.ShadowSystemProperties
/**
* A JUnit TestRule for setting up an environment to exercise AppFunction APIs in unit or
@@ -160,6 +161,10 @@
object : Statement() {
override fun evaluate() {
base?.evaluate()
+ // Robolectric platform doesn't set these properties, we have checks for certain
+ // AppSearch features that are only available if the sdk extensions for T are above
+ // 13.
+ ShadowSystemProperties.override(T_EXTENSION_PROPERTY_STRING, "13")
}
}
@@ -175,4 +180,8 @@
translatorSelector = NullTranslatorSelector(),
)
}
+
+ private companion object {
+ private const val T_EXTENSION_PROPERTY_STRING = "build.version.extensions.t"
+ }
}
diff --git a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
index e4c3dae..e540c5f 100644
--- a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
+++ b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
@@ -172,18 +172,23 @@
}
/**
- * Observes for available app functions metadata based on the provided filters.
+ * Observes available app functions metadata based on the provided filters.
*
* Allows discovering app functions that match the given [searchSpec] criteria and continuously
- * emits updates when relevant metadata changes. The calling app can only observe metadata for
- * functions in packages that it is allowed to query via
- * [android.content.pm.PackageManager.canPackageQuery]. If a package is not queryable by the
- * calling app, its functions' metadata will not be visible.
+ * emits updates when relevant metadata changes.
*
* Updates to [AppFunctionPackageMetadata] can occur when the app defining the function is
- * updated or when a function's enabled state changes.
+ * updated or when a function's enabled state changes, and if multiple updates happen within a
+ * short duration, only the latest update might be emitted.
*
- * If multiple updates happen within a short duration, only the latest update might be emitted.
+ * The calling app can observe metadata for:
+ * - Functions in its own package (no permission required).
+ * - Functions in other packages that it is allowed to query via
+ * [android.content.pm.PackageManager.canPackageQuery] and when holding the
+ * `android.permission.EXECUTE_APP_FUNCTIONS` permission.
+ *
+ * If a package is not queryable by the calling app, its functions' metadata will not be
+ * visible, even when holding the `android.permission.EXECUTE_APP_FUNCTIONS` permission.
*
* @param searchSpec an [AppFunctionSearchSpec] instance specifying the filters for searching
* the app function metadata.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 702e668..9f491fc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -561,7 +561,6 @@
private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) {
project.extensions.getByType<ApplicationExtension>().apply {
configureAndroidBaseOptions(project, androidXExtension)
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk
val debugSigningConfig = signingConfigs.getByName("debug")
// Use a local debug keystore to avoid build server issues.
@@ -598,7 +597,6 @@
private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
project.extensions.getByType<TestExtension>().apply {
configureAndroidBaseOptions(project, androidXExtension)
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk
val debugSigningConfig = signingConfigs.getByName("debug")
// Use a local debug keystore to avoid build server issues.
@@ -885,7 +883,6 @@
libraryAndroidComponentsExtension.apply {
finalizeDsl {
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
it.defaultConfig.aarMetadata.configure(it.compileSdk)
it.lint.targetSdk = project.defaultAndroidConfig.targetSdk
it.testOptions.targetSdk = project.defaultAndroidConfig.targetSdk
@@ -1054,19 +1051,16 @@
// Suppress output of android:compileSdkVersion and related attributes (b/277836549).
androidResources.additionalParameters += "--no-compile-sdk-metadata"
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
compileSdk = project.defaultAndroidConfig.compileSdk
buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion
defaultConfig.ndk.abiFilters.addAll(SUPPORTED_BUILD_ABIS)
- @Suppress("DEPRECATION") // TODO(aurimas): migrate to new API
defaultConfig.minSdk = defaultMinSdk
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = !project.isMacrobenchmark()
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
project.afterEvaluate {
check(
!androidXExtension.shouldPublish() || !compileOptions.isCoreLibraryDesugaringEnabled
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index 4a4adfc..e65cbf3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -748,6 +748,10 @@
configureDefaultIncrementalSyncTask()
configureKotlinJsTests()
configureNode()
+
+ // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same:
+ // https://youtrack.jetbrains.com/issue/KT-71032
+ configurePinnedKotlinLibraries(platform)
}
} else null
}
@@ -836,6 +840,9 @@
@OptIn(ExperimentalWasmDsl::class)
private fun Project.configureBinaryen() {
+ if (ProjectLayoutType.isPlayground(project)) {
+ return
+ }
plugins.withType<BinaryenPlugin>().configureEach {
the<BinaryenEnvSpec>()
.downloadBaseUrl
@@ -847,6 +854,25 @@
}
}
+private fun Project.configurePinnedKotlinLibraries(platform: PlatformIdentifier) {
+ multiplatformExtension?.let {
+ val kotlinLibSuffix =
+ when (platform) {
+ PlatformIdentifier.JS -> "js"
+ PlatformIdentifier.WASM_JS -> "wasm-js"
+ else -> throw IllegalStateException("Unsupported platform: $platform")
+ }
+ val kotlinVersion = project.getVersionByName("kotlin")
+ it.sourceSets.getByName("${platform.id}Main").dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+ }
+ it.sourceSets.getByName("${platform.id}Test").dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+ implementation("org.jetbrains.kotlin:kotlin-test-$kotlinLibSuffix:$kotlinVersion")
+ }
+ }
+}
+
private fun Project.configureKotlinJsTests() {
tasks.withType(KotlinJsTest::class.java).configureEach { task ->
if (!ProjectLayoutType.isPlayground(this)) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt b/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
index 3087d26..ad61749 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
@@ -29,7 +29,7 @@
*/
internal fun Project.fetchDevelocityKeysIfNeeded() {
// We are in CI, so we should not fetch these keys
- if (System.getenv("BUILD_NUMBER") != null) return
+ if (System.getenv("IS_ANDROIDX_CI") != null) return
// User does not have remote cache enabled, so we will not have access to GCP
if (System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE") !in setOf("gcp", "true")) return
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index b9742ea..ff4176e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -170,7 +170,6 @@
val addStubAar = isKmpAnchor && pomPlatform == PlatformIdentifier.ANDROID.id
val buildDir = project.layout.buildDirectory
if (addStubAar) {
- @Suppress("DEPRECATION") // TODO(aurimas): migrate to new API
val minSdk =
project.extensions.findByType<LibraryExtension>()?.defaultConfig?.minSdk
?: extensions
@@ -242,14 +241,18 @@
}
}
+ val buildIdProvider = project.providers.getBuildId()
// Workaround for https://github.com/gradle/gradle/issues/31218
project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
task.doLast {
- val metadata = task.outputFile.asFile.get()
- val text = metadata.readText()
- metadata.writeText(
- text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${getBuildId()}\"")
- )
+ if (buildIdProvider.isPresent) {
+ val buildId = buildIdProvider.get()
+ val metadata = task.outputFile.asFile.get()
+ val text = metadata.readText()
+ metadata.writeText(
+ text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${buildId}\"")
+ )
+ }
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index bb337f0..9acdb5a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -104,7 +104,6 @@
when (plugin) {
is LibraryPlugin -> {
val libraryExtension = project.extensions.getByType<LibraryExtension>()
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
libraryExtension.compileSdk =
project.defaultAndroidConfig.latestStableCompileSdk
libraryExtension.buildToolsVersion =
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 98d624e..6a2850f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -162,7 +162,7 @@
task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
task.outputXml.set(getFileInTestConfigDirectory(xmlName))
jsonName?.let { task.outputJson.set(getFileInTestConfigDirectory(it)) }
- task.presubmit.set(isPresubmitBuild())
+ task.presubmit.set(project.providers.isPresubmitBuild())
task.instrumentationArgs.putAll(instrumentationRunnerArgs)
task.minSdk.set(minSdk)
task.hasBenchmarkPlugin.set(hasBenchmarkPlugin())
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
index 4b8281b..aa2a200 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
@@ -22,6 +22,7 @@
import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
+import org.gradle.api.provider.ProviderFactory
/**
* @return build id string for current build
@@ -29,13 +30,7 @@
* The build server does not pass the build id so we infer it from the last folder of the
* distribution directory name.
*/
-fun getBuildId(): String {
- return if (System.getenv("BUILD_NUMBER") != null) {
- System.getenv("BUILD_NUMBER")
- } else {
- "0"
- }
-}
+fun ProviderFactory.getBuildId(): Provider<String> = environmentVariable("BUILD_NUMBER")
/**
* Gets set to true when the build id is prefixed with P.
@@ -43,12 +38,8 @@
* In AffectedModuleDetector, we return a different ProjectSubset in presubmit vs. postsubmit, to
* get the desired test behaviors.
*/
-fun isPresubmitBuild(): Boolean {
- return if (System.getenv("BUILD_NUMBER") != null) {
- System.getenv("BUILD_NUMBER").startsWith("P")
- } else {
- false
- }
+fun ProviderFactory.isPresubmitBuild(): Provider<Boolean> {
+ return environmentVariable("BUILD_NUMBER").map { it.startsWith("P") }.orElse(false)
}
/**
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index dfdb0b9..674243b 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -32,6 +32,7 @@
IS_POSTSUBMIT=false
fi
export IS_POSTSUBMIT
+export IS_ANDROIDX_CI=true
# parse arguments
if [ "$1" == "--diagnose" ]; then
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 53b2910..fe95b3e 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -75,7 +75,7 @@
testImplementation("org.robolectric:shadowapi:4.14")
testImplementation("org.robolectric:shadows-framework:4.14")
testImplementation(libs.guavaAndroid)
- testImplementation(libs.kotlinTestForWasmTests)
+ testImplementation(libs.kotlinTest)
testImplementation(libs.testMonitor)
androidTestImplementation(libs.testExtJunit)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
index 6da1081..bfb6e9e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
@@ -21,9 +21,7 @@
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.params.InputConfiguration
import android.hardware.camera2.params.StreamConfigurationMap
-import android.os.Build
import android.util.Size
-import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsPrivateReprocessing
import androidx.camera.camera2.pipe.core.Log
@@ -111,7 +109,6 @@
public fun dequeueImageFromBuffer(): ImageProxy?
}
-@RequiresApi(Build.VERSION_CODES.M)
@CameraScope
public class ZslControlImpl @Inject constructor(private val cameraProperties: CameraProperties) :
ZslControl {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
index 45c4ac5..adab96f3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
@@ -52,14 +52,6 @@
// before we use this workaround to finalize the capture session, and thereby
// releasing the Surfaces.
FinalizeSessionOnCloseBehavior.IMMEDIATE
- } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- // When CloseCaptureSessionOnVideoQuirk is enabled, we close the capture session
- // in anticipation that the onClosed() callback would finalize the session. However,
- // on API levels < M, it could be possible that onClosed() isn't invoked if a new
- // capture session (or CameraGraph) is created too soon (read b/144817309 or
- // CaptureSessionOnClosedNotCalledQuirk for more context). Therefore, we're enabling
- // this quirk (on a timeout) for API levels < M, too.
- FinalizeSessionOnCloseBehavior.TIMEOUT
} else {
FinalizeSessionOnCloseBehavior.OFF
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
index 68e5959..128593c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -18,7 +18,6 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.params.StreamConfigurationMap
-import android.os.Build
import androidx.annotation.Nullable
import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.pipe.CameraId
@@ -32,7 +31,6 @@
import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
import androidx.camera.camera2.pipe.integration.adapter.ZslControl
import androidx.camera.camera2.pipe.integration.adapter.ZslControlImpl
-import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
import androidx.camera.camera2.pipe.integration.compat.CameraCompatModule
import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
@@ -163,11 +161,7 @@
@CameraScope
@Provides
public fun provideZslControl(cameraProperties: CameraProperties): ZslControl {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return ZslControlImpl(cameraProperties)
- } else {
- return ZslControlNoOpImpl()
- }
+ return ZslControlImpl(cameraProperties)
}
@CameraScope
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt
index c324c9d..efa5d25 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt
@@ -162,24 +162,8 @@
captureSessionId: CameraInterop.CameraCaptureSessionId,
surface: Surface,
) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Api23CompatImpl.onSurfacePrepared(placeholderSession, surface, callbacks)
- } else {
- Log.error { "onSurfacePrepared called for unsupported OS version." }
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- private object Api23CompatImpl {
- @JvmStatic
- fun onSurfacePrepared(
- session: CameraCaptureSession,
- surface: Surface,
- callbacks: AtomicRef<List<CameraCaptureSession.StateCallback>>,
- ) {
- for (callback in callbacks.value) {
- callback.onSurfacePrepared(session, surface)
- }
+ for (callback in callbacks.value) {
+ callback.onSurfacePrepared(placeholderSession, surface)
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 6d52745..2ca912c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -16,7 +16,6 @@
package androidx.camera.camera2.pipe.compat
-import android.content.Context
import android.graphics.ColorSpace
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCaptureSession
@@ -45,63 +44,6 @@
import androidx.camera.camera2.pipe.CameraMetadata.Companion.availableVideoStabilizationModes
import java.util.concurrent.Executor
-@RequiresApi(23)
-internal object Api23Compat {
- @JvmStatic
- @Throws(CameraAccessException::class)
- @Suppress("deprecation")
- fun createReprocessableCaptureSession(
- cameraDevice: CameraDevice,
- inputConfig: InputConfiguration,
- outputs: List<Surface>,
- callback: CameraCaptureSession.StateCallback,
- handler: Handler?,
- ) {
- cameraDevice.createReprocessableCaptureSession(inputConfig, outputs, callback, handler)
- }
-
- @JvmStatic
- @Throws(CameraAccessException::class)
- @Suppress("deprecation")
- fun createConstrainedHighSpeedCaptureSession(
- cameraDevice: CameraDevice,
- outputs: List<Surface>,
- stateCallback: CameraCaptureSession.StateCallback,
- handler: Handler?,
- ) {
- cameraDevice.createConstrainedHighSpeedCaptureSession(outputs, stateCallback, handler)
- }
-
- @JvmStatic
- @Throws(CameraAccessException::class)
- fun createReprocessCaptureRequest(
- cameraDevice: CameraDevice,
- inputResult: TotalCaptureResult,
- ): CaptureRequest.Builder {
- return cameraDevice.createReprocessCaptureRequest(inputResult)
- }
-
- @JvmStatic
- fun isReprocessable(cameraCaptureSession: CameraCaptureSession): Boolean {
- return cameraCaptureSession.isReprocessable
- }
-
- @JvmStatic
- fun getInputSurface(cameraCaptureSession: CameraCaptureSession): Surface? {
- return cameraCaptureSession.inputSurface
- }
-
- @JvmStatic
- fun newInputConfiguration(width: Int, height: Int, format: Int): InputConfiguration {
- return InputConfiguration(width, height, format)
- }
-
- @JvmStatic
- fun checkSelfPermission(context: Context, permission: String): Int {
- return context.checkSelfPermission(permission)
- }
-}
-
@RequiresApi(24)
internal object Api24Compat {
@JvmStatic
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index 8f1069a..ec5f04b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -155,7 +155,7 @@
// surface per request.
check(hasSurface)
- if (request.inputRequest != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (request.inputRequest != null) {
checkNotNull(imageWriter) {
"Failed to create ImageWriter for capture session: $session"
}
@@ -387,7 +387,7 @@
* created, assuming it's a reprocessing session.
*/
private val imageWriter =
- if (streamGraph.inputs.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (streamGraph.inputs.isNotEmpty()) {
val inputStream = streamGraph.inputs.first()
val sessionInputSurface = session.inputSurface
checkNotNull(sessionInputSurface) {
@@ -558,17 +558,13 @@
): CaptureRequest.Builder? {
val requestBuilder =
if (request.inputRequest != null) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- val totalCaptureResult =
- request.inputRequest.frameInfo.unwrapAs(TotalCaptureResult::class)
- checkNotNull(totalCaptureResult) {
- "Failed to unwrap FrameInfo ${request.inputRequest.frameInfo} as " +
- "TotalCaptureResult"
- }
- session.device.createReprocessCaptureRequest(totalCaptureResult)
- } else {
- null
+ val totalCaptureResult =
+ request.inputRequest.frameInfo.unwrapAs(TotalCaptureResult::class)
+ checkNotNull(totalCaptureResult) {
+ "Failed to unwrap FrameInfo ${request.inputRequest.frameInfo} as " +
+ "TotalCaptureResult"
}
+ session.device.createReprocessCaptureRequest(totalCaptureResult)
} else {
session.device.createCaptureRequest(requestTemplate)
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
index 11f7b35..ee8bbaae 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
@@ -22,6 +22,7 @@
import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.EXACT
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy
+import androidx.camera.camera2.pipe.compat.Camera2Quirks.Companion.SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max
@@ -95,7 +96,7 @@
*/
internal fun shouldCloseCameraBeforeCreatingCaptureSession(cameraId: CameraId): Boolean {
val isLegacyDevice =
- Build.VERSION.SDK_INT in (Build.VERSION_CODES.M..Build.VERSION_CODES.S_V2) &&
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 &&
metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy
val isQuirkyDevice =
"motorola".equals(Build.BRAND, ignoreCase = true) &&
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index 12081ac..6680082 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -53,7 +53,6 @@
fun createCaptureRequest(template: RequestTemplate): CaptureRequest.Builder?
/** @see CameraDevice.createReprocessCaptureRequest */
- @RequiresApi(23)
fun createReprocessCaptureRequest(inputResult: TotalCaptureResult): CaptureRequest.Builder?
/** @see CameraDevice.createCaptureSession */
@@ -63,7 +62,6 @@
): Boolean
/** @see CameraDevice.createReprocessableCaptureSession */
- @RequiresApi(23)
fun createReprocessableCaptureSession(
input: InputConfiguration,
outputs: List<Surface>,
@@ -71,7 +69,6 @@
): Boolean
/** @see CameraDevice.createConstrainedHighSpeedCaptureSession */
- @RequiresApi(23)
fun createConstrainedHighSpeedCaptureSession(
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -216,7 +213,7 @@
return result != null
}
- @RequiresApi(23)
+ @Suppress("deprecation")
override fun createReprocessableCaptureSession(
input: InputConfiguration,
outputs: List<Surface>,
@@ -229,8 +226,7 @@
instrumentAndCatch("createReprocessableCaptureSession") {
// This function was deprecated in Android Q, but is required for some
// configurations when running on older versions of the OS.
- Api23Compat.createReprocessableCaptureSession(
- cameraDevice,
+ cameraDevice.createReprocessableCaptureSession(
input,
outputs,
AndroidCaptureSessionStateCallback(
@@ -256,7 +252,7 @@
return result != null
}
- @RequiresApi(23)
+ @Suppress("deprecation")
override fun createConstrainedHighSpeedCaptureSession(
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -269,8 +265,7 @@
// This function was deprecated in Android Q, but is required for some
// configurations
// when running on older versions of the OS.
- Api23Compat.createConstrainedHighSpeedCaptureSession(
- cameraDevice,
+ cameraDevice.createConstrainedHighSpeedCaptureSession(
outputs,
AndroidCaptureSessionStateCallback(
this,
@@ -349,11 +344,7 @@
// configurations when running on older versions of the OS.
Api24Compat.createReprocessableCaptureSessionByConfigurations(
cameraDevice,
- Api23Compat.newInputConfiguration(
- inputConfig.width,
- inputConfig.height,
- inputConfig.format,
- ),
+ InputConfiguration(inputConfig.width, inputConfig.height, inputConfig.format),
outputs.map { it.unwrapAs(OutputConfiguration::class) },
AndroidCaptureSessionStateCallback(
this,
@@ -412,7 +403,7 @@
} else {
Api28Compat.setInputConfiguration(
sessionConfig,
- Api23Compat.newInputConfiguration(
+ InputConfiguration(
config.inputConfiguration.single().width,
config.inputConfiguration.single().height,
config.inputConfiguration.single().format,
@@ -479,12 +470,11 @@
cameraDevice.createCaptureRequest(template.value)
}
- @RequiresApi(23)
override fun createReprocessCaptureRequest(
inputResult: TotalCaptureResult
): CaptureRequest.Builder? =
instrumentAndCatch("createReprocessCaptureRequest") {
- Api23Compat.createReprocessCaptureRequest(cameraDevice, inputResult)
+ cameraDevice.createReprocessCaptureRequest(inputResult)
}
@RequiresApi(30)
@@ -577,7 +567,6 @@
}
}
- @RequiresApi(23)
override fun createReprocessableCaptureSession(
input: InputConfiguration,
outputs: List<Surface>,
@@ -593,7 +582,6 @@
}
}
- @RequiresApi(23)
override fun createConstrainedHighSpeedCaptureSession(
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -687,7 +675,6 @@
}
}
- @RequiresApi(23)
override fun createReprocessCaptureRequest(inputResult: TotalCaptureResult) =
synchronized(lock) {
if (disconnected) {
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 b93ff84..9b1b156 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
@@ -53,7 +53,6 @@
@Camera2ControllerScope
@Provides
fun provideSessionFactory(
- androidLProvider: Provider<AndroidLSessionFactory>,
androidMProvider: Provider<AndroidMSessionFactory>,
androidMHighSpeedProvider: Provider<AndroidMHighSpeedSessionFactory>,
androidNProvider: Provider<AndroidNSessionFactory>,
@@ -73,9 +72,6 @@
}
if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
- check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- "Cannot use HighSpeed sessions below Android M"
- }
return androidMHighSpeedProvider.get()
}
@@ -83,34 +79,11 @@
return androidNProvider.get()
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return androidMProvider.get()
- }
-
- check(graphConfig.input == null) { "Reprocessing is not supported on Android L" }
-
- return androidLProvider.get()
+ check(graphConfig.input == null) { "Reprocessing is not supported on Android M" }
+ return androidMProvider.get()
}
}
-internal class AndroidLSessionFactory @Inject constructor(private val threads: Threads) :
- CaptureSessionFactory {
- override fun create(
- cameraDevice: CameraDeviceWrapper,
- surfaces: Map<StreamId, Surface>,
- captureSessionState: CaptureSessionState,
- ): Map<StreamId, OutputConfigurationWrapper> {
- if (!cameraDevice.createCaptureSession(surfaces.map { it.value }, captureSessionState)) {
- Log.warn {
- "Failed to create capture session from $cameraDevice for $captureSessionState!"
- }
- captureSessionState.onSessionFinalized()
- }
- return emptyMap()
- }
-}
-
-@RequiresApi(23)
internal class AndroidMSessionFactory
@Inject
constructor(private val threads: Threads, private val graphConfig: CameraGraph.Config) :
@@ -153,7 +126,6 @@
}
}
-@RequiresApi(23)
internal class AndroidMHighSpeedSessionFactory @Inject constructor(private val threads: Threads) :
CaptureSessionFactory {
override fun create(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
index d7e2840..102f717 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
@@ -222,9 +222,7 @@
// return a CameraConstrainedHighSpeedCaptureSession depending on the configuration. If
// this happens, several methods are not allowed, the behavior is different, and interacting
// with the session requires several behavior changes for these interactions to work well.
- return if (
- Build.VERSION.SDK_INT >= 23 && session is CameraConstrainedHighSpeedCaptureSession
- ) {
+ return if (session is CameraConstrainedHighSpeedCaptureSession) {
AndroidCameraConstrainedHighSpeedCaptureSession(
device,
session,
@@ -299,23 +297,10 @@
CameraInterop.nextCameraCaptureSessionId()
override val isReprocessable: Boolean
- get() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return Api23Compat.isReprocessable(cameraCaptureSession)
- }
- // Reprocessing is not supported prior to Android M
- return false
- }
+ get() = cameraCaptureSession.isReprocessable
override val inputSurface: Surface?
- get() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return Api23Compat.getInputSurface(cameraCaptureSession)
- }
- // Reprocessing is not supported prior to Android M, and a CaptureSession that does not
- // support reprocessing will have a null input surface on M and beyond.
- return null
- }
+ get() = cameraCaptureSession.inputSurface
@RequiresApi(26)
override fun finalizeOutputConfigurations(
@@ -358,7 +343,6 @@
* An implementation of [CameraConstrainedHighSpeedCaptureSessionWrapper] forwards calls to a real
* [CameraConstrainedHighSpeedCaptureSession].
*/
-@RequiresApi(23)
internal class AndroidCameraConstrainedHighSpeedCaptureSession
internal constructor(
device: CameraDeviceWrapper,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
index bb2e486..ae25eb0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
@@ -20,8 +20,6 @@
import android.content.Context
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.compat.Api23Compat
import androidx.camera.camera2.pipe.config.CameraPipeContext
import javax.inject.Inject
import javax.inject.Singleton
@@ -38,16 +36,8 @@
constructor(@CameraPipeContext private val cameraPipeContext: Context) {
@Volatile private var _hasCameraPermission = false
val hasCameraPermission: Boolean
- get() =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- checkCameraPermission()
- } else {
- // On older versions of Android, permissions are required in order to install a
- // package and so the permission check is redundant.
- true
- }
+ get() = checkCameraPermission()
- @RequiresApi(23)
private fun checkCameraPermission(): Boolean {
if (Build.FINGERPRINT == "robolectric") {
// If we're running under Robolectric, assume we have camera permission since
@@ -63,7 +53,7 @@
if (!_hasCameraPermission) {
Debug.traceStart { "CXCP#checkCameraPermission" }
if (
- Api23Compat.checkSelfPermission(cameraPipeContext, Manifest.permission.CAMERA) ==
+ cameraPipeContext.checkSelfPermission(Manifest.permission.CAMERA) ==
PERMISSION_GRANTED
) {
_hasCameraPermission = true
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index da77b7a..2d96e71 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -25,6 +25,7 @@
import androidx.camera.camera2.pipe.AwbMode
import androidx.camera.camera2.pipe.CameraController
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraGraph.Session
import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.FrameMetadata
@@ -46,7 +47,6 @@
import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
import androidx.camera.camera2.pipe.internal.FrameDistributor
import javax.inject.Inject
-import kotlin.use
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
@@ -107,10 +107,7 @@
}
}
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- require(graphConfig.input == null) { "Reprocessing not supported under Android M" }
- }
- if (graphConfig.input != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (graphConfig.input != null) {
require(graphConfig.input.isNotEmpty()) {
"At least one InputConfiguration is required for reprocessing"
}
@@ -154,7 +151,7 @@
Debug.traceStop()
}
- override suspend fun acquireSession(): CameraGraph.Session {
+ override suspend fun acquireSession(): Session {
// Step 1: Acquire a lock on the session mutex, which returns a releasable token. This may
// or may not suspend.
val token = sessionLock.acquireToken()
@@ -164,14 +161,12 @@
return createSessionFromToken(token)
}
- override fun acquireSessionOrNull(): CameraGraph.Session? {
+ override fun acquireSessionOrNull(): Session? {
val token = sessionLock.tryAcquireToken() ?: return null
return createSessionFromToken(token)
}
- override suspend fun <T> useSession(
- action: suspend CoroutineScope.(CameraGraph.Session) -> T
- ): T =
+ override suspend fun <T> useSession(action: suspend CoroutineScope.(Session) -> T): T =
acquireSession().use {
// Wrap the block in a coroutineScope to ensure all operations are completed before
// releasing the lock.
@@ -180,7 +175,7 @@
override fun <T> useSessionIn(
scope: CoroutineScope,
- action: suspend CoroutineScope.(CameraGraph.Session) -> T,
+ action: suspend CoroutineScope.(Session) -> T,
): Deferred<T> {
return sessionLock.withTokenIn(scope) { token ->
// Create and use the session
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 6e7af11..cc10bdd 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -28,9 +28,7 @@
import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.params.MeteringRectangle
-import android.os.Build
import androidx.annotation.GuardedBy
-import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
@@ -587,40 +585,7 @@
if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return unlock3APostCaptureAndroidMAndAbove(cancelAf)
- }
- return unlock3APostCaptureAndroidLAndBelow(cancelAf)
- }
-
- /**
- * For api level below 23, to resume the normal scan of ae after precapture metering sequence,
- * we have to first send a request with ae lock = true and then a request with ae lock = false.
- * REF :
- * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
- */
- private fun unlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true): Deferred<Result3A> {
- debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
- val cancelParams =
- if (cancelAf) {
- unlock3APostCaptureLockAeAndCancelAfParams
- } else {
- unlock3APostCaptureLockAeParams
- }
- if (!graphProcessor.trigger(cancelParams)) return deferredResult3ASubmitFailed
-
- // Listener to monitor when we receive the capture result corresponding to the request
- // below.
- val listener = Result3AStateListenerImpl(emptyMap())
- graphListener3A.addListener(listener)
-
- debug { "unlock3AForCapture - sending a request to turn off ae." }
- if (!graphProcessor.trigger(unlock3APostCaptureUnlockAeParams)) {
- graphListener3A.removeListener(listener)
- return deferredResult3ASubmitFailed
- }
-
- return listener.result
+ return unlock3APostCaptureAndroidMAndAbove(cancelAf)
}
/**
@@ -628,7 +593,6 @@
* = CANCEL can be used to unlock the camera device's internally locked AE. REF :
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
- @RequiresApi(23)
private fun unlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true): Deferred<Result3A> {
debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
val cancelParams = if (cancelAf) aePrecaptureAndAfCancelParams else aePrecaptureCancelParams
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
index b69a9d9..1c80885 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
@@ -21,7 +21,6 @@
import android.os.Build
import android.os.Handler
import android.view.Surface
-import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.InputStreamId
import androidx.camera.camera2.pipe.StreamFormat
import androidx.camera.camera2.pipe.compat.Api29Compat
@@ -31,7 +30,6 @@
import kotlinx.atomicfu.atomic
/** Implements an [ImageWriterWrapper] using an [ImageWriter]. */
-@RequiresApi(23)
public class AndroidImageWriter
private constructor(
private val imageWriter: ImageWriter,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
index faae846..113aef5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
@@ -17,12 +17,10 @@
package androidx.camera.camera2.pipe.media
import android.media.ImageWriter
-import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.InputStreamId
import androidx.camera.camera2.pipe.UnsafeWrapper
/** Simplified wrapper for [ImageWriter]-like classes. */
-@RequiresApi(23)
public interface ImageWriterWrapper : UnsafeWrapper, AutoCloseable {
/**
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index a6ea031..bf05a42 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -20,7 +20,6 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
-import android.os.Build
import androidx.camera.camera2.pipe.FrameMetadata
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestNumber
@@ -395,20 +394,12 @@
@Test
fun testUnlock3APostCapture() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- testUnlock3APostCaptureAndroidMAndAbove()
- } else {
- testUnlock3APostCaptureAndroidLAndBelow()
- }
+ testUnlock3APostCaptureAndroidMAndAbove()
}
@Test
fun testUnlock3APostCapture_whenAfNotTriggered() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- testUnlock3APostCaptureAndroidMAndAbove(false)
- } else {
- testUnlock3APostCaptureAndroidLAndBelow(false)
- }
+ testUnlock3APostCaptureAndroidMAndAbove(false)
}
private fun testUnlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true) = runTest {
@@ -488,45 +479,6 @@
)
}
- private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
- val result = controller3A.unlock3APostCapture(cancelAf)
- assertThat(result.isCompleted).isFalse()
-
- val cameraResponse = async {
- listener3A.onRequestSequenceCreated(
- FakeRequestMetadata(requestNumber = RequestNumber(1))
- )
- listener3A.onPartialCaptureResult(
- FakeRequestMetadata(requestNumber = RequestNumber(1)),
- FrameNumber(101L),
- FakeFrameMetadata(frameNumber = FrameNumber(101L), resultMetadata = mapOf()),
- )
- }
-
- cameraResponse.await()
- val result3A = result.await()
- assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
- assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
-
- // We now check if the correct sequence of requests were submitted by unlock3APostCapture
- // call. There should be a request to cancel AF and lock ae.
- val event1 = captureSequenceProcessor.nextEvent()
- if (cancelAf) {
- assertThat(event1.requiredParameters)
- .containsEntry(
- CaptureRequest.CONTROL_AF_TRIGGER,
- CaptureRequest.CONTROL_AF_TRIGGER_CANCEL,
- )
- }
-
- assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
-
- // Then another request to unlock ae.
- val captureSequence2 = captureSequenceProcessor.nextEvent()
- assertThat(captureSequence2.requiredParameters)
- .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
- }
-
private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
val event1 = captureSequenceProcessor.nextEvent()
assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
index a0d5447..a88d926 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
@@ -20,13 +20,11 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.InputConfiguration
-import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.AudioRestrictionMode
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.compat.Api23Compat
import androidx.camera.camera2.pipe.compat.CameraCaptureSessionWrapper
import androidx.camera.camera2.pipe.compat.CameraDeviceWrapper
import androidx.camera.camera2.pipe.compat.CameraExtensionSessionWrapper
@@ -53,12 +51,7 @@
override fun createReprocessCaptureRequest(
inputResult: TotalCaptureResult
): CaptureRequest.Builder {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return Api23Compat.createReprocessCaptureRequest(fakeCamera.cameraDevice, inputResult)
- }
- throw UnsupportedOperationException(
- "createReprocessCaptureRequest is not supported below API 23"
- )
+ return fakeCamera.cameraDevice.createReprocessCaptureRequest(inputResult)
}
override fun createCaptureSession(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 2b5352e..680f627 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -18,7 +18,6 @@
import static androidx.camera.core.CameraUnavailableException.CAMERA_ERROR;
import static androidx.camera.core.impl.CameraValidator.CameraIdListIncorrectException;
-import static androidx.camera.core.impl.CameraValidator.validateCameras;
import android.app.Application;
import android.content.ComponentName;
@@ -47,6 +46,7 @@
import androidx.camera.core.impl.CameraProviderExecutionState;
import androidx.camera.core.impl.CameraRepository;
import androidx.camera.core.impl.CameraThreadConfig;
+import androidx.camera.core.impl.CameraValidator;
import androidx.camera.core.impl.MetadataHolderService;
import androidx.camera.core.impl.QuirkSettings;
import androidx.camera.core.impl.QuirkSettingsHolder;
@@ -401,6 +401,8 @@
CameraSelector availableCamerasLimiter =
mCameraXConfig.getAvailableCamerasLimiter(null);
+ CameraValidator cameraValidator =
+ CameraValidator.create(appContext, availableCamerasLimiter);
long cameraOpenRetryMaxTimeoutInMillis =
mCameraXConfig.getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
@@ -457,13 +459,13 @@
mCameraUseCaseAdapterProvider);
}
- mCameraPresenceProvider.startup(mCameraFactory, mCameraRepository);
+ mCameraPresenceProvider.startup(cameraValidator, mCameraFactory, mCameraRepository);
mCameraPresenceProvider.addDependentInternalListener(mSurfaceManager);
mCameraPresenceProvider.addDependentInternalListener(
mCameraFactory.getCameraCoordinator());
// Please ensure only validate the camera at the last of the initialization.
- validateCameras(appContext, mCameraRepository, availableCamerasLimiter);
+ cameraValidator.validateOnFirstInit(mCameraRepository);
// Set completer to null if the init was successful.
if (attemptCount > 1) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
similarity index 88%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt
rename to camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
index 8f2e783..781ad1c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
@@ -45,6 +45,7 @@
private var cameraFactory: CameraFactory? = null
private var cameraRepository: CameraRepository? = null
private var sourcePresenceObservable: Observable<List<CameraIdentifier>>? = null
+ private var cameraValidator: CameraValidator? = null
private val sourceObserver: SourceObservableObserver = SourceObservableObserver()
@Volatile private var currentFilteredIds: List<CameraIdentifier> = emptyList()
@@ -64,15 +65,21 @@
/**
* Starts monitoring camera presence.
*
+ * @param cameraValidator The validator to verify whether the change of the camera is valid.
* @param cameraFactory The factory that provides the presence observable.
* @param cameraRepository The repository to get camera instances from.
*/
- public fun startup(cameraFactory: CameraFactory, cameraRepository: CameraRepository) {
+ public fun startup(
+ cameraValidator: CameraValidator,
+ cameraFactory: CameraFactory,
+ cameraRepository: CameraRepository,
+ ) {
if (!isMonitoring.compareAndSet(false, true)) {
return
}
Logger.i(TAG, "Starting CameraPresenceProvider monitoring.")
+ this.cameraValidator = cameraValidator
this.currentFilteredIds =
cameraFactory.availableCameraIds.map { CameraIdentifier.create(it) }
this.cameraFactory = cameraFactory
@@ -96,6 +103,7 @@
sourcePresenceObservable?.removeObserver(sourceObserver)
clearAllCameraStateObservers()
+ cameraValidator = null
dependentInternalListeners.clear()
publicApiListeners.clear()
currentFilteredIds = emptyList()
@@ -106,20 +114,50 @@
private inner class SourceObservableObserver : Observable.Observer<List<CameraIdentifier>> {
override fun onNewData(rawCameraIdentifiers: List<CameraIdentifier>?) {
if (!isMonitoring.get()) return
- val factory = cameraFactory ?: return
- // Phase 1: Update CameraFactory
+ val factory = cameraFactory ?: return
+ val repo = cameraRepository ?: return
+ val validator = cameraValidator ?: return
+
val rawIdStrings = rawCameraIdentifiers?.map { it.internalId } ?: emptyList()
+
+ // For factories that support interrogation, we can pre-validate the change.
+ if (factory is CameraFactory.Interrogator) {
+ val oldFilteredIds = currentFilteredIds
+ val potentialNewIds =
+ factory.getAvailableCameraIds(rawIdStrings).map { CameraIdentifier.create(it) }
+
+ val removedCameras = oldFilteredIds.toSet() - potentialNewIds.toSet()
+ if (
+ removedCameras.isNotEmpty() &&
+ validator.isChangeInvalid(repo.cameras, removedCameras)
+ ) {
+ Logger.w(TAG, "Camera removal update invalid. Aborting.")
+ return
+ }
+ }
+
+ // After any pre-validation has passed, commit the update to the factory.
try {
factory.onCameraIdsUpdated(rawIdStrings)
} catch (e: Exception) {
- Logger.e(TAG, "CameraFactory failed to update. Triggering refresh.", e)
- sourcePresenceObservable?.fetchData()
+ Logger.w(
+ TAG,
+ "CameraFactory failed to update. The camera list may be stale until the next update.",
+ e,
+ )
return
}
- // Phase 2: Get filtered list and start transaction
+ // Now, get the definitive new list from the factory's updated state.
val newFilteredIds = factory.availableCameraIds.map { CameraIdentifier.create(it) }
+
+ // If the final list results in no change, we can stop.
+ if (newFilteredIds == currentFilteredIds) {
+ return
+ }
+
+ // Proceed with the full update transaction.
processFilteredCameraIdUpdate(newFilteredIds)
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java
deleted file mode 100644
index ce4ef85..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2020 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.camera.core.impl;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-
-import androidx.annotation.OptIn;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ExperimentalLensFacing;
-import androidx.camera.core.Logger;
-
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import java.util.Set;
-
-/**
- * Validation methods to verify the camera is initialized successfully, more info please reference
- * b/167201193.
- */
-@OptIn(markerClass = ExperimentalLensFacing.class)
-public final class CameraValidator {
- private CameraValidator() {
- }
-
- private static final String TAG = "CameraValidator";
- private static final CameraSelector EXTERNAL_LENS_FACING =
- new CameraSelector.Builder().requireLensFacing(
- CameraSelector.LENS_FACING_EXTERNAL).build();
-
- /**
- * Verifies the initialized camera instance in the CameraRepository
- *
- * <p>It should initialize the cameras that physically supported on the device. The
- * physically supported device lens facing information comes from the package manager and the
- * system feature flags are set by the vendor as part of the device build and CTS verified.
- *
- * @param context The application or activity context.
- * @param cameraRepository The camera repository for verify.
- * @param availableCamerasSelector Indicate the camera that we need to check.
- * @throws CameraIdListIncorrectException if it fails to find all the camera instances that
- * physically supported on the device.
- */
- public static void validateCameras(@NonNull Context context,
- @NonNull CameraRepository cameraRepository,
- @Nullable CameraSelector availableCamerasSelector)
- throws CameraIdListIncorrectException {
-
- // Check if running on a virtual device with Android U or higher
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
- && Api34Impl.getDeviceId(context) != Context.DEVICE_ID_DEFAULT) {
-
- Set<CameraInternal> availableCameras = cameraRepository.getCameras();
- // Log details and skip validation since we have at least one camera
- Logger.d(TAG, "Virtual device with ID: " + Api34Impl.getDeviceId(context)
- + " has " + availableCameras.size() + " cameras. Skipping validation.");
- return;
- }
-
- Integer lensFacing = null;
- try {
- if (availableCamerasSelector != null
- && (lensFacing = availableCamerasSelector.getLensFacing()) == null) {
- Logger.w(TAG, "No lens facing info in the availableCamerasSelector, don't "
- + "verify the camera lens facing.");
- return;
- }
- } catch (IllegalStateException e) {
- Logger.e(TAG, "Cannot get lens facing from the availableCamerasSelector don't "
- + "verify the camera lens facing.", e);
- return;
- }
-
- Logger.d(TAG,
- "Verifying camera lens facing on " + Build.DEVICE + ", lensFacingInteger: "
- + lensFacing);
-
- PackageManager pm = context.getPackageManager();
- Throwable exception = null;
- int availableCameraCount = 0;
- try {
- if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
- if (availableCamerasSelector == null
- || lensFacing.intValue() == CameraSelector.LENS_FACING_BACK) {
- // Only verify the main camera if it is NOT specifying the available lens
- // facing or it required the LENS_FACING_BACK camera.
- CameraSelector.DEFAULT_BACK_CAMERA.select(cameraRepository.getCameras());
- availableCameraCount++;
- }
- }
- } catch (IllegalArgumentException e) {
- Logger.w(TAG, "Camera LENS_FACING_BACK verification failed", e);
- exception = e;
- }
- try {
- if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) {
- if (availableCamerasSelector == null
- || lensFacing.intValue() == CameraSelector.LENS_FACING_FRONT) {
- // Only verify the front camera if it is NOT specifying the available lens
- // facing or it required the LENS_FACING_FRONT camera.
- CameraSelector.DEFAULT_FRONT_CAMERA.select(cameraRepository.getCameras());
- availableCameraCount++;
- }
- }
- } catch (IllegalArgumentException e) {
- Logger.w(TAG, "Camera LENS_FACING_FRONT verification failed", e);
- exception = e;
- }
- try {
- // Verifies the EXTERNAL camera.
- EXTERNAL_LENS_FACING.select(cameraRepository.getCameras());
- Logger.d(TAG, "Found a LENS_FACING_EXTERNAL camera");
- availableCameraCount++;
- } catch (IllegalArgumentException e) {
- }
-
- if (exception != null) {
- Logger.e(TAG, "Camera LensFacing verification failed, existing cameras: "
- + cameraRepository.getCameras());
- throw new CameraIdListIncorrectException(
- "Expected camera missing from device.", availableCameraCount, exception);
- }
- }
-
- /** The exception for the b/167201193: incorrect camera id list. */
- public static class CameraIdListIncorrectException extends Exception {
-
- private final int mAvailableCameraCount;
-
- public CameraIdListIncorrectException(@Nullable String message,
- int availableCameraCount, @Nullable Throwable cause) {
- super(message, cause);
- mAvailableCameraCount = availableCameraCount;
- }
-
- public int getAvailableCameraCount() {
- return mAvailableCameraCount;
- }
- }
-
- @RequiresApi(34)
- private static class Api34Impl {
- private Api34Impl() {
- }
-
- static int getDeviceId(@NonNull Context context) {
- return context.getDeviceId();
- }
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt
new file mode 100644
index 0000000..1f43bf6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.camera.core.impl
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.CameraIdentifier
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Logger
+
+/** An interface for validating the camera state against system-advertised features. */
+public interface CameraValidator {
+
+ /**
+ * Validates the initial set of cameras upon startup.
+ *
+ * @throws CameraIdListIncorrectException if a camera required by system features is missing.
+ */
+ @Throws(CameraIdListIncorrectException::class)
+ public fun validateOnFirstInit(cameraRepository: CameraRepository)
+
+ /**
+ * Checks if a proposed camera removal would result in a degraded state.
+ *
+ * This method determines if removing a set of cameras would violate the device's hardware
+ * contract as defined by system features (e.g., `PackageManager.FEATURE_CAMERA`). A change is
+ * considered **invalid** if it removes a **required** camera that is **currently available**,
+ * thereby making the camera state "worse".
+ *
+ * **Examples (Assuming device requires a BACK and FRONT camera):**
+ * 1. **Valid:** Removing a non-required `EXTERNAL` camera from `[BACK, FRONT, EXTERNAL]` is
+ * allowed.
+ * 2. **Invalid:** Removing the required `BACK` camera from `[BACK, FRONT]` is blocked.
+ * 3. **Valid (Degraded State):** If the initial state is `[BACK, EXTERNAL]` (missing the
+ * required `FRONT`), removing the non-required `EXTERNAL` camera is still allowed as the
+ * situation does not worsen.
+ *
+ * The validation logic is constrained by the `availableCamerasSelector` provided when this
+ * validator was created.
+ *
+ * @param currentCameras The complete set of `CameraInternal` objects available *before* the
+ * proposed removal.
+ * @param removedCameras The set of `CameraIdentifier`s for the cameras that are being removed.
+ * @return `true` if the change is invalid and should be aborted, `false` otherwise.
+ */
+ public fun isChangeInvalid(
+ currentCameras: Set<CameraInternal>,
+ removedCameras: Set<CameraIdentifier>,
+ ): Boolean
+
+ /** The exception for an incorrect camera id list. */
+ public class CameraIdListIncorrectException(
+ message: String?,
+ public val availableCameraCount: Int,
+ cause: Throwable?,
+ ) : Exception(message, cause)
+
+ /** Companion object to hold the factory method. */
+ public companion object {
+ /**
+ * Creates a new instance of the default CameraValidator.
+ *
+ * @param context The application context.
+ * @param availableCamerasSelector The selector that filters which cameras to validate.
+ * @return A new [CameraValidator] instance.
+ */
+ @JvmStatic
+ public // Makes this callable as a static method from Java
+ fun create(context: Context, availableCamerasSelector: CameraSelector?): CameraValidator {
+ return CameraValidatorImpl(context, availableCamerasSelector)
+ }
+ }
+}
+
+/**
+ * The default implementation of [CameraValidator].
+ *
+ * This validator is configured with a context and an optional camera selector upon creation.
+ */
+public class CameraValidatorImpl(
+ private val context: Context,
+ private val availableCamerasSelector: CameraSelector?,
+) : CameraValidator {
+
+ private val isVirtualDevice = isVirtualDevice(context)
+ private val validationCriteria = getValidationCriteria()
+
+ override fun validateOnFirstInit(cameraRepository: CameraRepository) {
+ if (isVirtualDevice) {
+ Logger.d(
+ TAG,
+ "Virtual device with " +
+ "${cameraRepository.cameras.size} cameras. Skipping validation.",
+ )
+ return
+ }
+
+ Logger.d(TAG, "Verifying camera lens facing on " + Build.DEVICE)
+ var exception: RuntimeException? = null
+
+ if (validationCriteria.checkBack) {
+ try {
+ CameraSelector.DEFAULT_BACK_CAMERA.select(cameraRepository.cameras)
+ } catch (e: RuntimeException) {
+ Logger.w(TAG, "Camera LENS_FACING_BACK verification failed", e)
+ exception = e
+ }
+ }
+
+ if (validationCriteria.checkFront) {
+ try {
+ CameraSelector.DEFAULT_FRONT_CAMERA.select(cameraRepository.cameras)
+ } catch (e: RuntimeException) {
+ Logger.w(TAG, "Camera LENS_FACING_FRONT verification failed", e)
+ if (exception == null) exception = e
+ }
+ }
+
+ if (exception != null) {
+ throw CameraValidator.CameraIdListIncorrectException(
+ "Expected camera missing from device.",
+ cameraRepository.cameras.size,
+ exception,
+ )
+ }
+ }
+
+ override fun isChangeInvalid(
+ currentCameras: Set<CameraInternal>,
+ removedCameras: Set<CameraIdentifier>,
+ ): Boolean {
+ if (isVirtualDevice || (!validationCriteria.checkBack && !validationCriteria.checkFront)) {
+ return false
+ }
+
+ val hadBack = hasCamera(currentCameras, CameraSelector.DEFAULT_BACK_CAMERA)
+ val hadFront = hasCamera(currentCameras, CameraSelector.DEFAULT_FRONT_CAMERA)
+
+ val removedCameraIds = removedCameras.map { it.internalId }.toSet()
+ val newProposedCameras =
+ currentCameras.filter { it.cameraInfoInternal.cameraId !in removedCameraIds }.toSet()
+
+ val willHaveBack = hasCamera(newProposedCameras, CameraSelector.DEFAULT_BACK_CAMERA)
+ val willHaveFront = hasCamera(newProposedCameras, CameraSelector.DEFAULT_FRONT_CAMERA)
+
+ val backCameraIsLost = validationCriteria.checkBack && hadBack && !willHaveBack
+ val frontCameraIsLost = validationCriteria.checkFront && hadFront && !willHaveFront
+
+ return backCameraIsLost || frontCameraIsLost
+ }
+
+ private fun hasCamera(cameras: Set<CameraInternal>, selector: CameraSelector): Boolean {
+ return try {
+ selector.select(LinkedHashSet(cameras))
+ true
+ } catch (_: IllegalArgumentException) {
+ false
+ }
+ }
+
+ private data class ValidationCriteria(val checkBack: Boolean, val checkFront: Boolean)
+
+ private fun getValidationCriteria(): ValidationCriteria {
+ val pm = context.packageManager
+ val lensFacing = this.availableCamerasSelector?.lensFacing
+ val needsBackCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+ val needsFrontCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)
+
+ val checkBack =
+ needsBackCamera && (lensFacing == null || lensFacing == CameraSelector.LENS_FACING_BACK)
+ val checkFront =
+ needsFrontCamera &&
+ (lensFacing == null || lensFacing == CameraSelector.LENS_FACING_FRONT)
+ return ValidationCriteria(checkBack, checkFront)
+ }
+
+ private fun isVirtualDevice(context: Context): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
+ Api34Impl.getDeviceId(context) != Context.DEVICE_ID_DEFAULT
+ }
+
+ @RequiresApi(34)
+ private object Api34Impl {
+ fun getDeviceId(context: Context): Int {
+ return context.getDeviceId()
+ }
+ }
+
+ public companion object {
+ private const val TAG = "CameraValidator"
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
index b6f718c..8af0795 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
@@ -45,6 +45,7 @@
private lateinit var testCameraRepository: TestCameraRepository
private val fakeCoordinator = FakeCameraCoordinator()
private val fakeSurfaceManager = FakeCameraDeviceSurfaceManager()
+ private val fakeValidator = FakeCameraValidator()
private val publicListener = TestCameraPresenceListener()
private lateinit var provider: CameraPresenceProvider
@@ -58,13 +59,103 @@
testCameraRepository = TestCameraRepository(fakeCameraFactory)
provider = CameraPresenceProvider(MoreExecutors.directExecutor())
- provider.startup(fakeCameraFactory, testCameraRepository)
+ provider.startup(fakeValidator, fakeCameraFactory, testCameraRepository)
provider.addDependentInternalListener(fakeSurfaceManager)
provider.addDependentInternalListener(fakeCoordinator)
provider.addCameraPresenceListener(publicListener, MoreExecutors.directExecutor())
}
@Test
+ fun onNewData_abortsUpdate_whenValidatorReturnsInvalid() {
+ // Arrange: Start with one camera.
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+ FakeCamera(CAMERA_ID_0, null, FakeCameraInfoInternal())
+ }
+ sourceObservable.updateData(listOf(IDENTIFIER_0))
+
+ // Sanity check initial state.
+ assertThat(testCameraRepository.updateCount).isEqualTo(1)
+ assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+
+ // Act: Configure validator to fail the next update.
+ fakeValidator.setNextIsChangeInvalid(true)
+ // Attempt to remove the camera.
+ sourceObservable.updateData(emptyList())
+
+ // Assert: The update should have been aborted.
+ // The repository update count remains 1 because the invalid update was blocked.
+ assertThat(testCameraRepository.updateCount).isEqualTo(1)
+ // The repository state should not have changed.
+ assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+ // The public listener should not be notified of any removal.
+ assertThat(publicListener.removedCameras).isEmpty()
+ }
+
+ @Test
+ fun onNewData_allowsNonImpactfulRemoval_fromDegradedState() {
+ // Arrange: Start in a degraded state with a required BACK camera and an EXTERNAL camera,
+ // but missing the required FRONT camera.
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+ FakeCamera(
+ CAMERA_ID_0,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_0, 0, CameraSelector.LENS_FACING_BACK),
+ )
+ }
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_EXTERNAL, CAMERA_ID_EXTERNAL) {
+ FakeCamera(
+ CAMERA_ID_EXTERNAL,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_EXTERNAL, 0, CameraSelector.LENS_FACING_EXTERNAL),
+ )
+ }
+ sourceObservable.updateData(listOf(IDENTIFIER_0, IDENTIFIER_EXTERNAL))
+ assertThat(testCameraRepository.updateCount).isEqualTo(1)
+
+ // Act: Remove the non-essential EXTERNAL camera. The validator will allow this
+ // by default (isChangeInvalid returns false).
+ fakeCameraFactory.removeCamera(CAMERA_ID_EXTERNAL)
+ sourceObservable.updateData(listOf(IDENTIFIER_0))
+
+ // Assert: The update proceeds because it doesn't make the state worse.
+ assertThat(testCameraRepository.updateCount).isEqualTo(2)
+ assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+ assertThat(publicListener.removedCameras).containsExactly(IDENTIFIER_EXTERNAL)
+ }
+
+ @Test
+ fun onNewData_blocksImpactfulRemoval_fromDegradedState() {
+ // Arrange: Start in a degraded state with a required BACK camera and an EXTERNAL camera.
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+ FakeCamera(
+ CAMERA_ID_0,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_0, 0, CameraSelector.LENS_FACING_BACK),
+ )
+ }
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_EXTERNAL, CAMERA_ID_EXTERNAL) {
+ FakeCamera(
+ CAMERA_ID_EXTERNAL,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_EXTERNAL, 0, CameraSelector.LENS_FACING_EXTERNAL),
+ )
+ }
+ sourceObservable.updateData(listOf(IDENTIFIER_0, IDENTIFIER_EXTERNAL))
+ assertThat(testCameraRepository.updateCount).isEqualTo(1)
+
+ // Act: Configure the validator to block the next removal, then attempt to remove the
+ // last required BACK camera.
+ fakeValidator.setNextIsChangeInvalid(true)
+ sourceObservable.updateData(listOf(IDENTIFIER_EXTERNAL))
+
+ // Assert: The update is blocked to prevent losing the last required camera.
+ assertThat(testCameraRepository.updateCount).isEqualTo(1)
+ assertThat(testCameraRepository.lastReceivedIds)
+ .containsExactly(CAMERA_ID_0, CAMERA_ID_EXTERNAL)
+ assertThat(publicListener.removedCameras).isEmpty()
+ }
+
+ @Test
fun startup_notifiesPublicListenerOfInitialCameras() {
// Arrange: Initial state in factory
fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
@@ -250,8 +341,10 @@
private companion object {
private const val CAMERA_ID_0 = "0"
private const val CAMERA_ID_1 = "1"
+ private const val CAMERA_ID_EXTERNAL = "2"
private val IDENTIFIER_0 = CameraIdentifier.create(CAMERA_ID_0)
private val IDENTIFIER_1 = CameraIdentifier.create(CAMERA_ID_1)
+ private val IDENTIFIER_EXTERNAL = CameraIdentifier.create(CAMERA_ID_EXTERNAL)
}
private class MutableObservable<T> : Observable<T> {
@@ -315,4 +408,27 @@
this.shouldThrow = shouldThrow
}
}
+
+ /** A fake implementation of CameraValidator for testing purposes. */
+ private class FakeCameraValidator : CameraValidator {
+ private var nextIsChangeInvalid = false
+
+ fun setNextIsChangeInvalid(isInvalid: Boolean) {
+ nextIsChangeInvalid = isInvalid
+ }
+
+ override fun validateOnFirstInit(cameraRepository: CameraRepository) {
+ // No-op for these tests, as we are not testing initial validation here.
+ }
+
+ override fun isChangeInvalid(
+ currentCameras: Set<CameraInternal>,
+ removedCameras: Set<CameraIdentifier>,
+ ): Boolean {
+ val result = nextIsChangeInvalid
+ // Reset after use so the next check defaults to valid.
+ nextIsChangeInvalid = false
+ return result
+ }
+ }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt
new file mode 100644
index 0000000..256ca8b
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt
@@ -0,0 +1,238 @@
+/*
+ * 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.camera.core.impl
+
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.camera.core.CameraIdentifier
+import androidx.camera.core.CameraSelector
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.impl.fakes.FakeCameraFactory
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+class CameraValidatorTest {
+
+ private lateinit var context: Context
+ private lateinit var packageManager: PackageManager
+ private lateinit var fakeCameraFactory: FakeCameraFactory
+ private lateinit var cameraRepository: CameraRepository
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ packageManager = context.packageManager
+ fakeCameraFactory = FakeCameraFactory()
+ cameraRepository = CameraRepository()
+ }
+
+ @Test
+ fun validateOnFirstInit_succeeds_whenAllRequiredCamerasExist() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ setupCameras(hasBack = true, hasFront = true)
+ val validator = CameraValidator.create(context, null)
+
+ // Act & Assert
+ // Should not throw any exception
+ validator.validateOnFirstInit(cameraRepository)
+ }
+
+ @Test
+ fun validateOnFirstInit_throwsException_whenBackCameraIsMissing() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = false)
+ setupCameras(hasBack = false, hasFront = true) // Missing required back camera
+ val validator = CameraValidator.create(context, null)
+
+ // Act & Assert
+ assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+ validator.validateOnFirstInit(cameraRepository)
+ }
+ }
+
+ @Test
+ fun validateOnFirstInit_throwsException_whenFrontCameraIsMissing() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ setupCameras(hasBack = true, hasFront = false) // Missing required front camera
+ val validator = CameraValidator.create(context, null)
+
+ // Act & Assert
+ assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+ validator.validateOnFirstInit(cameraRepository)
+ }
+ }
+
+ @Test
+ fun validateOnFirstInit_succeeds_whenMissingCameraIsNotInSelectorScope() {
+ // Arrange: System requires both, but repository only has a BACK camera.
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ setupCameras(hasBack = true, hasFront = false)
+ // Act: Create a validator that ONLY cares about the back camera.
+ val validator = CameraValidator.create(context, CameraSelector.DEFAULT_BACK_CAMERA)
+
+ // Assert: No exception, because the missing FRONT camera is out of scope.
+ validator.validateOnFirstInit(cameraRepository)
+ }
+
+ @Test
+ fun validateOnFirstInit_reportsCorrectCameraCount_onFailure() {
+ // Arrange: System requires a back camera, but it's missing. One front camera exists.
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = false)
+ setupCameras(hasBack = false, hasFront = true)
+ val validator = CameraValidator.create(context, null)
+
+ // Act: Catch the exception
+ val exception =
+ assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+ validator.validateOnFirstInit(cameraRepository)
+ }
+
+ // Assert: The exception correctly reports that 1 camera was still available,
+ // which allows recovery logic to proceed with a degraded state.
+ assertThat(exception.availableCameraCount).isEqualTo(1)
+ }
+
+ @Test
+ fun isChangeInvalid_returnsFalse_forRemovingNonRequiredCamera() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ val cameras = setupCameras(hasBack = true, hasFront = true, hasExternal = true)
+ val validator = CameraValidator.create(context, null)
+
+ // Act: Remove the external camera
+ val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_EXTERNAL))
+
+ // Assert
+ assertThat(isInvalid).isFalse()
+ }
+
+ @Test
+ fun isChangeInvalid_returnsTrue_forRemovingRequiredBackCamera() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ val cameras = setupCameras(hasBack = true, hasFront = true)
+ val validator = CameraValidator.create(context, null)
+
+ // Act: Remove the BACK camera
+ val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_BACK))
+
+ // Assert
+ assertThat(isInvalid).isTrue()
+ }
+
+ @Test
+ fun isChangeInvalid_returnsFalse_whenMissingCameraIsNotInSelectorScope() {
+ // Arrange
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ val cameras = setupCameras(hasBack = true, hasFront = true)
+ // This validator only cares about the BACK camera.
+ val validator = CameraValidator.create(context, CameraSelector.DEFAULT_BACK_CAMERA)
+
+ // Act: Remove the FRONT camera.
+ val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_FRONT))
+
+ // Assert: Change is valid because the removed camera was out of scope.
+ assertThat(isInvalid).isFalse()
+ }
+
+ @Test
+ fun isChangeInvalid_allowsNonRequiredRemoval_fromDegradedState() {
+ // Arrange: System requires BACK and FRONT, but we start in a degraded state
+ // with only BACK and an EXTERNAL camera (e.g. after a partial init).
+ setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+ val cameras = setupCameras(hasBack = true, hasFront = false, hasExternal = true)
+ val validator = CameraValidator.create(context, null)
+
+ // Act: Remove the EXTERNAL camera.
+ val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_EXTERNAL))
+
+ // Assert: This change is NOT invalid because we didn't lose a *required* camera
+ // that we actually had. The state did not become worse.
+ assertThat(isInvalid).isFalse()
+ }
+
+ private fun setSystemFeatures(hasBackCamera: Boolean, hasFrontCamera: Boolean) {
+ val shadowPackageManager = Shadows.shadowOf(packageManager)
+ shadowPackageManager.setSystemFeature(PackageManager.FEATURE_CAMERA, hasBackCamera)
+ shadowPackageManager.setSystemFeature(PackageManager.FEATURE_CAMERA_FRONT, hasFrontCamera)
+ }
+
+ private fun setupCameras(
+ hasBack: Boolean = false,
+ hasFront: Boolean = false,
+ hasExternal: Boolean = false,
+ ): Set<CameraInternal> {
+ if (hasBack) {
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_BACK) {
+ FakeCamera(
+ CAMERA_ID_BACK,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_BACK, 0, CameraSelector.LENS_FACING_BACK),
+ )
+ }
+ }
+ if (hasFront) {
+ fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT, CAMERA_ID_FRONT) {
+ FakeCamera(
+ CAMERA_ID_FRONT,
+ null,
+ FakeCameraInfoInternal(CAMERA_ID_FRONT, 0, CameraSelector.LENS_FACING_FRONT),
+ )
+ }
+ }
+ if (hasExternal) {
+ fakeCameraFactory.insertCamera(
+ CameraSelector.LENS_FACING_EXTERNAL,
+ CAMERA_ID_EXTERNAL,
+ ) {
+ FakeCamera(
+ CAMERA_ID_EXTERNAL,
+ null,
+ FakeCameraInfoInternal(
+ CAMERA_ID_EXTERNAL,
+ 0,
+ CameraSelector.LENS_FACING_EXTERNAL,
+ ),
+ )
+ }
+ }
+ cameraRepository.init(fakeCameraFactory)
+ return cameraRepository.cameras
+ }
+
+ private companion object {
+ private const val CAMERA_ID_BACK = "0"
+ private const val CAMERA_ID_FRONT = "1"
+ private const val CAMERA_ID_EXTERNAL = "2"
+
+ private val IDENTIFIER_BACK = CameraIdentifier.create(CAMERA_ID_BACK)
+ private val IDENTIFIER_FRONT = CameraIdentifier.create(CAMERA_ID_FRONT)
+ private val IDENTIFIER_EXTERNAL = CameraIdentifier.create(CAMERA_ID_EXTERNAL)
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
index e16cc31..2743fd0 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
@@ -27,11 +27,13 @@
import androidx.camera.core.concurrent.CameraCoordinator;
import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.ConstantObservable;
import androidx.camera.core.impl.Observable;
import androidx.core.util.Pair;
import androidx.core.util.Preconditions;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
@@ -42,15 +44,17 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
/**
* A {@link CameraFactory} implementation that contains and produces fake cameras.
*
*/
@RestrictTo(Scope.LIBRARY_GROUP)
-public final class FakeCameraFactory implements CameraFactory {
+public final class FakeCameraFactory implements CameraFactory, CameraFactory.Interrogator {
private static final String TAG = "FakeCameraFactory";
@@ -63,7 +67,7 @@
private @NonNull CameraCoordinator mCameraCoordinator = new FakeCameraCoordinator();
private @NonNull Observable<List<CameraIdentifier>> mCameraSourceObservable =
- ConstantObservable.withValue(new ArrayList<>());
+ new ControllableObservable();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Map<String, Pair<Integer, Callable<CameraInternal>>> mCameraMap = new HashMap<>();
@@ -74,6 +78,8 @@
public FakeCameraFactory(@Nullable CameraSelector availableCamerasSelector) {
mAvailableCamerasSelector = availableCamerasSelector;
+
+ updateCameraPresence();
}
@Override
@@ -104,6 +110,8 @@
mCachedCameraIds = null;
mCameraMap.put(cameraId, Pair.create(lensFacing, cameraInternal));
+
+ updateCameraPresence();
}
/**
@@ -152,6 +160,9 @@
// Remove from the map and return the old value.
Pair<Integer, Callable<CameraInternal>> removed = mCameraMap.remove(cameraId);
+
+ updateCameraPresence();
+
if (removed != null) {
return removed.second;
}
@@ -166,19 +177,42 @@
if (mAvailableCamerasSelector == null) {
mCachedCameraIds = Collections.unmodifiableSet(new HashSet<>(mCameraMap.keySet()));
} else {
- mCachedCameraIds = Collections.unmodifiableSet(new HashSet<>(filteredCameraIds()));
+ mCachedCameraIds = Collections.unmodifiableSet(
+ new HashSet<>(filterCameraIds(mCameraMap.keySet())));
}
}
return mCachedCameraIds;
}
- /** Returns a list of camera ids filtered with {@link #mAvailableCamerasSelector}. */
- private @NonNull List<String> filteredCameraIds() {
+ @NonNull
+ @Override
+ public List<String> getAvailableCameraIds(@NonNull List<String> cameraIds) {
+ if (mAvailableCamerasSelector == null) {
+ // No selector, just return the input list but ensure cameras exist in our map.
+ List<String> existingIds = new ArrayList<>();
+ for (String cameraId : cameraIds) {
+ if (mCameraMap.containsKey(cameraId)) {
+ existingIds.add(cameraId);
+ }
+ }
+ return existingIds;
+ }
+ return filterCameraIds(cameraIds);
+ }
+
+ /**
+ * A private helper to apply the CameraSelector filter to any list of camera IDs.
+ * This is used by both getAvailableCameraIds() and the new Interrogator method.
+ */
+ private @NonNull List<String> filterCameraIds(@NonNull Iterable<String> cameraIds) {
Preconditions.checkNotNull(mAvailableCamerasSelector);
final List<String> filteredCameraIds = new ArrayList<>();
- for (Map.Entry<String, Pair<Integer, Callable<CameraInternal>>> entry :
- mCameraMap.entrySet()) {
- final Callable<CameraInternal> callable = entry.getValue().second;
+ for (String cameraId : cameraIds) {
+ if (!mCameraMap.containsKey(cameraId)) {
+ continue;
+ }
+ final Callable<CameraInternal> callable =
+ Objects.requireNonNull(mCameraMap.get(cameraId)).second;
if (callable == null) {
continue;
}
@@ -188,7 +222,7 @@
mAvailableCamerasSelector.filter(
new LinkedHashSet<>(Collections.singleton(camera)));
if (!filteredCameraInternals.isEmpty()) {
- filteredCameraIds.add(entry.getKey());
+ filteredCameraIds.add(cameraId);
}
} catch (Exception exception) {
Logger.e(TAG, "Failed to get access to the camera instance.", exception);
@@ -229,4 +263,58 @@
public void onCameraIdsUpdated(@NonNull List<String> cameraIds) {
}
+
+ /**
+ * A new private helper to push updates to the camera presence observable.
+ */
+ private void updateCameraPresence() {
+ Set<String> availableIds = getAvailableCameraIds();
+ List<CameraIdentifier> identifiers = new ArrayList<>();
+ for (String id : availableIds) {
+ identifiers.add(CameraIdentifier.create(id));
+ }
+
+ // This check is needed because setCameraPresenceSource can overwrite our observable.
+ if (mCameraSourceObservable instanceof ControllableObservable) {
+ ((ControllableObservable) mCameraSourceObservable).updateData(identifiers);
+ }
+ }
+
+ /**
+ * A simple observable implementation that allows internal updates.
+ */
+ private static class ControllableObservable implements Observable<List<CameraIdentifier>> {
+ private Observer<? super List<CameraIdentifier>> mObserver;
+ private Executor mExecutor;
+ private List<CameraIdentifier> mData = new ArrayList<>();
+
+ @Override
+ public void addObserver(@NonNull Executor executor,
+ @NonNull Observer<? super List<CameraIdentifier>> observer) {
+ mExecutor = executor;
+ mObserver = observer;
+ updateData(mData);
+ }
+
+ @Override
+ public void removeObserver(@NonNull Observer<? super List<CameraIdentifier>> observer) {
+ if (Objects.equals(mObserver, observer)) {
+ mObserver = null;
+ mExecutor = null;
+ }
+ }
+
+ @Override
+ public @NonNull ListenableFuture<List<CameraIdentifier>> fetchData() {
+ return Futures.immediateFuture(mData);
+ }
+
+ void updateData(@NonNull List<CameraIdentifier> data) {
+ mData = data;
+ if (mExecutor != null && mObserver != null) {
+ Observer<? super List<CameraIdentifier>> observer = mObserver;
+ mExecutor.execute(() -> observer.onNewData(data));
+ }
+ }
+ }
}
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 5906650..e31fa5c 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -61,12 +61,7 @@
commonTest {
dependencies {
implementation(libs.kotlinTestAnnotationsCommon)
- // TODO(b/375358921) Set to libs.kotlinTest when collection targets Kotlin 2
- // For KotlinWasm, versions of toolchain and stdlib need to be the same for tests:
- // https://youtrack.jetbrains.com/issue/KT-71032
- // So we force kotlin-test to be on Kotlin 2.
- // Published artifacts are unaffected by this, they continue to target Kotlin 1.9
- implementation(libs.kotlinTestForWasmTests)
+ implementation(libs.kotlinTest)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
}
@@ -133,9 +128,6 @@
wasmJsMain {
dependsOn(nonJsMain)
dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
}
wasmJsTest {
@@ -145,9 +137,6 @@
jsMain {
dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
}
jsTest {
@@ -157,15 +146,11 @@
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet {
- def konanTargetFamily = target.konanTarget.family
- if (konanTargetFamily == Family.OSX ||
- konanTargetFamily == Family.IOS ||
- konanTargetFamily == Family.WATCHOS ||
- konanTargetFamily == Family.TVOS) {
+ if (target.konanTarget.family.appleFamily) {
dependsOn(darwinMain)
- } else if (konanTargetFamily == Family.LINUX) {
+ } else if (target.konanTarget.family == Family.LINUX) {
dependsOn(linuxMain)
- } else if (konanTargetFamily == Family.MINGW) {
+ } else if (target.konanTarget.family == Family.MINGW) {
dependsOn(mingwMain)
} else {
throw new GradleException("unknown native target ${target}")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index f8db0710..9fdaa7d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -16,8 +16,11 @@
package androidx.compose.foundation.anchoredDraggable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorizedAnimationSpec
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.AtomicLong
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.anchoredDraggable.AnchoredDraggableTestValue.A
import androidx.compose.foundation.anchoredDraggable.AnchoredDraggableTestValue.B
@@ -33,7 +36,6 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
@@ -63,7 +65,6 @@
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Test
@@ -823,18 +824,66 @@
assertThat(state.currentValue).isEqualTo(C)
}
- private val DefaultSnapAnimationSpec = tween<Float>()
+ @Test
+ fun anchoredDraggable_fling_confirmValueChange_returnsFalse_returnsToSettledAnchor() {
+ val inspectAnimationSpec = InspectSpringAnimationSpec(DefaultSnapAnimationSpec)
+ val (state, modifier) =
+ createStateAndModifier(
+ initialValue = A,
+ Orientation.Horizontal,
+ confirmValueChange = { it != B },
+ snapAnimationSpec = inspectAnimationSpec,
+ )
+ val anchors = DraggableAnchors {
+ A at 0f
+ B at AnchoredDraggableBoxSize.value / 2f
+ C at AnchoredDraggableBoxSize.value
+ }
+ state.updateAnchors(anchors)
- private class HandPumpTestFrameClock : MonotonicFrameClock {
- private val frameCh = Channel<Long>(1)
- private val time = AtomicLong(0)
-
- suspend fun advanceByFrame() {
- frameCh.send(time.getAndAdd(16_000_000L))
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+ WithTouchSlop(0f) {
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier.requiredSize(AnchoredDraggableBoxSize)
+ .testTag(AnchoredDraggableTestTag)
+ .then(modifier)
+ .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
+ .background(Color.Red)
+ )
+ }
+ }
+ }
}
- override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
- return onFrame(frameCh.receive())
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.settledValue).isEqualTo(A)
+
+ rule.onNodeWithTag(AnchoredDraggableTestTag).performTouchInput {
+ swipeRight(endX = right / 2)
+ }
+ assertThat(state.offset).isWithin(0.5f).of(anchors.positionOf(B))
+ rule.waitForIdle()
+
+ assertThat(inspectAnimationSpec.animationWasExecutions).isEqualTo(1)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.settledValue).isEqualTo(A)
+ assertThat(state.offset).isEqualTo(anchors.positionOf(A))
+ }
+
+ private val DefaultSnapAnimationSpec = tween<Float>()
+
+ private class InspectSpringAnimationSpec(private val animation: AnimationSpec<Float>) :
+ AnimationSpec<Float> {
+
+ var animationWasExecutions = 0
+
+ override fun <V : AnimationVector> vectorize(
+ converter: TwoWayConverter<Float, V>
+ ): VectorizedAnimationSpec<V> {
+ animationWasExecutions++
+ return animation.vectorize(converter)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
index d76e785..0055610 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
@@ -29,14 +29,12 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import kotlin.test.BeforeTest
-import kotlin.test.Ignore
import kotlinx.coroutines.runBlocking
import org.junit.Assume
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-@Ignore
@OptIn(ExperimentalFoundationApi::class)
@LargeTest
@RunWith(Parameterized::class)
@@ -66,7 +64,7 @@
}
@Test
- fun noPrefetchingForwardInitially() {
+ fun prefetchingForwardInitially() {
createPager(
modifier = Modifier.size(pagesSizeDp * 1.5f),
pageSize = { PageSize.Fixed(pagesSizeDp) },
@@ -75,10 +73,10 @@
waitForPrefetch()
if (config.beyondViewportPageCount == 0) {
// window will fill automatically 1 extra item
- rule.onNodeWithTag("2").assertDoesNotExist()
+ rule.onNodeWithTag("2").assertExists()
} else {
// window will fill automatically 1 extra item
- rule.onNodeWithTag("3").assertDoesNotExist()
+ rule.onNodeWithTag("3").assertExists()
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
index 2215e8a..6263d26 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
@@ -83,13 +83,16 @@
Action.Measure(prefetchIndex, 0),
Action.Compose(prefetchIndex, 1),
Action.Measure(prefetchIndex, 1),
+ Action.Compose(prefetchIndex, 2),
+ Action.Measure(prefetchIndex, 2),
)
.inOrder()
rule.onNodeWithTag(tagFor(prefetchIndex)).assertExists()
rule.onNodeWithTag(tagFor(2, 0)).assertExists()
rule.onNodeWithTag(tagFor(2, 1)).assertExists()
- rule.onNodeWithTag(tagFor(2, 2)).assertDoesNotExist()
+ rule.onNodeWithTag(tagFor(2, 2)).assertExists()
+ rule.onNodeWithTag(tagFor(2, 3)).assertDoesNotExist()
}
@Test
@@ -158,6 +161,8 @@
Action.Measure(prefetchIndex, 4),
Action.Compose(prefetchIndex, 5),
Action.Measure(prefetchIndex, 5),
+ Action.Compose(prefetchIndex, 6),
+ Action.Measure(prefetchIndex, 6),
)
.inOrder()
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
index 3a912c8..6fc2d19 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
@@ -147,5 +147,5 @@
* of 1 item in the direction of the scroll. The window used will be 1 view port AFTER the
* currently composed items, this includes visible and items composed through beyond bounds.
*/
- @Suppress("MutableBareField") @JvmField var isCacheWindowForPagerEnabled = false
+ @Suppress("MutableBareField") @JvmField var isCacheWindowForPagerEnabled = true
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index cac5397..fe42aa7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -1739,14 +1739,20 @@
override fun calculateSnapOffset(velocity: Float): Float {
val currentOffset = state.requireOffset()
- val target =
+ val proposedTargetValue =
state.anchors.computeTarget(
currentOffset = currentOffset,
velocity = velocity,
positionalThreshold = positionalThreshold,
velocityThreshold = velocityThreshold,
)
- return state.anchors.positionOf(target) - currentOffset
+ val targetValue =
+ if (state.confirmValueChange(proposedTargetValue)) {
+ proposedTargetValue
+ } else {
+ state.settledValue
+ }
+ return state.anchors.positionOf(targetValue) - currentOffset
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
index 7ebf089..8ce42f1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
@@ -97,7 +97,7 @@
fun CacheWindowScope.onVisibleItemsUpdated() {
debugLog { "hasUpdatedVisibleItemsOnce=$hasUpdatedVisibleItemsOnce" }
- if (!hasUpdatedVisibleItemsOnce && enableInitialPrefetch) {
+ if (!hasUpdatedVisibleItemsOnce) {
val prefetchForwardWindow =
with(cacheWindow) { density?.calculateAheadWindow(mainAxisViewportSize) ?: 0 }
// we won't fill the window if we don't have a prefetch window
@@ -478,8 +478,6 @@
val lastVisibleLineIndex: Int
val mainAxisViewportSize: Int
val density: Density?
- val enableInitialPrefetch: Boolean
- get() = true
fun schedulePrefetch(lineIndex: Int, onItemPrefetched: (Int, Int) -> Unit): List<PrefetchHandle>
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
index b25ffaa..7f8a510 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
@@ -28,10 +28,10 @@
@OptIn(ExperimentalFoundationApi::class)
internal class PagerCacheWindowLogic(
- viewportFraction: Float,
+ val cacheWindow: LazyLayoutCacheWindow,
val state: LazyLayoutPrefetchState,
val itemCount: () -> Int,
-) : CacheWindowLogic(LazyLayoutCacheWindow(viewportFraction, viewportFraction)) {
+) : CacheWindowLogic(cacheWindow) {
private val cacheWindowScope = PagerCacheWindowScope(itemCount)
fun onScroll(delta: Float, layoutInfo: PagerMeasureResult) {
@@ -105,9 +105,6 @@
override val density: Density?
get() = layoutInfo.density
- override val enableInitialPrefetch: Boolean
- get() = false
-
override fun schedulePrefetch(
lineIndex: Int,
onItemPrefetched: (Int, Int) -> Unit,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 98f6a6b..322c278 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -36,6 +36,7 @@
import androidx.compose.foundation.internal.requirePrecondition
import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
@@ -351,6 +352,9 @@
internal val pageSizeWithSpacing: Int
get() = pageSize + pageSpacing
+ // non state backed version
+ internal var latestPageSizeWithSpacing: Int = 0
+
/**
* How far the current page needs to scroll so the target page is considered to be the next
* page.
@@ -458,7 +462,23 @@
Snapshot.withoutReadObservation { schedulePrecomposition(firstVisiblePage) }
}
- internal val cacheWindowLogic = PagerCacheWindowLogic(1.0f, prefetchState) { pageCount }
+ /**
+ * Cache window in Pager Initial Layout prefetching happens after the initial measure pass and
+ * latestPageSizeWithSpacing is updated before the prefetching happens.
+ *
+ * For scroll backed prefetching we will use the last known latestPageSizeWithSpacing.
+ */
+ private val pagerCacheWindow =
+ object : LazyLayoutCacheWindow {
+ override fun Density.calculateAheadWindow(viewport: Int): Int =
+ latestPageSizeWithSpacing
+
+ override fun Density.calculateBehindWindow(viewport: Int): Int =
+ latestPageSizeWithSpacing
+ }
+
+ internal val cacheWindowLogic =
+ PagerCacheWindowLogic(pagerCacheWindow, prefetchState) { pageCount }
internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
@@ -689,6 +709,9 @@
// should use.
prefetchState.idealNestedPrefetchCount = result.visiblePagesInfo.size
+ // Update non state backed page size info
+ latestPageSizeWithSpacing = result.pageSize + result.pageSpacing
+
if (!isLookingAhead && hasLookaheadOccurred) {
debugLog { "Applying Approach Measure Result" }
// If there was already a lookahead pass, record this result as Approach result
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
index ac21947..06fdcac 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
@@ -91,7 +91,9 @@
* @param scrim the scrim to show when the pane is levitated to block user interaction with the
* underlying layout and emphasize the levitated pane; by default it will be `null` and no
* scrim will show.
- * @sample androidx.compose.material3.adaptive.samples.levitateAdaptStrategySample
+ * @sample androidx.compose.material3.adaptive.samples.levitateAsBottomSheetSample
+ * @sample androidx.compose.material3.adaptive.samples.levitateAsDialogSample
+ * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog
* @sample androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet
* @see [onlyIf] and [onlyIfSinglePane] for finer control over when the pane should be
* levitated.
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
index 2952f38..e2d7917 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
@@ -30,7 +30,7 @@
* The class is used to create a scrim when a levitated pane is shown, to block the user interaction
* from the underlying layout. See [AdaptStrategy.Levitate] for more detailed info.
*
- * @sample androidx.compose.material3.adaptive.samples.levitateAdaptStrategySample
+ * @sample androidx.compose.material3.adaptive.samples.levitateAsDialogSample
* @param color the color of scrim, by default if [Color.Unspecified] is provided, the pane scaffold
* implementation will use a translucent black color.
* @param onClick the on-click listener of the scrim; usually used to dismiss the levitated pane;
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index 89c7648..fcdb730 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -40,6 +40,7 @@
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -99,7 +100,6 @@
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -204,6 +204,61 @@
@Preview
@Sampled
@Composable
+fun ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog() {
+ val coroutineScope = rememberCoroutineScope()
+ val scaffoldNavigator = levitateAsDialogSample<NavItemData>()
+ val items = listOf("Item 1", "Item 2", "Item 3")
+ val extraItems = listOf("Extra 1", "Extra 2", "Extra 3")
+ val selectedItem = scaffoldNavigator.currentDestination?.contentKey
+
+ ListDetailPaneScaffold(
+ directive = scaffoldNavigator.scaffoldDirective,
+ scaffoldState = scaffoldNavigator.scaffoldState,
+ listPane = {
+ AnimatedPane(modifier = Modifier.preferredWidth(200.dp)) {
+ ListPaneContent(
+ items = items,
+ selectedItem = selectedItem,
+ scaffoldNavigator = scaffoldNavigator,
+ coroutineScope = coroutineScope,
+ )
+ }
+ },
+ detailPane = {
+ AnimatedPane {
+ DetailPaneContent(
+ items = items,
+ selectedItem = selectedItem,
+ scaffoldNavigator = scaffoldNavigator,
+ hasExtraPane = true,
+ coroutineScope = coroutineScope,
+ )
+ }
+ },
+ extraPane = {
+ AnimatedPane {
+ ExtraPaneContent(
+ extraItems = extraItems,
+ selectedItem = selectedItem,
+ scaffoldNavigator = scaffoldNavigator,
+ coroutineScope = coroutineScope,
+ )
+ }
+ },
+ paneExpansionState =
+ rememberPaneExpansionState(
+ keyProvider = scaffoldNavigator.scaffoldValue,
+ anchors = PaneExpansionAnchors,
+ initialAnchoredIndex = 1,
+ ),
+ paneExpansionDragHandle = { state -> PaneExpansionDragHandleSample(state) },
+ )
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
fun SupportingPaneScaffoldSample() {
val coroutineScope = rememberCoroutineScope()
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<NavItemData>()
@@ -254,7 +309,7 @@
* 2. The use of [androidx.compose.material3.adaptive.layout.PaneScaffoldScope.dragToResize] with
* [DockedEdge.Bottom] so that the levitated extra pane can be resized by dragging.
*
- * @see levitateAdaptStrategySample for more usage samples of [AdaptStrategy.Levitate].
+ * @see levitateAsDialogSample for more usage samples of [AdaptStrategy.Levitate].
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@@ -262,17 +317,7 @@
@Composable
fun SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet() {
val coroutineScope = rememberCoroutineScope()
- val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
- val scaffoldNavigator =
- rememberSupportingPaneScaffoldNavigator<NavItemData>(
- scaffoldDirective = scaffoldDirective,
- adaptStrategies =
- SupportingPaneScaffoldDefaults.adaptStrategies(
- extraPaneAdaptStrategy =
- AdaptStrategy.Levitate(alignment = Alignment.BottomCenter)
- .onlyIfSinglePane(scaffoldDirective)
- ),
- )
+ val scaffoldNavigator = levitateAsBottomSheetSample<NavItemData>()
val extraItems = listOf("Extra content")
val selectedItem = NavItemData(index = 0, showExtra = true)
@@ -385,14 +430,14 @@
/**
* This sample shows how to create a [ThreePaneScaffoldNavigator] that will show the extra pane as a
- * modal dialog in a single pane layout when the extra pane is the current destination. The dialog
- * will be centered in the scaffold, with a scrim that clicking on it will dismiss the dialog.
+ * modal dialog when the extra pane is the current destination. The dialog will be centered in the
+ * scaffold, with a scrim that clicking on it will dismiss the dialog.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Sampled
@Composable
-fun <T> levitateAdaptStrategySample(): ThreePaneScaffoldNavigator<T> {
+fun <T> levitateAsDialogSample(): ThreePaneScaffoldNavigator<T> {
val coroutineScope = rememberCoroutineScope()
val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
var navigator: ThreePaneScaffoldNavigator<T>? = null
@@ -417,6 +462,35 @@
return navigator
}
+/**
+ * This sample shows how to create a [ThreePaneScaffoldNavigator] that will show the extra pane as a
+ * bottom sheet in a single pane layout when the extra pane is the current destination.
+ *
+ * Note that besides the navigator, you also need to apply
+ * [androidx.compose.material3.adaptive.layout.PaneScaffoldScope.dragToResize] on the extra pane to
+ * make it be resizable by dragging. See
+ * [SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet] for more info.
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun <T> levitateAsBottomSheetSample(): ThreePaneScaffoldNavigator<T> {
+ val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
+ var navigator: ThreePaneScaffoldNavigator<T>? = null
+ navigator =
+ rememberSupportingPaneScaffoldNavigator<T>(
+ scaffoldDirective = scaffoldDirective,
+ adaptStrategies =
+ SupportingPaneScaffoldDefaults.adaptStrategies(
+ extraPaneAdaptStrategy =
+ AdaptStrategy.Levitate(alignment = Alignment.BottomCenter)
+ .onlyIfSinglePane(scaffoldDirective)
+ ),
+ )
+ return navigator
+}
+
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
@Preview
@Sampled
@@ -585,11 +659,7 @@
else -> MaterialTheme.colorScheme.surface
}
),
- modifier =
- modifier.height(80.dp).fillMaxWidth().semantics {
- contentDescription = title
- selected = isSelected
- },
+ modifier = modifier.height(80.dp).fillMaxWidth().selectable(isSelected, onClick = onClick),
) {
Row(
modifier = Modifier.padding(12.dp),
@@ -623,7 +693,10 @@
modifier: Modifier = Modifier,
coroutineScope: CoroutineScope,
) {
- Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Column(
+ modifier = modifier.selectableGroup(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
items.forEachIndexed { index, item ->
ListCard(
title = item,
@@ -736,7 +809,10 @@
private fun SupportingPaneContent(modifier: Modifier = Modifier) {
val items = listOf("Item 1", "Item 2", "Item 3")
var selectedIndex by rememberSaveable { mutableIntStateOf(-1) }
- Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Column(
+ modifier = modifier.selectableGroup(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
items.forEachIndexed { index, item ->
ListCard(
title = item,
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 a29658d3..04bbcbc 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
@@ -23,7 +23,10 @@
import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
+import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog
import androidx.compose.material3.adaptive.samples.NavigableListDetailPaneScaffoldSample
+import androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSample
+import androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet
import androidx.compose.material3.catalog.library.util.AdaptiveNavigationSuiteSampleSourceUrl
import androidx.compose.material3.catalog.library.util.AdaptiveSampleSourceUrl
import androidx.compose.material3.catalog.library.util.SampleSourceUrl
@@ -312,6 +315,14 @@
ListDetailPaneScaffoldSampleWithExtraPane()
},
Example(
+ name = "ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog",
+ description = AdaptiveExampleDescription,
+ sourceUrl = AdaptiveExampleSourceUrl,
+ isExpressive = false,
+ ) {
+ ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog()
+ },
+ Example(
name = "NavigableListDetailPaneScaffoldSample",
description = AdaptiveExampleDescription,
sourceUrl = AdaptiveExampleSourceUrl,
@@ -319,6 +330,22 @@
) {
NavigableListDetailPaneScaffoldSample()
},
+ Example(
+ name = "SupportingPaneScaffoldSample",
+ description = AdaptiveExampleDescription,
+ sourceUrl = AdaptiveExampleSourceUrl,
+ isExpressive = false,
+ ) {
+ SupportingPaneScaffoldSample()
+ },
+ Example(
+ name = "SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet",
+ description = AdaptiveExampleDescription,
+ sourceUrl = AdaptiveExampleSourceUrl,
+ isExpressive = false,
+ ) {
+ SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet()
+ },
)
private const val BadgeExampleDescription = "Badge examples"
diff --git a/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt b/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
index d5fb7a7..0fe7c58 100644
--- a/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
+++ b/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
@@ -39,10 +39,11 @@
private val RemoteComposeWriter.painter: Painter
get() {
- if (this is RemoteComposeWriterAndroid) {
- this.painter
+ if (this !is RemoteComposeWriterAndroid) {
+ throw Exception("Invalid Writer $this, painter inaccessible")
}
- throw (Exception("Invalid Writer, painter inaccessible"))
+
+ return this.painter
}
@Composable
diff --git a/compose/remote/remote-player-compose/build.gradle b/compose/remote/remote-player-compose/build.gradle
new file mode 100644
index 0000000..278c640
--- /dev/null
+++ b/compose/remote/remote-player-compose/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+import androidx.build.SoftwareType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("AndroidXComposePlugin")
+}
+
+androidx {
+ name = "Remote Compose Player Compose"
+ type = SoftwareType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2025"
+ description = "Compose player for RemoteCompose documents"
+ doNotDocumentReason = "Not shipped externally"
+}
+
+android {
+ namespace = "androidx.compose.remote.player.compose"
+ compileSdk = 35
+ defaultConfig {
+ minSdk = 29
+ }
+}
+
+dependencies {
+ implementation(project(":compose:remote:remote-core"))
+ implementation(project(":compose:remote:remote-player-view"))
+ implementation(libs.androidx.core)
+ implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("androidx.compose.foundation:foundation:1.8.1")
+ implementation("androidx.compose.ui:ui:1.8.1")
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt
new file mode 100644
index 0000000..4655b69
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.remote.player.compose
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.remote.core.RemoteClock.nanoTime
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.SystemClock
+import androidx.compose.remote.player.compose.context.ComposePaintContext
+import androidx.compose.remote.player.compose.context.ComposeRemoteContext
+import androidx.compose.remote.player.view.RemoteComposeDocument
+import androidx.compose.remote.player.view.action.NamedActionHandler
+import androidx.compose.remote.player.view.action.StateUpdaterActionCallback
+import androidx.compose.remote.player.view.state.StateUpdater
+import androidx.compose.remote.player.view.state.StateUpdaterImpl
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChanged
+import androidx.compose.ui.platform.LocalHapticFeedback
+import java.time.Clock
+
+/**
+ * This is a player for a [androidx.compose.remote.player.view.RemoteComposeDocument].
+ *
+ * <p>It displays the document as well as providing the integration with the Android system (e.g.
+ * passing sensor values, etc.). It also exposes player APIs that allows to control how the document
+ * is displayed as well as reacting to document events.
+ */
+@Composable
+internal fun RemoteComposePlayer(
+ document: RemoteComposeDocument,
+ modifier: Modifier = Modifier,
+ theme: Int = -1,
+ debugMode: Int = 0,
+ clock: Clock = SystemClock(),
+ onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> },
+) {
+ var start by remember(document) { mutableLongStateOf(System.nanoTime()) }
+ var lastAnimationTime by remember(document) { mutableFloatStateOf(0.1f) }
+ val haptic = LocalHapticFeedback.current
+
+ val remoteContext by
+ remember(document) {
+ val composeRemoteContext = ComposeRemoteContext(SystemClock())
+ document.initializeContext(composeRemoteContext)
+ composeRemoteContext.isAnimationEnabled = true
+ composeRemoteContext.setDebug(debugMode)
+ composeRemoteContext.theme = theme
+ composeRemoteContext.setHaptic(haptic)
+ composeRemoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, -Float.MAX_VALUE)
+ mutableStateOf<RemoteContext>(composeRemoteContext)
+ }
+
+ val coreDocument = document.document
+ coreDocument.addActionCallback(
+ object :
+ StateUpdaterActionCallback(
+ StateUpdaterImpl(remoteContext),
+ object : NamedActionHandler {
+ override fun execute(name: String, value: Any?, stateUpdater: StateUpdater) {
+ onNamedAction.invoke(name, value, stateUpdater)
+ }
+ },
+ ) {}
+ )
+
+ val dragHappened = remember { mutableStateOf(false) }
+ Canvas(
+ modifier =
+ modifier.pointerInput(remoteContext) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ for (i in 0 until event.changes.size) {
+ val change = event.changes[i]
+ if (change.changedToDown()) {
+ val x = change.position.x
+ val y = change.position.y
+ val time = remoteContext.animationTime
+ remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+ coreDocument.touchDown(remoteContext, x, y)
+ dragHappened.value = false
+ change.consume()
+ }
+ if (change.changedToUp()) {
+ val x = change.position.x
+ val y = change.position.y
+ val time = remoteContext.animationTime
+ remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+ coreDocument.touchUp(remoteContext, x, y, 0f, 0f)
+ if (!dragHappened.value) {
+ coreDocument.onClick(remoteContext, x, y)
+ }
+ change.consume()
+ }
+ if (change.positionChanged()) {
+ val x = change.position.x
+ val y = change.position.y
+ val time = remoteContext.animationTime
+ remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+ coreDocument.touchDrag(remoteContext, x, y)
+ dragHappened.value = true
+ change.consume()
+ }
+ }
+ }
+ }
+ }
+ ) {
+ drawIntoCanvas {
+ it.save()
+
+ val nanoStart = nanoTime(clock)
+ val animationTime: Float = (nanoStart - start) * 1E-9f
+ remoteContext.animationTime = animationTime
+ remoteContext.loadFloat(RemoteContext.ID_ANIMATION_TIME, animationTime)
+ val loopTime: Float = animationTime - lastAnimationTime
+ remoteContext.loadFloat(RemoteContext.ID_ANIMATION_DELTA_TIME, loopTime)
+ lastAnimationTime = animationTime
+ remoteContext.currentTime = clock.millis()
+
+ remoteContext.density = density
+ remoteContext.mWidth = size.width
+ remoteContext.mHeight = size.height
+
+ remoteContext.loadFloat(RemoteContext.ID_FONT_SIZE, 30f)
+
+ remoteContext.setPaintContext(
+ ComposePaintContext(remoteContext as ComposeRemoteContext, it)
+ )
+ document.paint(remoteContext, 0)
+ it.restore()
+ }
+ }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt
new file mode 100644
index 0000000..6e40a4f
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.remote.player.compose
+
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.size
+import androidx.compose.remote.core.CoreDocument
+import androidx.compose.remote.core.operations.Theme
+import androidx.compose.remote.player.view.RemoteComposeDocument
+import androidx.compose.remote.player.view.state.StateUpdater
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/** A player of a [CoreDocument] */
+@Composable
+public fun RemoteDocumentPlayer(
+ document: CoreDocument,
+ documentWidth: Int,
+ documentHeight: Int,
+ modifier: Modifier = Modifier,
+ debugMode: Int = 0,
+ onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> },
+) {
+ var inDarkTheme by remember { mutableStateOf(false) }
+ var playbackTheme by remember { mutableIntStateOf(Theme.UNSPECIFIED) }
+
+ val remoteDoc = remember(document) { RemoteComposeDocument(document) }
+
+ inDarkTheme =
+ when (AppCompatDelegate.getDefaultNightMode()) {
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> isSystemInDarkTheme()
+ AppCompatDelegate.MODE_NIGHT_YES -> true
+ AppCompatDelegate.MODE_NIGHT_NO -> false
+ AppCompatDelegate.MODE_NIGHT_UNSPECIFIED -> isSystemInDarkTheme()
+ else -> {
+ false
+ }
+ }
+
+ playbackTheme =
+ if (inDarkTheme) {
+ Theme.DARK
+ } else {
+ Theme.LIGHT
+ }
+
+ RemoteComposePlayer(
+ document = remoteDoc,
+ modifier = modifier.size(documentWidth.dp, documentHeight.dp),
+ theme = playbackTheme,
+ debugMode = debugMode,
+ onNamedAction = onNamedAction,
+ )
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt
new file mode 100644
index 0000000..ce3d715
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt
@@ -0,0 +1,370 @@
+/*
+ * 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.remote.player.compose.context
+
+import android.annotation.SuppressLint
+import android.graphics.*
+import android.graphics.Typeface.CustomFallbackBuilder
+import android.graphics.fonts.Font
+import android.graphics.fonts.FontFamily
+import android.graphics.fonts.FontStyle
+import android.graphics.fonts.FontVariationAxis
+import android.os.Build
+import android.util.Log
+import androidx.compose.remote.core.MatrixAccess
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.operations.ShaderData
+import androidx.compose.remote.core.operations.Utils
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.remote.core.operations.paint.PaintChanges
+import androidx.compose.remote.player.compose.utils.remoteToBlendMode
+import androidx.compose.remote.player.compose.utils.remoteToPorterDuffMode
+import androidx.compose.remote.player.compose.utils.toPaintingStyle
+import androidx.compose.remote.player.compose.utils.toStrokeCap
+import androidx.compose.remote.player.compose.utils.toStrokeJoin
+import androidx.compose.remote.player.view.platform.AndroidRemoteContext
+import androidx.compose.ui.graphics.NativePaint
+import java.io.File
+import java.io.IOException
+import java.nio.ByteBuffer
+
+/** A [PaintChanges] implementation for [ComposePaintContext]. */
+internal class ComposePaintChanges(
+ private val remoteContext: RemoteContext,
+ private val getPaint: () -> androidx.compose.ui.graphics.Paint,
+) : PaintChanges {
+ private var fontBuilder: Font.Builder? = null
+ val tmpMatrix: Matrix = Matrix()
+ var tileModes: Array<Shader.TileMode> =
+ arrayOf(Shader.TileMode.CLAMP, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR)
+
+ override fun setTextSize(size: Float) {
+ getNativePaint().textSize = size
+ }
+
+ override fun setTypeFace(fontType: Int, weight: Int, italic: Boolean) {
+ when (fontType) {
+ PaintBundle.FONT_TYPE_DEFAULT ->
+ if (weight == 400 && !italic) { // for normal case
+ getNativePaint().setTypeface(Typeface.DEFAULT)
+ } else {
+ getNativePaint().setTypeface(Typeface.create(Typeface.DEFAULT, weight, italic))
+ }
+ PaintBundle.FONT_TYPE_SERIF ->
+ if (weight == 400 && !italic) { // for normal case
+ getNativePaint().setTypeface(Typeface.SERIF)
+ } else {
+ getNativePaint().setTypeface(Typeface.create(Typeface.SERIF, weight, italic))
+ }
+ PaintBundle.FONT_TYPE_SANS_SERIF ->
+ if (weight == 400 && !italic) { // for normal case
+ getNativePaint().setTypeface(Typeface.SANS_SERIF)
+ } else {
+ getNativePaint()
+ .setTypeface(Typeface.create(Typeface.SANS_SERIF, weight, italic))
+ }
+ PaintBundle.FONT_TYPE_MONOSPACE ->
+ if (weight == 400 && !italic) { // for normal case
+ getNativePaint().setTypeface(Typeface.MONOSPACE)
+ } else {
+ getNativePaint()
+ .setTypeface(Typeface.create(Typeface.MONOSPACE, weight, italic))
+ }
+ else -> {
+ val fi = remoteContext.getObject(fontType) as RemoteContext.FontInfo?
+ var builder = fi!!.fontBuilder as Font.Builder?
+ if (builder == null) {
+ builder = createFontBuilder(fi.mFontData, weight, italic)
+ fi.fontBuilder = builder
+ }
+ fontBuilder = builder
+ setAxis(null)
+ }
+ }
+ }
+
+ override fun setShaderMatrix(matrixId: Float) {
+ val id = Utils.idFromNan(matrixId)
+ if (id == 0) {
+ getNativePaint().shader.setLocalMatrix(null)
+ return
+ }
+ val matAccess = remoteContext.getObject(id) as MatrixAccess?
+ tmpMatrix.setValues(MatrixAccess.to3x3(matAccess!!.get()))
+ val s: Shader = getNativePaint().shader
+ s.setLocalMatrix(tmpMatrix)
+ }
+
+ /**
+ * @param fontType String to be looked up in system
+ * @param weight the weight of the font
+ * @param italic if the font is italic
+ */
+ override fun setTypeFace(fontType: String, weight: Int, italic: Boolean) {
+ val path = getFontPath(fontType)
+ fontBuilder = Font.Builder(File(path!!))
+ fontBuilder!!.setWeight(weight)
+ fontBuilder!!.setSlant(
+ if (italic) FontStyle.FONT_SLANT_ITALIC else FontStyle.FONT_SLANT_UPRIGHT
+ )
+ setAxis(null)
+ }
+
+ private fun createFontBuilder(data: ByteArray, weight: Int, italic: Boolean): Font.Builder {
+ val buffer = ByteBuffer.allocateDirect(data.size)
+
+ // 2. Put the fontBytes into the direct buffer.
+ buffer.put(data)
+ buffer.rewind()
+ fontBuilder = Font.Builder(buffer)
+ fontBuilder!!.setWeight(weight)
+ fontBuilder!!.setSlant(
+ if (italic) FontStyle.FONT_SLANT_ITALIC else FontStyle.FONT_SLANT_UPRIGHT
+ )
+ setAxis(null)
+ return fontBuilder!!
+ }
+
+ private fun setAxis(axis: Array<FontVariationAxis?>?) {
+ var font: Font?
+ try {
+ if (axis != null) {
+ fontBuilder!!.setFontVariationSettings(axis)
+ }
+ font = fontBuilder!!.build()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ throw RuntimeException(e)
+ }
+
+ val fontFamilyBuilder = FontFamily.Builder(font)
+ val fontFamily = fontFamilyBuilder.build()
+ val typeface = CustomFallbackBuilder(fontFamily).setSystemFallback("sans-serif").build()
+ getNativePaint().setTypeface(typeface)
+ }
+
+ private fun getFontPath(fontName: String): String? {
+ var fontName = fontName
+ val fontsDir = File(SYSTEM_FONTS_PATH)
+ if (!fontsDir.exists() || !fontsDir.isDirectory()) {
+ Log.i(TAG, "System fonts directory not found")
+ return null
+ }
+
+ val fontFiles = fontsDir.listFiles()
+ if (fontFiles == null) {
+ Log.i(TAG, "Unable to list font files")
+ return null
+ }
+ fontName = fontName.lowercase()
+ for (fontFile in fontFiles) {
+ if (fontFile.getName().lowercase().contains(fontName)) {
+ return fontFile.absolutePath
+ }
+ }
+ Log.i(TAG, "font \"$fontName\" not found")
+ return null
+ }
+
+ /**
+ * Set the font variation axes
+ *
+ * @param tags tags
+ * @param values values
+ */
+ override fun setFontVariationAxes(tags: Array<String>, values: FloatArray) {
+ val axes = arrayOfNulls<FontVariationAxis>(tags.size)
+ for (i in tags.indices) {
+ axes[i] = FontVariationAxis(tags[i], values[i])
+ }
+ setAxis(axes)
+ }
+
+ /**
+ * Set the texture shader
+ *
+ * @param bitmapId the id of the bitmap to use
+ * @param tileX The tiling mode for x to draw the bitmap in.
+ * @param tileY The tiling mode for y to draw the bitmap in.
+ * @param filterMode the filter mode to be used when sampling from this shader.
+ * @param maxAnisotropy The Anisotropy value to use for filtering. Must be greater than 0.
+ */
+ override fun setTextureShader(
+ bitmapId: Int,
+ tileX: Short,
+ tileY: Short,
+ filterMode: Short,
+ maxAnisotropy: Short,
+ ) {
+
+ // TODO implement getBitmap(bitmapId)
+ val bitmap = remoteContext.mRemoteComposeState.getFromId(bitmapId) as Bitmap ?: return
+ val bs =
+ BitmapShader(
+ bitmap,
+ Shader.TileMode.entries[tileX.toInt()],
+ Shader.TileMode.entries[tileY.toInt()],
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (filterMode > 0) {
+ bs.filterMode = filterMode.toInt()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ bs.maxAnisotropy = filterMode.toInt()
+ }
+ }
+ }
+ getPaint().shader = bs
+ // Todo cache shader once created limit cache to 10 shaders
+ }
+
+ override fun setStrokeWidth(width: Float) {
+ getPaint().strokeWidth = width
+ }
+
+ override fun setColor(color: Int) {
+ getNativePaint().setColor(color)
+ }
+
+ override fun setStrokeCap(cap: Int) {
+ getPaint().strokeCap = Paint.Cap.entries[cap].toStrokeCap()
+ }
+
+ override fun setStyle(style: Int) {
+ getPaint().style = Paint.Style.entries[style].toPaintingStyle()
+ }
+
+ @SuppressLint("NewApi")
+ override fun setShader(shaderId: Int) {
+ // TODO this stuff should check the shader creation
+ if (shaderId == 0) {
+ getNativePaint().setShader(null)
+ return
+ }
+ val data: ShaderData? = getShaderData(shaderId)
+ if (data == null) {
+ return
+ }
+ val shader = RuntimeShader(remoteContext.getText(data.shaderTextId)!!)
+ var names = data.getUniformFloatNames()
+ for (i in names.indices) {
+ val name = names[i]
+ val `val` = data.getUniformFloats(name)
+ shader.setFloatUniform(name, `val`)
+ }
+ names = data.getUniformIntegerNames()
+ for (i in names.indices) {
+ val name = names[i]
+ val `val` = data.getUniformInts(name)
+ shader.setIntUniform(name, `val`)
+ }
+ names = data.getUniformBitmapNames()
+ for (i in names.indices) {
+ val name = names[i]
+ val `val` = data.getUniformBitmapId(name)
+ val androidContext = remoteContext as AndroidRemoteContext
+ val bitmap = androidContext.mRemoteComposeState.getFromId(`val`) as Bitmap?
+ val bitmapShader = BitmapShader(bitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ shader.setInputShader(name, bitmapShader)
+ }
+ getNativePaint().setShader(shader)
+ }
+
+ override fun setImageFilterQuality(quality: Int) {
+ getNativePaint().isFilterBitmap = quality == 1
+ }
+
+ override fun setBlendMode(mode: Int) {
+ getPaint().blendMode = remoteToBlendMode(mode)!!
+ }
+
+ override fun setAlpha(a: Float) {
+ getNativePaint().setAlpha((255 * a).toInt())
+ }
+
+ override fun setStrokeMiter(miter: Float) {
+ getNativePaint().strokeMiter = miter
+ }
+
+ override fun setStrokeJoin(join: Int) {
+ getPaint().strokeJoin = Paint.Join.entries[join].toStrokeJoin()
+ }
+
+ override fun setFilterBitmap(filter: Boolean) {
+ getNativePaint().isFilterBitmap = filter
+ }
+
+ override fun setAntiAlias(aa: Boolean) {
+ getPaint().isAntiAlias = aa
+ }
+
+ override fun clear(mask: Long) {
+ if ((mask and (1L shl PaintBundle.COLOR_FILTER)) != 0L) {
+ getNativePaint().setColorFilter(null)
+ }
+ }
+
+ override fun setLinearGradient(
+ colors: IntArray,
+ stops: FloatArray?,
+ startX: Float,
+ startY: Float,
+ endX: Float,
+ endY: Float,
+ tileMode: Int,
+ ) {
+ getNativePaint()
+ .setShader(
+ LinearGradient(startX, startY, endX, endY, colors, stops, tileModes[tileMode])
+ )
+ }
+
+ override fun setRadialGradient(
+ colors: IntArray,
+ stops: FloatArray?,
+ centerX: Float,
+ centerY: Float,
+ radius: Float,
+ tileMode: Int,
+ ) {
+ getNativePaint()
+ .setShader(RadialGradient(centerX, centerY, radius, colors, stops, tileModes[tileMode]))
+ }
+
+ override fun setSweepGradient(
+ colors: IntArray,
+ stops: FloatArray?,
+ centerX: Float,
+ centerY: Float,
+ ) {
+ getNativePaint().setShader(SweepGradient(centerX, centerY, colors, stops))
+ }
+
+ override fun setColorFilter(color: Int, mode: Int) {
+ val porterDuffMode = remoteToPorterDuffMode(mode)
+ getNativePaint().setColorFilter(PorterDuffColorFilter(color, porterDuffMode))
+ }
+
+ private fun getNativePaint(): NativePaint = getPaint().asFrameworkPaint()
+
+ private fun getShaderData(id: Int): ShaderData? {
+ return remoteContext.mRemoteComposeState.getFromId(id) as ShaderData?
+ }
+
+ companion object {
+ private const val TAG = "ComposePaintContext"
+ private const val SYSTEM_FONTS_PATH: String = "/system/fonts/"
+ }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt
new file mode 100644
index 0000000..0cbd3dc
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt
@@ -0,0 +1,698 @@
+/*
+ * 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.remote.player.compose.context
+
+import android.graphics.Bitmap
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.Typeface
+import android.os.Build
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.TextUtils
+import androidx.compose.remote.core.PaintContext
+import androidx.compose.remote.core.Platform
+import androidx.compose.remote.core.operations.ClipPath
+import androidx.compose.remote.core.operations.layout.managers.TextLayout
+import androidx.compose.remote.core.operations.layout.modifiers.GraphicsLayerModifierOperation
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.remote.player.compose.utils.FloatsToPath
+import androidx.compose.remote.player.compose.utils.copy
+import androidx.compose.remote.player.view.platform.AndroidComputedTextLayout
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.PathMeasure
+import androidx.compose.ui.graphics.PathOperation
+import androidx.compose.ui.graphics.asAndroidPath
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.nativeCanvas
+import kotlin.math.atan2
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * A [PaintContext] implementation for [androidx.compose.remote.player.compose.RemoteComposePlayer].
+ */
+internal class ComposePaintContext(
+ remoteContext: ComposeRemoteContext,
+ private var canvas: Canvas,
+) : PaintContext(remoteContext) {
+
+ var paint = Paint()
+ var paintList: MutableList<Paint> = mutableListOf()
+ var tmpRect: Rect = Rect() // use in calculation of bounds
+ var node: RenderNode? = null
+ var mainCanvas: Canvas? = null
+ var previousCanvas: Canvas? = null
+ var canvasCache: MutableMap<Bitmap, Canvas> = mutableMapOf()
+ private var cachedFontMetrics: android.graphics.Paint.FontMetrics? = null
+ private val cachedPaintChanges =
+ ComposePaintChanges(remoteContext = remoteContext, getPaint = { this.paint })
+
+ override fun drawBitmap(
+ imageId: Int,
+ srcLeft: Int,
+ srcTop: Int,
+ srcRight: Int,
+ srcBottom: Int,
+ dstLeft: Int,
+ dstTop: Int,
+ dstRight: Int,
+ dstBottom: Int,
+ cdId: Int,
+ ) {
+ val androidContext = mContext as ComposeRemoteContext
+ if (androidContext.mRemoteComposeState.containsId(imageId)) {
+ val bitmap = androidContext.mRemoteComposeState.getFromId(imageId) as Bitmap?
+ bitmap?.let {
+ nativeCanvas()
+ .drawBitmap(
+ bitmap,
+ Rect(srcLeft, srcTop, srcRight, srcBottom),
+ Rect(dstLeft, dstTop, dstRight, dstBottom),
+ paint.asFrameworkPaint(),
+ )
+ }
+ }
+ }
+
+ override fun scale(scaleX: Float, scaleY: Float) {
+ canvas.scale(scaleX, scaleY)
+ }
+
+ override fun translate(translateX: Float, translateY: Float) {
+ canvas.translate(translateX, translateY)
+ }
+
+ override fun drawArc(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ startAngle: Float,
+ sweepAngle: Float,
+ ) {
+ canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, false, paint)
+ }
+
+ override fun drawSector(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ startAngle: Float,
+ sweepAngle: Float,
+ ) {
+ canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, true, paint)
+ }
+
+ override fun drawBitmap(id: Int, left: Float, top: Float, right: Float, bottom: Float) {
+ val androidContext = mContext as ComposeRemoteContext
+ if (androidContext.mRemoteComposeState.containsId(id)) {
+ val bitmap = androidContext.mRemoteComposeState.getFromId(id) as Bitmap?
+ val src = Rect(0, 0, bitmap!!.getWidth(), bitmap.getHeight())
+ val dst = RectF(left, top, right, bottom)
+ nativeCanvas().drawBitmap(bitmap, src, dst, paint.asFrameworkPaint())
+ }
+ }
+
+ override fun drawCircle(centerX: Float, centerY: Float, radius: Float) {
+ canvas.drawCircle(Offset(centerX, centerY), radius, paint)
+ }
+
+ override fun drawLine(x1: Float, y1: Float, x2: Float, y2: Float) {
+ canvas.drawLine(Offset(x1, y1), Offset(x2, y2), paint)
+ }
+
+ override fun drawOval(left: Float, top: Float, right: Float, bottom: Float) {
+ canvas.drawOval(left, top, right, bottom, paint)
+ }
+
+ override fun drawPath(id: Int, start: Float, end: Float) {
+ canvas.drawPath(getPath(id, start, end), paint)
+ }
+
+ override fun drawRect(left: Float, top: Float, right: Float, bottom: Float) {
+ canvas.drawRect(left, top, right, bottom, paint)
+ }
+
+ override fun savePaint() {
+ paintList.add(paint.copy())
+ }
+
+ override fun restorePaint() {
+ paint = paintList.removeAt(paintList.size - 1)
+ }
+
+ override fun replacePaint(paint: PaintBundle) {
+ this.paint.asFrameworkPaint().reset()
+ applyPaint(paint)
+ }
+
+ override fun drawRoundRect(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ radiusX: Float,
+ radiusY: Float,
+ ) {
+ canvas.drawRoundRect(left, top, right, bottom, radiusX, radiusY, paint)
+ }
+
+ override fun drawTextOnPath(textId: Int, pathId: Int, hOffset: Float, vOffset: Float) {
+ nativeCanvas()
+ .drawTextOnPath(
+ getText(textId)!!,
+ getNativePath(pathId, 0f, 1f),
+ hOffset,
+ vOffset,
+ paint.asFrameworkPaint(),
+ )
+ }
+
+ override fun getTextBounds(textId: Int, start: Int, end: Int, flags: Int, bounds: FloatArray) {
+ val str = getText(textId)
+
+ val endSanitized =
+ if (end == -1 || end > str!!.length) {
+ str!!.length
+ } else end
+
+ val paint = paint.asFrameworkPaint()
+ if (cachedFontMetrics == null) {
+ cachedFontMetrics = paint.getFontMetrics()
+ }
+ paint.getFontMetrics(cachedFontMetrics)
+ paint.getTextBounds(str, start, endSanitized, tmpRect)
+ if ((flags and TEXT_MEASURE_SPACES) != 0) {
+ bounds[0] = 0f
+ bounds[2] = paint.measureText(str, start, endSanitized)
+ } else {
+ bounds[0] = tmpRect.left.toFloat()
+ if ((flags and TEXT_MEASURE_MONOSPACE_WIDTH) != 0) {
+ bounds[2] = paint.measureText(str, start, endSanitized) - tmpRect.left
+ } else {
+ bounds[2] = tmpRect.right.toFloat()
+ }
+ }
+
+ if ((flags and TEXT_MEASURE_FONT_HEIGHT) != 0) {
+ bounds[1] = cachedFontMetrics!!.ascent.roundToInt().toFloat()
+ bounds[3] = cachedFontMetrics!!.descent.roundToInt().toFloat()
+ } else {
+ bounds[1] = tmpRect.top.toFloat()
+ bounds[3] = tmpRect.bottom.toFloat()
+ }
+ }
+
+ override fun layoutComplexText(
+ textId: Int,
+ start: Int,
+ end: Int,
+ alignment: Int,
+ overflow: Int,
+ maxLines: Int,
+ maxWidth: Float,
+ flags: Int,
+ ): Platform.ComputedTextLayout? {
+ val str = getText(textId)
+ if (str == null) {
+ return null
+ }
+
+ val endSanitized =
+ if (end == -1 || end > str.length) {
+ str.length
+ } else end
+
+ val textPaint = TextPaint()
+ textPaint.set(paint.asFrameworkPaint())
+ val staticLayoutBuilder =
+ StaticLayout.Builder.obtain(str, start, endSanitized, textPaint, maxWidth.toInt())
+ when (alignment) {
+ TextLayout.TEXT_ALIGN_RIGHT,
+ TextLayout.TEXT_ALIGN_END ->
+ staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_OPPOSITE)
+ TextLayout.TEXT_ALIGN_CENTER ->
+ staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_CENTER)
+ else -> staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_NORMAL)
+ }
+ when (overflow) {
+ TextLayout.OVERFLOW_ELLIPSIS ->
+ staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.END)
+ TextLayout.OVERFLOW_MIDDLE_ELLIPSIS ->
+ staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.MIDDLE)
+ TextLayout.OVERFLOW_START_ELLIPSIS ->
+ staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.START)
+ else -> {}
+ }
+ staticLayoutBuilder.setMaxLines(maxLines)
+ staticLayoutBuilder.setIncludePad(false)
+
+ val staticLayout = staticLayoutBuilder.build()
+ return AndroidComputedTextLayout(
+ staticLayout,
+ staticLayout.width.toFloat(),
+ staticLayout.height.toFloat(),
+ )
+ }
+
+ override fun drawTextRun(
+ textId: Int,
+ start: Int,
+ end: Int,
+ contextStart: Int,
+ contextEnd: Int,
+ x: Float,
+ y: Float,
+ rtl: Boolean,
+ ) {
+ var textToPaint = getText(textId)
+ if (textToPaint == null) {
+ return
+ }
+ if (end == -1) {
+ if (start != 0) {
+ textToPaint = textToPaint.substring(start)
+ }
+ } else if (end > textToPaint.length) {
+ textToPaint = textToPaint.substring(start)
+ } else {
+ textToPaint = textToPaint.substring(start, end)
+ }
+
+ nativeCanvas().drawText(textToPaint, x, y, paint.asFrameworkPaint())
+ }
+
+ override fun drawComplexText(computedTextLayout: Platform.ComputedTextLayout?) {
+ if (computedTextLayout == null) {
+ return
+ }
+ val staticLayout = (computedTextLayout as AndroidComputedTextLayout).get()
+ staticLayout.draw(nativeCanvas())
+ }
+
+ override fun drawTweenPath(
+ path1Id: Int,
+ path2Id: Int,
+ tween: Float,
+ start: Float,
+ stop: Float,
+ ) {
+ canvas.drawPath(getPath(path1Id, path2Id, tween, start, stop), paint)
+ }
+
+ override fun tweenPath(out: Int, path1: Int, path2: Int, tween: Float) {
+ val p: FloatArray = getPathArray(path1, path2, tween)
+ val androidContext = mContext as ComposeRemoteContext
+ androidContext.mRemoteComposeState.putPathData(out, p)
+ }
+
+ override fun combinePath(out: Int, path1: Int, path2: Int, operation: Byte) {
+ val p1 = getPath(path1, 0f, 1f)
+ val p2 = getPath(path2, 0f, 1f)
+ val op =
+ arrayOf(
+ PathOperation.Difference,
+ PathOperation.Intersect,
+ PathOperation.ReverseDifference,
+ PathOperation.Union,
+ PathOperation.Xor,
+ )
+ val p = Path.combine(op[operation.toInt()], p1, p2)
+
+ val androidContext = mContext as ComposeRemoteContext
+ androidContext.mRemoteComposeState.putPath(out, p)
+ }
+
+ override fun applyPaint(paintData: PaintBundle) {
+ paintData.applyPaintChange(this, cachedPaintChanges)
+ }
+
+ override fun matrixScale(scaleX: Float, scaleY: Float, centerX: Float, centerY: Float) {
+ if (centerX.isNaN()) {
+ canvas.scale(scaleX, scaleY)
+ } else {
+ nativeCanvas().scale(scaleX, scaleY, centerX, centerY)
+ }
+ }
+
+ override fun matrixTranslate(translateX: Float, translateY: Float) {
+ canvas.translate(translateX, translateY)
+ }
+
+ override fun matrixSkew(skewX: Float, skewY: Float) {
+ canvas.skew(skewX, skewY)
+ }
+
+ override fun matrixRotate(rotate: Float, pivotX: Float, pivotY: Float) {
+ if (pivotX.isNaN()) {
+ canvas.rotate(rotate)
+ } else {
+ nativeCanvas().rotate(rotate, pivotX, pivotY)
+ }
+ }
+
+ override fun matrixSave() {
+ canvas.save()
+ }
+
+ override fun matrixRestore() {
+ canvas.restore()
+ }
+
+ override fun clipRect(left: Float, top: Float, right: Float, bottom: Float) {
+ canvas.clipRect(left, top, right, bottom)
+ }
+
+ override fun clipPath(pathId: Int, regionOp: Int) {
+ val path = getPath(pathId, 0f, 1f)
+ if (regionOp == ClipPath.DIFFERENCE) {
+ canvas.clipPath(path, ClipOp.Difference)
+ } else {
+ canvas.clipPath(path, ClipOp.Intersect)
+ }
+ }
+
+ override fun roundedClipRect(
+ width: Float,
+ height: Float,
+ topStart: Float,
+ topEnd: Float,
+ bottomStart: Float,
+ bottomEnd: Float,
+ ) {
+ val roundedPath = Path()
+ val roundRect =
+ RoundRect(
+ left = 0f,
+ top = 0f,
+ right = width,
+ bottom = height,
+ topLeftCornerRadius = CornerRadius(topStart, topStart),
+ topRightCornerRadius = CornerRadius(topEnd, topEnd),
+ bottomRightCornerRadius = CornerRadius(bottomEnd, bottomEnd),
+ bottomLeftCornerRadius = CornerRadius(bottomStart, bottomStart),
+ )
+ roundedPath.addRoundRect(roundRect)
+ canvas.clipPath(roundedPath)
+ }
+
+ override fun reset() {
+ with(paint.asFrameworkPaint()) {
+ // With out calling setTypeface before or after paint is reset()
+ // Variable type fonts corrupt memory resulting in a
+ // segmentation violation
+ setTypeface(Typeface.DEFAULT)
+ reset()
+ }
+ }
+
+ override fun startGraphicsLayer(w: Int, h: Int) {
+ val newNode = RenderNode("layer")
+ newNode.setPosition(0, 0, w, h)
+ node = newNode
+ previousCanvas = canvas
+ canvas = Canvas(newNode.beginRecording())
+ }
+
+ override fun setGraphicsLayer(attributes: HashMap<Int?, in Any>) {
+ node?.let {
+ val node = it
+ var hasBlurEffect = false
+ var hasOutline = false
+ for (key in attributes.keys) {
+ val value = attributes.get(key)
+ when (key) {
+ GraphicsLayerModifierOperation.SCALE_X -> node.scaleX = value as Float
+ GraphicsLayerModifierOperation.SCALE_Y -> node.scaleY = value as Float
+ GraphicsLayerModifierOperation.ROTATION_X -> node.rotationX = value as Float
+ GraphicsLayerModifierOperation.ROTATION_Y -> node.rotationY = value as Float
+ GraphicsLayerModifierOperation.ROTATION_Z -> node.rotationZ = value as Float
+ GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_X ->
+ node.pivotX = value as Float * node.width
+ GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_Y ->
+ node.pivotY = value as Float * node.width
+ GraphicsLayerModifierOperation.TRANSLATION_X ->
+ node.translationX = value as Float
+ GraphicsLayerModifierOperation.TRANSLATION_Y ->
+ node.translationY = value as Float
+ GraphicsLayerModifierOperation.TRANSLATION_Z ->
+ node.translationZ = value as Float
+ GraphicsLayerModifierOperation.SHAPE -> hasOutline = true
+ GraphicsLayerModifierOperation.SHADOW_ELEVATION ->
+ node.elevation = value as Float
+ GraphicsLayerModifierOperation.ALPHA -> node.alpha = value as Float
+ GraphicsLayerModifierOperation.CAMERA_DISTANCE ->
+ node.setCameraDistance(value as Float)
+ GraphicsLayerModifierOperation.SPOT_SHADOW_COLOR ->
+ node.spotShadowColor = value as Int
+ GraphicsLayerModifierOperation.AMBIENT_SHADOW_COLOR ->
+ node.ambientShadowColor = value as Int
+ GraphicsLayerModifierOperation.HAS_BLUR -> hasBlurEffect = (value as Int?) != 0
+ }
+ }
+ if (hasOutline) {
+ val outline = Outline()
+ outline.alpha = 1f
+ val oShape = attributes.get(GraphicsLayerModifierOperation.SHAPE)
+ if (oShape != null) {
+ val oShapeRadius = attributes.get(GraphicsLayerModifierOperation.SHAPE_RADIUS)
+ val type = oShape as Int
+ if (type == GraphicsLayerModifierOperation.SHAPE_RECT) {
+ outline.setRect(0, 0, node.width, node.height)
+ } else if (type == GraphicsLayerModifierOperation.SHAPE_ROUND_RECT) {
+ if (oShapeRadius != null) {
+ val radius = oShapeRadius as Float
+ outline.setRoundRect(Rect(0, 0, node.width, node.height), radius)
+ } else {
+ outline.setRect(0, 0, node.width, node.height)
+ }
+ } else if (type == GraphicsLayerModifierOperation.SHAPE_CIRCLE) {
+ val radius: Float = min(node.width, node.height) / 2f
+ outline.setRoundRect(Rect(0, 0, node.width, node.height), radius)
+ }
+ }
+ node.setOutline(outline)
+ }
+ if (hasBlurEffect) {
+ val oBlurRadiusX = attributes.get(GraphicsLayerModifierOperation.BLUR_RADIUS_X)
+ var blurRadiusX = 0f
+ if (oBlurRadiusX != null) {
+ blurRadiusX = oBlurRadiusX as Float
+ }
+ val oBlurRadiusY = attributes.get(GraphicsLayerModifierOperation.BLUR_RADIUS_Y)
+ var blurRadiusY = 0f
+ if (oBlurRadiusY != null) {
+ blurRadiusY = oBlurRadiusY as Float
+ }
+ var blurTileMode = 0
+ val oBlurTileMode = attributes.get(GraphicsLayerModifierOperation.BLUR_TILE_MODE)
+ if (oBlurTileMode != null) {
+ blurTileMode = oBlurTileMode as Int
+ }
+ var tileMode = Shader.TileMode.CLAMP
+ when (blurTileMode) {
+ GraphicsLayerModifierOperation.TILE_MODE_CLAMP ->
+ tileMode = Shader.TileMode.CLAMP
+ GraphicsLayerModifierOperation.TILE_MODE_DECAL ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // REMOVE IN PLATFORM
+ tileMode = Shader.TileMode.DECAL
+ } // REMOVE IN PLATFORM
+ GraphicsLayerModifierOperation.TILE_MODE_MIRROR ->
+ tileMode = Shader.TileMode.MIRROR
+ GraphicsLayerModifierOperation.TILE_MODE_REPEATED ->
+ tileMode = Shader.TileMode.REPEAT
+ }
+ if (
+ Build.VERSION.SDK_INT // REMOVE IN PLATFORM
+ >= Build.VERSION_CODES.S
+ ) { // REMOVE IN PLATFORM
+ val effect = RenderEffect.createBlurEffect(blurRadiusX, blurRadiusY, tileMode)
+ node.setRenderEffect(effect)
+ } // REMOVE IN PLATFORM
+ }
+ }
+ }
+
+ override fun endGraphicsLayer() {
+ node!!.endRecording()
+ previousCanvas?.let { canvas = it }
+ if (nativeCanvas().isHardwareAccelerated) {
+ canvas.enableZ()
+ nativeCanvas().drawRenderNode(node!!)
+ canvas.disableZ()
+ }
+
+ // node.discardDisplayList();
+ node = null
+ }
+
+ override fun getText(textId: Int): String? {
+ return mContext.mRemoteComposeState.getFromId(textId) as String?
+ }
+
+ override fun matrixFromPath(pathId: Int, fraction: Float, vOffset: Float, flags: Int) {
+ val path = getPath(pathId, 0f, 1f)
+ if (path.isEmpty) return
+
+ val measure = PathMeasure()
+ measure.setPath(path, false)
+
+ val matrix = Matrix()
+
+ measure.getMatrix(matrix, fraction, flags)
+ canvas.concat(matrix)
+ }
+
+ override fun drawToBitmap(bitmapId: Int, mode: Int, color: Int) {
+ if (mainCanvas == null) {
+ mainCanvas = canvas
+ }
+ if (bitmapId == 0) {
+ canvas = mainCanvas!!
+ return
+ }
+ val bitmap = mContext.mRemoteComposeState.getFromId(bitmapId)!! as Bitmap
+ if (canvasCache.containsKey(bitmap)) {
+ canvas = canvasCache[bitmap]!!
+
+ if ((mode and 1) == 0) {
+ bitmap.eraseColor(color)
+ }
+ return
+ }
+ canvas = Canvas(bitmap.asImageBitmap())
+ if ((mode and 1) == 0) {
+ bitmap.eraseColor(color)
+ }
+ canvasCache[bitmap] = canvas
+ }
+
+ private fun nativeCanvas() = canvas.nativeCanvas
+
+ private fun getPath(path1Id: Int, path2Id: Int, tween: Float, start: Float, end: Float): Path {
+ return getPath(getPathArray(path1Id, path2Id, tween), start, end)
+ }
+
+ private fun getPath(tmp: FloatArray, start: Float, end: Float): Path {
+ val path = Path()
+ FloatsToPath.genPath(path, tmp, start, end)
+ return path
+ }
+
+ private fun getPath(id: Int, start: Float, end: Float): Path {
+ val p: Path? = mContext.mRemoteComposeState.getPath(id) as Path?
+ val w: Int = mContext.mRemoteComposeState.getPathWinding(id)
+ if (p != null) {
+ return p
+ }
+ val path = Path()
+ val pathData: FloatArray? = mContext.mRemoteComposeState.getPathData(id)
+ if (pathData != null) {
+ FloatsToPath.genPath(path, pathData, start, end)
+ if (w == 1) {
+ path.fillType = PathFillType.EvenOdd
+ }
+ mContext.mRemoteComposeState.putPath(id, path)
+ }
+
+ return path
+ }
+
+ private fun getNativePath(id: Int, start: Float, end: Float): android.graphics.Path {
+ val androidContext = mContext as ComposeRemoteContext
+ val p = androidContext.mRemoteComposeState.getPath(id) as Path?
+ if (p != null) {
+ return p.asAndroidPath()
+ }
+ val path = android.graphics.Path()
+ val pathData = androidContext.mRemoteComposeState.getPathData(id)
+ if (pathData != null) {
+ androidx.compose.remote.player.view.platform.FloatsToPath.genPath(
+ path,
+ pathData,
+ start,
+ end,
+ )
+ androidContext.mRemoteComposeState.putPath(id, path)
+ }
+
+ return path
+ }
+
+ private fun getPathArray(path1Id: Int, path2Id: Int, tween: Float): FloatArray {
+ val androidContext = mContext as ComposeRemoteContext
+ if (tween == 0.0f) {
+ return androidContext.mRemoteComposeState.getPathData(path1Id)!!
+ }
+ if (tween == 1.0f) {
+ return androidContext.mRemoteComposeState.getPathData(path2Id)!!
+ }
+
+ val data1: FloatArray = androidContext.mRemoteComposeState.getPathData(path1Id)!!
+ val data2: FloatArray = androidContext.mRemoteComposeState.getPathData(path2Id)!!
+ val tmp = FloatArray(data2.size)
+ for (i in tmp.indices) {
+ if (java.lang.Float.isNaN(data1[i]) || java.lang.Float.isNaN(data2[i])) {
+ tmp[i] = data1[i]
+ } else {
+ tmp[i] = (data2[i] - data1[i]) * tween + data1[i]
+ }
+ }
+ return tmp
+ }
+
+ private fun PathMeasure.getMatrix(matrix: Matrix, fraction: Float, flags: Int) {
+ val len = this.length
+ if (len == 0f) return
+
+ val distanceOnPath = (len * fraction) % len
+
+ // Get position
+ val position = this.getPosition(distanceOnPath) // Returns Offset(x, y)
+
+ // Apply translation for the position
+ matrix.translate(position.x, position.y)
+
+ // Check if tangent/rotation is requested (similar to
+ // android.graphics.PathMeasure.TANGENT_MATRIX_FLAG)
+ // Android: PATH_MEASURE_TANGENT_MATRIX_FLAG = 2
+ if ((flags and 2) != 0) { // If the tangent flag is set
+ val tangent =
+ this.getTangent(distanceOnPath) // Returns Offset representing vector dx, dy
+ // Calculate rotation angle from the tangent vector
+ val angleRadians = atan2(tangent.y, tangent.x)
+ val angleDegrees = Math.toDegrees(angleRadians.toDouble()).toFloat()
+ // Apply rotation around the (translated) origin.
+ // Since we already translated, the rotation is effectively at the point on the path.
+ matrix.rotateZ(angleDegrees) // Rotate around Z-axis for 2D graphics
+ }
+ }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt
new file mode 100644
index 0000000..9297bf0
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt
@@ -0,0 +1,389 @@
+/*
+ * 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.remote.player.compose.context
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.createBitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.VariableSupport
+import androidx.compose.remote.core.operations.BitmapData
+import androidx.compose.remote.core.operations.FloatExpression
+import androidx.compose.remote.core.operations.ShaderData
+import androidx.compose.remote.core.operations.utilities.ArrayAccess
+import androidx.compose.remote.core.operations.utilities.DataMap
+import androidx.compose.remote.core.types.LongConstant
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import java.io.IOException
+import java.net.MalformedURLException
+import java.net.URL
+import java.time.Clock
+
+/**
+ * An implementation of [RemoteContext] for
+ * [androidx.compose.remote.player.compose.RemoteComposePlayer].
+ */
+internal class ComposeRemoteContext(clock: Clock) : RemoteContext(clock) {
+ private lateinit var haptic: HapticFeedback
+ private var varNameHashMap: HashMap<String, VarName?> = HashMap<String, VarName?>()
+
+ override fun loadPathData(instanceId: Int, winding: Int, floatPath: FloatArray) {
+ mRemoteComposeState.putPathData(instanceId, floatPath)
+ mRemoteComposeState.putPathWinding(instanceId, winding)
+ }
+
+ override fun getPathData(instanceId: Int): FloatArray? {
+ return mRemoteComposeState.getPathData(instanceId)
+ }
+
+ override fun loadVariableName(varName: String, varId: Int, varType: Int) {
+ varNameHashMap.put(varName, VarName(varName, varId, varType))
+ }
+
+ override fun loadColor(id: Int, color: Int) {
+ mRemoteComposeState.updateColor(id, color)
+ }
+
+ override fun setNamedColorOverride(colorName: String, color: Int) {
+ val id = varNameHashMap[colorName]!!.id
+ mRemoteComposeState.overrideColor(id, color)
+ }
+
+ override fun setNamedStringOverride(stringName: String, value: String) {
+ if (varNameHashMap[stringName] != null) {
+ val id = varNameHashMap[stringName]!!.id
+ overrideText(id, value)
+ }
+ }
+
+ fun clearDataOverride(id: Int) {
+ mRemoteComposeState.clearDataOverride(id)
+ }
+
+ fun overrideInt(id: Int, value: Int) {
+ mRemoteComposeState.overrideInteger(id, value)
+ }
+
+ fun clearIntegerOverride(id: Int) {
+ mRemoteComposeState.clearIntegerOverride(id)
+ }
+
+ fun clearFloatOverride(id: Int) {
+ mRemoteComposeState.clearFloatOverride(id)
+ }
+
+ fun overrideData(id: Int, value: Any?) {
+ mRemoteComposeState.overrideData(id, value!!)
+ }
+
+ override fun clearNamedStringOverride(stringName: String) {
+ if (varNameHashMap[stringName] != null) {
+ val id = varNameHashMap[stringName]!!.id
+ clearDataOverride(id)
+ }
+ varNameHashMap[stringName] = null
+ }
+
+ override fun setNamedIntegerOverride(stringName: String, value: Int) {
+ if (varNameHashMap[stringName] != null) {
+ val id = varNameHashMap[stringName]!!.id
+ overrideInt(id, value)
+ }
+ }
+
+ override fun clearNamedIntegerOverride(integerName: String) {
+ if (varNameHashMap[integerName] != null) {
+ val id = varNameHashMap[integerName]!!.id
+ clearIntegerOverride(id)
+ }
+ varNameHashMap[integerName] = null
+ }
+
+ override fun setNamedFloatOverride(floatName: String, value: Float) {
+ if (varNameHashMap[floatName] != null) {
+ val id = varNameHashMap[floatName]!!.id
+ overrideFloat(id, value)
+ }
+ }
+
+ override fun clearNamedFloatOverride(floatName: String) {
+ if (varNameHashMap[floatName] != null) {
+ val id = varNameHashMap[floatName]!!.id
+ clearFloatOverride(id)
+ }
+ varNameHashMap[floatName] = null
+ }
+
+ override fun setNamedLong(name: String, value: Long) {
+ val entry = varNameHashMap[name]
+ if (entry != null) {
+ val id = entry.id
+ val longConstant = mRemoteComposeState.getObject(id) as LongConstant?
+ longConstant!!.value = value
+ }
+ }
+
+ override fun setNamedDataOverride(dataName: String, value: Any) {
+ if (varNameHashMap[dataName] != null) {
+ val id = varNameHashMap[dataName]!!.id
+ overrideData(id, value)
+ }
+ }
+
+ override fun clearNamedDataOverride(dataName: String) {
+ if (varNameHashMap[dataName] != null) {
+ val id = varNameHashMap[dataName]!!.id
+ clearDataOverride(id)
+ }
+ varNameHashMap[dataName] = null
+ }
+
+ override fun addCollection(id: Int, collection: ArrayAccess) {
+ mRemoteComposeState.addCollection(id, collection)
+ }
+
+ override fun putDataMap(id: Int, map: DataMap) {
+ mRemoteComposeState.putDataMap(id, map)
+ }
+
+ override fun getDataMap(id: Int): DataMap? {
+ return mRemoteComposeState.getDataMap(id)
+ }
+
+ override fun runAction(id: Int, metadata: String) {
+ mDocument.performClick(this, id, metadata)
+ }
+
+ override fun runNamedAction(id: Int, value: Any?) {
+ val text = getText(id)
+ mDocument.runNamedAction(text!!, value)
+ }
+
+ override fun putObject(id: Int, value: Any) {
+ mRemoteComposeState.updateObject(id, value)
+ }
+
+ override fun getObject(id: Int): Any? {
+ return mRemoteComposeState.getObject(id)
+ }
+
+ override fun hapticEffect(type: Int) {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ }
+
+ override fun loadBitmap(
+ imageId: Int,
+ encoding: Short,
+ type: Short,
+ width: Int,
+ height: Int,
+ bitmap: ByteArray,
+ ) {
+ if (!mRemoteComposeState.containsId(imageId)) {
+ var image: Bitmap? = null
+ when (encoding) {
+ BitmapData.ENCODING_INLINE ->
+ when (type) {
+ BitmapData.TYPE_PNG_8888 -> {
+ if (CHECK_DATA_SIZE) {
+ val opts = BitmapFactory.Options()
+ opts.inJustDecodeBounds = true // <-- do a bounds-only pass
+ BitmapFactory.decodeByteArray(bitmap, 0, bitmap.size, opts)
+ if (opts.outWidth > width || opts.outHeight > height) {
+ throw RuntimeException(
+ ("dimension don't match " +
+ opts.outWidth +
+ "x" +
+ opts.outHeight +
+ " vs " +
+ width +
+ "x" +
+ height)
+ )
+ }
+ }
+ image = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.size)
+ }
+ BitmapData.TYPE_PNG_ALPHA_8 -> {
+ image = decodePreferringAlpha8(bitmap)
+
+ // If needed convert to ALPHA_8.
+ if (image!!.getConfig() != Bitmap.Config.ALPHA_8) {
+ val alpha8Bitmap =
+ createBitmap(
+ image.getWidth(),
+ image.getHeight(),
+ Bitmap.Config.ALPHA_8,
+ )
+ val canvas = Canvas(alpha8Bitmap)
+ val paint = Paint()
+ paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+ canvas.drawBitmap(image, 0f, 0f, paint)
+ image.recycle() // Release resources
+
+ image = alpha8Bitmap
+ }
+ }
+ BitmapData.TYPE_RAW8888 -> {
+ image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val intData = IntArray(bitmap.size / 4)
+ var i = 0
+ while (i < intData.size) {
+ val p = i * 4
+ intData[i] =
+ ((bitmap[p].toInt() shl 24) or
+ (bitmap[p + 1].toInt() shl 16) or
+ (bitmap[p + 2].toInt() shl 8) or
+ bitmap[p + 3].toInt())
+ i++
+ }
+ image.setPixels(intData, 0, width, 0, 0, width, height)
+ }
+ BitmapData.TYPE_RAW8 -> {
+ image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val bitmapData = IntArray(bitmap.size / 4)
+ var i = 0
+ while (i < bitmapData.size) {
+ bitmapData[i] = 0x1010101 * bitmap[i]
+ i++
+ }
+ image.setPixels(bitmapData, 0, width, 0, 0, width, height)
+ }
+ }
+ BitmapData.ENCODING_FILE -> image = BitmapFactory.decodeFile(String(bitmap))
+ BitmapData.ENCODING_URL ->
+ try {
+ image = BitmapFactory.decodeStream(URL(String(bitmap)).openStream())
+ } catch (e: MalformedURLException) {
+ throw RuntimeException(e)
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ BitmapData.ENCODING_EMPTY ->
+ image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ }
+ mRemoteComposeState.cacheData(imageId, image!!)
+ }
+ }
+
+ override fun loadText(id: Int, text: String) {
+ if (!mRemoteComposeState.containsId(id)) {
+ mRemoteComposeState.cacheData(id, text)
+ } else {
+ mRemoteComposeState.updateData(id, text)
+ }
+ }
+
+ override fun getText(id: Int): String? {
+ return mRemoteComposeState.getFromId(id) as? String
+ }
+
+ override fun loadFloat(id: Int, value: Float) {
+ mRemoteComposeState.updateFloat(id, value)
+ }
+
+ override fun overrideFloat(id: Int, value: Float) {
+ mRemoteComposeState.overrideFloat(id, value)
+ }
+
+ override fun loadInteger(id: Int, value: Int) {
+ mRemoteComposeState.updateInteger(id, value)
+ }
+
+ override fun overrideInteger(id: Int, value: Int) {
+ mRemoteComposeState.overrideInteger(id, value)
+ }
+
+ fun overrideText(id: Int, text: String?) {
+ mRemoteComposeState.overrideData(id, text!!)
+ }
+
+ override fun overrideText(id: Int, valueId: Int) {
+ val text = getText(valueId)
+ overrideText(id, text)
+ }
+
+ override fun loadAnimatedFloat(id: Int, animatedFloat: FloatExpression) {
+ mRemoteComposeState.cacheData(id, animatedFloat)
+ }
+
+ override fun loadShader(id: Int, value: ShaderData) {
+ mRemoteComposeState.cacheData(id, value)
+ }
+
+ override fun getFloat(id: Int): Float {
+ return mRemoteComposeState.getFloat(id)
+ }
+
+ override fun getInteger(id: Int): Int {
+ return mRemoteComposeState.getInteger(id)
+ }
+
+ override fun getLong(id: Int): Long {
+ return (mRemoteComposeState.getObject(id) as LongConstant?)!!.value
+ }
+
+ override fun getColor(id: Int): Int {
+ return mRemoteComposeState.getColor(id)
+ }
+
+ override fun listensTo(id: Int, variableSupport: VariableSupport) {
+ mRemoteComposeState.listenToVar(id, variableSupport)
+ }
+
+ override fun updateOps(): Int {
+ return mRemoteComposeState.getOpsToUpdate(this, currentTime)
+ }
+
+ override fun getShader(id: Int): ShaderData? {
+ return mRemoteComposeState.getFromId(id) as ShaderData?
+ }
+
+ override fun addClickArea(
+ id: Int,
+ contentDescriptionId: Int,
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ metadataId: Int,
+ ) {
+ val contentDescription = mRemoteComposeState.getFromId(contentDescriptionId) as String?
+ val metadata = mRemoteComposeState.getFromId(metadataId) as String?
+ mDocument.addClickArea(id, contentDescription, left, top, right, bottom, metadata)
+ }
+
+ fun setHaptic(haptic: HapticFeedback) {
+ this@ComposeRemoteContext.haptic = haptic
+ }
+
+ private fun decodePreferringAlpha8(data: ByteArray): Bitmap? {
+ val options = BitmapFactory.Options()
+ options.inPreferredConfig = Bitmap.Config.ALPHA_8
+ return BitmapFactory.decodeByteArray(data, 0, data.size, options)
+ }
+
+ companion object {
+ private const val CHECK_DATA_SIZE: Boolean = true
+ }
+}
+
+private data class VarName(val name: String, val id: Int, val type: Int)
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt
new file mode 100644
index 0000000..84a887d
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.utils
+
+import android.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.StrokeJoin
+
+/** Get a [StrokeCap] from a [Paint.Cap] */
+internal fun Paint.Cap.toStrokeCap() =
+ when (this) {
+ Paint.Cap.BUTT -> StrokeCap.Butt
+ Paint.Cap.ROUND -> StrokeCap.Round
+ Paint.Cap.SQUARE -> StrokeCap.Square
+ }
+
+/** Get a [PaintingStyle] from a [Paint.Style] */
+internal fun Paint.Style.toPaintingStyle() =
+ when (this) {
+ Paint.Style.FILL -> PaintingStyle.Fill
+ Paint.Style.STROKE -> PaintingStyle.Stroke
+ Paint.Style.FILL_AND_STROKE ->
+ // No equivalent in PaintingStyle, defaulting it to Fill
+ PaintingStyle.Fill
+ }
+
+/** Get a [StrokeJoin] from a [Paint.Join] */
+internal fun Paint.Join.toStrokeJoin() =
+ when (this) {
+ Paint.Join.MITER -> StrokeJoin.Miter
+ Paint.Join.ROUND -> StrokeJoin.Round
+ Paint.Join.BEVEL -> StrokeJoin.Bevel
+ }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt
new file mode 100644
index 0000000..9f2d17a
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.remote.player.compose.utils
+
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.ui.graphics.BlendMode
+
+/** Get a [BlendMode] from a [PaintBundle] */
+internal fun remoteToBlendMode(mode: Int): BlendMode? =
+ when (mode) {
+ PaintBundle.BLEND_MODE_CLEAR -> BlendMode.Clear
+ PaintBundle.BLEND_MODE_SRC -> BlendMode.Src
+ PaintBundle.BLEND_MODE_DST -> BlendMode.Dst
+ PaintBundle.BLEND_MODE_SRC_OVER -> BlendMode.SrcOver
+ PaintBundle.BLEND_MODE_DST_OVER -> BlendMode.DstOver
+ PaintBundle.BLEND_MODE_SRC_IN -> BlendMode.SrcIn
+ PaintBundle.BLEND_MODE_DST_IN -> BlendMode.DstIn
+ PaintBundle.BLEND_MODE_SRC_OUT -> BlendMode.SrcOut
+ PaintBundle.BLEND_MODE_DST_OUT -> BlendMode.DstOut
+ PaintBundle.BLEND_MODE_SRC_ATOP -> BlendMode.SrcAtop
+ PaintBundle.BLEND_MODE_DST_ATOP -> BlendMode.DstAtop
+ PaintBundle.BLEND_MODE_XOR -> BlendMode.Xor
+ PaintBundle.BLEND_MODE_PLUS -> BlendMode.Plus
+ PaintBundle.BLEND_MODE_MODULATE -> BlendMode.Modulate
+ PaintBundle.BLEND_MODE_SCREEN -> BlendMode.Screen
+ PaintBundle.BLEND_MODE_OVERLAY -> BlendMode.Overlay
+ PaintBundle.BLEND_MODE_DARKEN -> BlendMode.Darken
+ PaintBundle.BLEND_MODE_LIGHTEN -> BlendMode.Lighten
+ PaintBundle.BLEND_MODE_COLOR_DODGE -> BlendMode.ColorDodge
+ PaintBundle.BLEND_MODE_COLOR_BURN -> BlendMode.ColorBurn
+ PaintBundle.BLEND_MODE_HARD_LIGHT -> BlendMode.Hardlight
+ PaintBundle.BLEND_MODE_SOFT_LIGHT -> BlendMode.Softlight
+ PaintBundle.BLEND_MODE_DIFFERENCE -> BlendMode.Difference
+ PaintBundle.BLEND_MODE_EXCLUSION -> BlendMode.Exclusion
+ PaintBundle.BLEND_MODE_MULTIPLY -> BlendMode.Multiply
+ PaintBundle.BLEND_MODE_HUE -> BlendMode.Hue
+ PaintBundle.BLEND_MODE_SATURATION -> BlendMode.Saturation
+ PaintBundle.BLEND_MODE_COLOR -> BlendMode.Color
+ PaintBundle.BLEND_MODE_LUMINOSITY -> BlendMode.Luminosity
+ PaintBundle.BLEND_MODE_NULL -> null
+ else -> null
+ }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt
new file mode 100644
index 0000000..dcba2aa
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.remote.player.compose.utils
+
+import android.util.Log
+import androidx.compose.remote.core.operations.PathData
+import androidx.compose.remote.core.operations.Utils.idFromNan
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathMeasure
+import kotlin.math.max
+import kotlin.math.min
+
+/** Utility class to convert a float array representation of a path into a Compose [Path] object. */
+internal object FloatsToPath {
+ private const val TAG = "FloatsToPath"
+
+ /**
+ * Converts a float array representing a path into a Path object.
+ *
+ * @param retPath The Path object to populate with the converted path data.
+ * @param floatPath The float array representing the path.
+ * @param start The starting percentage (0.0 to 1.0) of the path to include.
+ * @param stop The ending percentage (0.0 to 1.0) of the path to include.
+ */
+ fun genPath(retPath: Path, floatPath: FloatArray, start: Float, stop: Float) {
+ var i = 0
+ val path = Path() // todo this should be cached for performance
+ while (i < floatPath.size) {
+ when (idFromNan(floatPath[i])) {
+ PathData.MOVE -> {
+ i++
+ path.moveTo(floatPath[i + 0], floatPath[i + 1])
+ i += 2
+ }
+ PathData.LINE -> {
+ i += 3
+ path.lineTo(floatPath[i + 0], floatPath[i + 1])
+ i += 2
+ }
+ PathData.QUADRATIC -> {
+ i += 3
+ path.quadraticTo(
+ floatPath[i + 0],
+ floatPath[i + 1],
+ floatPath[i + 2],
+ floatPath[i + 3],
+ )
+ i += 4
+ }
+ PathData.CONIC -> {
+ i += 3
+ // if (Build.VERSION.SDK_INT >= 34) { // REMOVE IN PLATFORM
+ // path.conicTo(
+ // floatPath[i + 0],
+ // floatPath[i + 1],
+ // floatPath[i + 2],
+ // floatPath[i + 3],
+ // floatPath[i + 4]
+ // )
+ // } // REMOVE IN PLATFORM
+ i += 5
+ // TODO(b/434130226): Conic operation not available in
+ // androidx.compose.ui.graphics
+ throw UnsupportedOperationException("Conic operation not yet implemented.")
+ }
+ PathData.CUBIC -> {
+ i += 3
+ path.cubicTo(
+ floatPath[i + 0],
+ floatPath[i + 1],
+ floatPath[i + 2],
+ floatPath[i + 3],
+ floatPath[i + 4],
+ floatPath[i + 5],
+ )
+ i += 6
+ }
+ PathData.CLOSE -> {
+ path.close()
+ i++
+ }
+ PathData.DONE -> i++
+ else -> Log.w(TAG, " Odd command " + idFromNan(floatPath[i]))
+ }
+ }
+
+ retPath.reset()
+ if (start > 0f || stop < 1f) {
+ if (start < stop) {
+ val measure: PathMeasure = PathMeasure() // todo cached
+ measure.setPath(path, false)
+ val len: Float = measure.length
+ val scaleStart = (max(start.toDouble(), 0.0) * len).toFloat()
+ val scaleStop = (min(stop.toDouble(), 1.0) * len).toFloat()
+ measure.getSegment(scaleStart, scaleStop, retPath, true)
+ }
+ } else {
+ retPath.addPath(path)
+ }
+ }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt
new file mode 100644
index 0000000..c4b91f5
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.remote.player.compose.utils
+
+import androidx.compose.ui.graphics.Paint
+
+/**
+ * Make a copy of [Paint]. This is not a deep copy, [Paint.shader], [Paint.colorFilter] and
+ * [Paint.pathEffect] hold the same reference as the original.
+ */
+internal fun Paint.copy(): Paint =
+ Paint().also { copy ->
+ copy.alpha = this.alpha
+ copy.isAntiAlias = this.isAntiAlias
+ copy.color = this.color
+ copy.blendMode = this.blendMode
+ copy.style = this.style
+ copy.strokeWidth = this.strokeWidth
+ copy.strokeCap = this.strokeCap
+ copy.strokeJoin = this.strokeJoin
+ copy.strokeMiterLimit = this.strokeMiterLimit
+ copy.filterQuality = this.filterQuality
+
+ // same references:
+ copy.shader = this.shader
+ copy.colorFilter = this.colorFilter
+ copy.pathEffect = this.pathEffect
+ }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt
new file mode 100644
index 0000000..e01378e
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.remote.player.compose.utils
+
+import android.graphics.PorterDuff
+import androidx.compose.remote.core.operations.paint.PaintBundle
+
+/** Get a [PorterDuff.Mode] from a [PaintBundle] */
+internal fun remoteToPorterDuffMode(mode: Int): PorterDuff.Mode =
+ when (mode) {
+ PaintBundle.BLEND_MODE_CLEAR -> PorterDuff.Mode.CLEAR
+ PaintBundle.BLEND_MODE_SRC -> PorterDuff.Mode.SRC
+ PaintBundle.BLEND_MODE_DST -> PorterDuff.Mode.DST
+ PaintBundle.BLEND_MODE_SRC_OVER -> PorterDuff.Mode.SRC_OVER
+ PaintBundle.BLEND_MODE_DST_OVER -> PorterDuff.Mode.DST_OVER
+ PaintBundle.BLEND_MODE_SRC_IN -> PorterDuff.Mode.SRC_IN
+ PaintBundle.BLEND_MODE_DST_IN -> PorterDuff.Mode.DST_IN
+ PaintBundle.BLEND_MODE_SRC_OUT -> PorterDuff.Mode.SRC_OUT
+ PaintBundle.BLEND_MODE_DST_OUT -> PorterDuff.Mode.DST_OUT
+ PaintBundle.BLEND_MODE_SRC_ATOP -> PorterDuff.Mode.SRC_ATOP
+ PaintBundle.BLEND_MODE_DST_ATOP -> PorterDuff.Mode.DST_ATOP
+ PaintBundle.BLEND_MODE_XOR -> PorterDuff.Mode.XOR
+ PaintBundle.BLEND_MODE_SCREEN -> PorterDuff.Mode.SCREEN
+ PaintBundle.BLEND_MODE_OVERLAY -> PorterDuff.Mode.OVERLAY
+ PaintBundle.BLEND_MODE_DARKEN -> PorterDuff.Mode.DARKEN
+ PaintBundle.BLEND_MODE_LIGHTEN -> PorterDuff.Mode.LIGHTEN
+ PaintBundle.BLEND_MODE_MULTIPLY -> PorterDuff.Mode.MULTIPLY
+ PaintBundle.PORTER_MODE_ADD -> PorterDuff.Mode.ADD
+ else -> PorterDuff.Mode.SRC_OVER
+ }
diff --git a/compose/runtime/runtime-annotation/build.gradle b/compose/runtime/runtime-annotation/build.gradle
index 026f4bf..ec07934 100644
--- a/compose/runtime/runtime-annotation/build.gradle
+++ b/compose/runtime/runtime-annotation/build.gradle
@@ -51,24 +51,6 @@
api(libs.kotlinStdlib)
}
}
-
- jsMain {
- dependsOn(commonMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- wasmJsMain {
- dependsOn(commonMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibWasm)
- }
- }
}
}
diff --git a/compose/runtime/runtime-saveable/bcv/native/current.txt b/compose/runtime/runtime-saveable/bcv/native/current.txt
index e80faaa..5c1eea4 100644
--- a/compose/runtime/runtime-saveable/bcv/native/current.txt
+++ b/compose/runtime/runtime-saveable/bcv/native/current.txt
@@ -1,5 +1,5 @@
// Klib ABI Dump
-// Targets: [linuxX64.linuxx64Stubs]
+// Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index c674dabe..7a52f98 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -37,8 +37,15 @@
namespace = "androidx.compose.runtime.saveable"
androidResources.enable = true
}
- jvmStubs()
- linuxX64Stubs()
+ desktop()
+ mingwX64()
+ linux()
+ mac()
+ ios()
+ tvos()
+ watchos()
+ js()
+ wasmJs()
defaultPlatform(PlatformIdentifier.ANDROID)
@@ -46,35 +53,27 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
- implementation("androidx.collection:collection:1.4.2")
- implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
+ implementation("androidx.collection:collection:1.5.0")
+ implementation(project(":lifecycle:lifecycle-runtime-compose"))
api(project(":compose:runtime:runtime"))
- api("androidx.savedstate:savedstate-compose:1.3.0")
+ api(project(":savedstate:savedstate-compose"))
}
}
commonTest {
dependencies {
- }
- }
-
- jvmMain {
- dependsOn(commonMain)
- dependencies {
+ implementation(libs.kotlinTest)
+ implementation(libs.kotlinCoroutinesTest)
}
}
androidMain {
- dependsOn(jvmMain)
+ dependsOn(commonMain)
dependencies {
api("androidx.annotation:annotation:1.8.1")
}
}
- jvmStubsMain {
- dependsOn(jvmMain)
- }
-
androidInstrumentedTest {
dependsOn(commonTest)
dependencies {
@@ -105,6 +104,30 @@
implementation(libs.truth)
}
}
+
+ webMain {
+ dependsOn(commonMain)
+ }
+
+ webTest {
+ dependsOn(commonTest)
+ }
+
+ jsMain {
+ dependsOn(webMain)
+ }
+
+ jsTest {
+ dependsOn(webTest)
+ }
+
+ wasmJsMain {
+ dependsOn(webMain)
+ }
+
+ wasmJsTest {
+ dependsOn(webTest)
+ }
}
}
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index 5cda752..44479b5 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -83,24 +83,6 @@
dependsOn(commonMain)
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
@@ -110,6 +92,8 @@
} else {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
}
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(webMain)
}
}
}
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 91e5df8..5c19a8a 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -68,9 +68,7 @@
commonTest {
dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinTestForWasmTests)
+ implementation(libs.kotlinTest)
implementation(libs.kotlinCoroutinesTest)
implementation(libs.kotlinReflect)
implementation(project(":compose:runtime:runtime-test-utils"))
@@ -178,39 +176,12 @@
dependsOn(nonEmulatorCommonTest)
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestJs)
- }
- }
-
wasmJsMain {
- dependsOn(webMain)
dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibWasm)
implementation(libs.kotlinXw3c)
}
}
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestWasm)
- }
- }
-
unixMain {
dependsOn(nativeMain)
}
@@ -247,6 +218,9 @@
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
}
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(webTest)
}
}
}
diff --git a/compose/ui/ui-tooling-data/api/current.txt b/compose/ui/ui-tooling-data/api/current.txt
index af4a03e..94bd660 100644
--- a/compose/ui/ui-tooling-data/api/current.txt
+++ b/compose/ui/ui-tooling-data/api/current.txt
@@ -6,6 +6,10 @@
property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
}
+ public final class CompositionDataTreeKt {
+ method @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public static <T> java.util.List<T> makeTree(java.util.Set<? extends androidx.compose.runtime.tooling.CompositionData>, kotlin.jvm.functions.Function4<? super androidx.compose.runtime.tooling.CompositionGroup,? super androidx.compose.ui.tooling.data.SourceContext,? super java.util.List<? extends T>,? super java.util.List<? extends T>?,? extends T?> factory, optional androidx.compose.ui.tooling.data.ContextCache cache);
+ }
+
@SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public final class ContextCache {
ctor public ContextCache();
method public void clear();
diff --git a/compose/ui/ui-tooling-data/api/restricted_current.txt b/compose/ui/ui-tooling-data/api/restricted_current.txt
index af4a03e..94bd660 100644
--- a/compose/ui/ui-tooling-data/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-data/api/restricted_current.txt
@@ -6,6 +6,10 @@
property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
}
+ public final class CompositionDataTreeKt {
+ method @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public static <T> java.util.List<T> makeTree(java.util.Set<? extends androidx.compose.runtime.tooling.CompositionData>, kotlin.jvm.functions.Function4<? super androidx.compose.runtime.tooling.CompositionGroup,? super androidx.compose.ui.tooling.data.SourceContext,? super java.util.List<? extends T>,? super java.util.List<? extends T>?,? extends T?> factory, optional androidx.compose.ui.tooling.data.ContextCache cache);
+ }
+
@SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public final class ContextCache {
ctor public ContextCache();
method public void clear();
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt
new file mode 100644
index 0000000..348eacf
--- /dev/null
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.tooling.data
+
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
+import androidx.compose.runtime.tooling.CompositionInstance
+import androidx.compose.runtime.tooling.findCompositionInstance
+
+/**
+ * Processes a set of [CompositionData] instances and constructs a list of trees of custom nodes.
+ *
+ * This function builds a hierarchy from the provided compositions and then uses the `mapTree` to
+ * transform each root composition into a custom tree structure defined by the [factory]. Results
+ * from `mapTree` that are `null` will be excluded from the final list.
+ *
+ * The processing involves:
+ * 1. Building a lookup map from [CompositionInstance] to [CompositionData].
+ * 2. Constructing a parent-to-children hierarchy of [CompositionInstance]s.
+ * 3. Recursively traversing the hierarchy to map each composition using the provided [factory],
+ * potentially stitching children processed from sub-compositions.
+ *
+ * @param factory A function that takes a [CompositionGroup], its [SourceContext], a list of already
+ * processed children of type [T] from the current composition, and an optional list of children
+ * of type [T] from stitched sub-compositions. It returns a custom node of type [T] or `null`.
+ * @param cache An optional [ContextCache] to optimize [SourceContext] creation.
+ * @return A list of root nodes of type [T] representing the successfully processed trees.
+ * @receiver A set of [CompositionData] to be processed into trees.
+ */
+@UiToolingDataApi
+@OptIn(UiToolingDataApi::class)
+fun <T> Set<CompositionData>.makeTree(
+ factory: (CompositionGroup, SourceContext, List<T>, List<T>?) -> T?,
+ cache: ContextCache = ContextCache(),
+): List<T> = CompositionDataTree(this, factory, cache).build()
+
+@OptIn(UiToolingDataApi::class)
+private class CompositionDataTree<T>(
+ private val compositions: Set<CompositionData>,
+ private val factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
+ private val cache: ContextCache,
+) {
+ private val hierarchy = mutableMapOf<CompositionInstance, MutableList<CompositionInstance>>()
+ private val processedNodes = mutableMapOf<CompositionInstance, T?>()
+ private val rootCompositionInstances = mutableSetOf<CompositionInstance>()
+
+ init {
+ for (compositionData in compositions) {
+ compositionData.findCompositionInstance()?.let { compositionInstance ->
+ buildCompositionParentHierarchy(compositionInstance)
+ }
+ }
+ }
+
+ fun build(): List<T> {
+ return rootCompositionInstances.mapNotNull { rootInstance -> mapTree(rootInstance) }
+ }
+
+ @OptIn(UiToolingDataApi::class)
+ private fun mapTree(instance: CompositionInstance): T? {
+ // Check if already processed to handle shared nodes and cycles
+ if (processedNodes.containsKey(instance)) {
+ return processedNodes[instance]
+ }
+ val compositionData = instance.data
+
+ // Recursively process children and collect their results
+ val children = hierarchy[instance] ?: emptyList()
+
+ for (childInstance in children) {
+ mapTree(childInstance)
+ }
+
+ val childrenToAdd = mutableMapOf<Any?, MutableList<T>>()
+ children
+ .filter { it in processedNodes }
+ .groupByTo(
+ childrenToAdd,
+ { it.findContextGroup()!!.identity },
+ { processedNodes[it]!! },
+ )
+
+ // Now, map the current tree, stitching the children's results.
+ // The `mapTreeWithStitching` function is an assumed extension that handles the actual
+ // mapping.
+ val result = compositionData.mapTreeWithStitching(factory, cache, childrenToAdd)
+
+ // Memoize the result
+ processedNodes[instance] = result
+ return result
+ }
+
+ /**
+ * Traverses up the composition tree from a given [CompositionInstance] to build its parent
+ * hierarchy and identify the ultimate root of its specific tree.
+ *
+ * Starting from the given [instance], this function iteratively moves to its parent:
+ * - It adds the current [instance] to its parent's list of children in the [hierarchy] map.
+ * - If this parent-child relationship is already recorded, it stops to avoid redundant
+ * processing.
+ * - This continues until an instance with no parent is reached (i.e., `parentComposition` is
+ * `null`).
+ * - The instance that has no parent is considered an ultimate root and is added to
+ * [rootCompositionInstances].
+ *
+ * @param instance The [CompositionInstance] from which to start building the parent hierarchy.
+ */
+ private fun buildCompositionParentHierarchy(instance: CompositionInstance) {
+ var currentComposition = instance
+ var parentComposition = currentComposition.parent
+ while (parentComposition != null) {
+ val children = hierarchy.getOrPut(parentComposition) { mutableListOf() }
+ if (children.contains(currentComposition)) {
+ // This path (parent-child link) has already been processed.
+ return
+ }
+ children.add(currentComposition)
+ currentComposition = parentComposition
+ parentComposition = currentComposition.parent
+ }
+ // After the loop, currentComposition is the instance that has no parent.
+ rootCompositionInstances.add(currentComposition)
+ }
+}
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
index 299de92..76ef39e 100644
--- a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
@@ -316,8 +316,9 @@
@UiToolingDataApi
private class CompositionCallStack<T>(
- private val factory: (CompositionGroup, SourceContext, List<T>) -> T?,
+ private val factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
private val contexts: MutableMap<String, Any?>,
+ private val childrenToAdd: MutableMap<Any?, MutableList<T>>? = null,
) : SourceContext {
private val stack = ArrayDeque<CompositionGroup>()
private var currentCallIndex = 0
@@ -336,7 +337,11 @@
box = (group.node as? LayoutInfo)?.let { boundsOfLayoutNode(it) } ?: box
currentCallIndex = callIndex
bounds = box
- factory(group, this, children)?.let { out.add(it) }
+
+ val childrenToStitchToGroup =
+ childrenToAdd?.takeIf { it.isNotEmpty() }?.remove(group.identity)
+
+ factory(group, this, children, childrenToStitchToGroup ?: emptyList())?.let { out.add(it) }
pop()
return box
}
@@ -456,7 +461,47 @@
cache: ContextCache = ContextCache(),
): T? {
val group = compositionGroups.firstOrNull() ?: return null
- val callStack = CompositionCallStack(factory, cache.contexts)
+
+ // After
+ val callStack =
+ CompositionCallStack(
+ { group, sourceContext, children, _ -> factory(group, sourceContext, children) },
+ cache.contexts,
+ )
+
+ val out = mutableListOf<T>()
+ callStack.convert(group, 0, out)
+ return out.firstOrNull()
+}
+
+/**
+ * Transforms a [CompositionData] instance into a tree of custom nodes.
+ *
+ * The [factory] method is invoked for each [CompositionGroup] within the slot tree. This allows for
+ * the creation of custom nodes based on the provided arguments. The [SourceContext] argument offers
+ * access to supplementary information encoded in [CompositionGroup.sourceInfo]. If [factory]
+ * returns `null`, the entire corresponding subtree is disregarded.
+ *
+ * An optional [cache] (of type [ContextCache]) can be supplied. This can enhance performance if
+ * [mapTree] is called multiple times and the values of [CompositionGroup.sourceInfo] are not
+ * unique.
+ *
+ * @param factory A function that takes a [CompositionGroup], its [SourceContext], and a list of
+ * already processed children of type [T], and if present children of type [T] that should be
+ * stitched and returns a custom node of type [T] or `null` to ignore the subtree.
+ * @param cache An optional [ContextCache] to optimize [SourceContext] creation.
+ * @param childrenToAdd A map to accumulate children that need to be stitched.
+ * @return The root of the custom node tree of type [T], or `null` if the input [CompositionData] is
+ * empty or the root group is processed to `null` by the [factory].
+ */
+@OptIn(UiToolingDataApi::class)
+internal fun <T> CompositionData.mapTreeWithStitching(
+ factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
+ cache: ContextCache = ContextCache(),
+ childrenToAdd: MutableMap<Any?, MutableList<T>>? = mutableMapOf(),
+): T? {
+ val group = compositionGroups.firstOrNull() ?: return null
+ val callStack = CompositionCallStack(factory, cache.contexts, childrenToAdd)
val out = mutableListOf<T>()
callStack.convert(group, 0, out)
return out.firstOrNull()
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 7e9f007..2539bddb 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -145,6 +145,7 @@
property public boolean isGraphicsLayerShapeSemanticsEnabled;
property public boolean isIgnoreInvalidPrevFocusRectEnabled;
property public boolean isIndirectTouchNavigationGestureDetectorEnabled;
+ property public boolean isInitialFocusOnFocusableAvailable;
property public boolean isNestedScrollDispatcherNodeFixEnabled;
property public boolean isNestedScrollInteropIntegerPropagationEnabled;
property @Deprecated public boolean isNoPinningInFocusRestorationEnabled;
@@ -165,6 +166,7 @@
field public static boolean isGraphicsLayerShapeSemanticsEnabled;
field public static boolean isIgnoreInvalidPrevFocusRectEnabled;
field public static boolean isIndirectTouchNavigationGestureDetectorEnabled;
+ field public static boolean isInitialFocusOnFocusableAvailable;
field public static boolean isNestedScrollDispatcherNodeFixEnabled;
field public static boolean isNestedScrollInteropIntegerPropagationEnabled;
field @Deprecated public static boolean isNoPinningInFocusRestorationEnabled;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 385eb15..a5868b0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -145,6 +145,7 @@
property public boolean isGraphicsLayerShapeSemanticsEnabled;
property public boolean isIgnoreInvalidPrevFocusRectEnabled;
property public boolean isIndirectTouchNavigationGestureDetectorEnabled;
+ property public boolean isInitialFocusOnFocusableAvailable;
property public boolean isNestedScrollDispatcherNodeFixEnabled;
property public boolean isNestedScrollInteropIntegerPropagationEnabled;
property @Deprecated public boolean isNoPinningInFocusRestorationEnabled;
@@ -165,6 +166,7 @@
field public static boolean isGraphicsLayerShapeSemanticsEnabled;
field public static boolean isIgnoreInvalidPrevFocusRectEnabled;
field public static boolean isIndirectTouchNavigationGestureDetectorEnabled;
+ field public static boolean isInitialFocusOnFocusableAvailable;
field public static boolean isNestedScrollDispatcherNodeFixEnabled;
field public static boolean isNestedScrollInteropIntegerPropagationEnabled;
field @Deprecated public static boolean isNoPinningInFocusRestorationEnabled;
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
index 6fcd84a..d38f9bf 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.focus
+import android.app.Instrumentation
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.view.View
@@ -30,6 +31,8 @@
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.platform.testTag
@@ -107,9 +110,9 @@
* Asserts that the elements appear in the specified order.
*
* Consider using this helper function instead of
- * [containsExactlyElementsIn][com.google.common.truth.IterableSubject.containsExactlyElementsIn] or
- * [containsExactly][com.google.common.truth.IterableSubject.containsExactly] as it also asserts
- * that the elements are in the specified order.
+ * [containsExactlyElementsIn][IterableSubject.containsExactlyElementsIn] or
+ * [containsExactly][IterableSubject.containsExactly] as it also asserts that the elements are in
+ * the specified order.
*/
fun IterableSubject.isExactly(vararg expected: Any?) {
return containsExactlyElementsIn(expected).inOrder()
@@ -129,7 +132,16 @@
Box(modifier.then(if (tag != null) Modifier.testTag(tag) else Modifier).size(50.dp).focusable())
}
+fun Instrumentation.setInTouchModeCompat(touchMode: Boolean) {
+ if (touchMode) {
+ setInTouchMode(true)
+ } else {
+ // setInTouchMode(false) is flaky, so we press a key to put the system in non-touch mode.
+ sendKeyDownUpSync(Key.Grave.nativeKeyCode)
+ }
+}
+
// TODO(b/267253920): Add a compose test API to set/reset InputMode.
-fun android.app.Instrumentation.resetInTouchModeCompat() {
+fun Instrumentation.resetInTouchModeCompat() {
if (SDK_INT < 33) setInTouchMode(true) else resetInTouchMode()
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt
new file mode 100644
index 0000000..dcd2da7
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.VERSION.SDK_INT
+import android.view.View
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+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
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class InitialFocusTest(private val initialFocusEnabled: Boolean, private val touchMode: Boolean) {
+ @get:Rule val rule = createComposeRule()
+
+ lateinit var owner: View
+ lateinit var layoutDirection: LayoutDirection
+ var previousFlagValue: Boolean = false
+
+ @Before
+ fun setTouchMode() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ previousFlagValue = ComposeUiFlags.isInitialFocusOnFocusableAvailable
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isInitialFocusOnFocusableAvailable = initialFocusEnabled
+ InstrumentationRegistry.getInstrumentation().setInTouchModeCompat(touchMode)
+ }
+
+ @After
+ fun resetTouchMode() {
+ InstrumentationRegistry.getInstrumentation().resetInTouchModeCompat()
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isInitialFocusOnFocusableAvailable = previousFlagValue
+ }
+
+ @Test
+ fun noFocusable() {
+ // Arrange.
+ rule.setTestContent { Box(Modifier.size(100.dp)) }
+
+ // Assert.
+ rule.runOnIdle { assertThat(owner.hasFocus()).isFalse() }
+ }
+
+ @Test
+ fun singleFocusedItem() {
+ // Arrange.
+ rule.setTestContent { Box(Modifier.size(100.dp).testTag("box").focusable()) }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ rule.onNodeWithTag("box").assertIsFocused()
+ }
+ }
+
+ @Test
+ fun topItemIsInitiallyFocused() {
+ // Arrange.
+ rule.setTestContent {
+ Column {
+ Box(Modifier.size(100.dp).testTag("top").focusable())
+ Box(Modifier.size(100.dp).focusable())
+ }
+ }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ rule.onNodeWithTag("top").assertIsFocused()
+ }
+ }
+
+ @Test
+ fun leftItemIsInitiallyFocused() {
+ // Arrange.
+ rule.setTestContent {
+ Row {
+ Box(Modifier.size(100.dp).testTag("left").focusable())
+ Box(Modifier.size(100.dp).testTag("right").focusable())
+ }
+ }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ when (layoutDirection) {
+ LayoutDirection.Ltr -> rule.onNodeWithTag("left").assertIsFocused()
+ LayoutDirection.Rtl -> rule.onNodeWithTag("right").assertIsFocused()
+ }
+ }
+ }
+
+ @Test
+ fun itemFocusedOnAppearing() {
+ // Arrange.
+ var showItem by mutableStateOf(false)
+ rule.setTestContent {
+ if (showItem) {
+ Box(Modifier.size(100.dp).testTag("box").focusable())
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { showItem = true }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ rule.onNodeWithTag("box").assertIsFocused()
+ }
+ }
+
+ @Test
+ fun topItemFocusedOnAppearing() {
+ // Arrange.
+ var showItem by mutableStateOf(false)
+ rule.setTestContent {
+ if (showItem) {
+ Column {
+ Box(Modifier.size(100.dp).testTag("top").focusable())
+ Box(Modifier.size(100.dp).focusable())
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { showItem = true }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ rule.onNodeWithTag("top").assertIsFocused()
+ }
+ }
+
+ @Test
+ fun leftItemFocusedOnAppearing() {
+ // Arrange.
+ var showItem by mutableStateOf(false)
+ rule.setTestContent {
+ if (showItem) {
+ Row {
+ Box(Modifier.size(100.dp).testTag("left").focusable())
+ Box(Modifier.size(100.dp).testTag("right").focusable())
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { showItem = true }
+
+ // Assert.
+ // Since API 28, we don't assign initial focus in touch mode.
+ // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+ if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+ assertThat(owner.hasFocus()).isFalse()
+ } else {
+ when (layoutDirection) {
+ LayoutDirection.Ltr -> rule.onNodeWithTag("left").assertIsFocused()
+ LayoutDirection.Rtl -> rule.onNodeWithTag("right").assertIsFocused()
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "initialFocusEnabled = {0}, touchMode = {1}")
+ fun initParameters() =
+ listOf(
+ arrayOf(false, false),
+ arrayOf(false, true),
+ arrayOf(true, false),
+ arrayOf(true, true),
+ )
+ }
+
+ private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
+ setContent {
+ owner = LocalView.current
+ layoutDirection = LocalLayoutDirection.current
+ composable()
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
index 9cc933b..7698818 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -30,7 +31,9 @@
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Alignment
@@ -42,6 +45,7 @@
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
@@ -576,6 +580,19 @@
}
}
+ @Test
+ fun performScrollThroughSemantics_shouldNotHang() {
+ // arrange
+ createViewComposeActivity {
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ repeat(200) { TestItem(it.toString()) }
+ }
+ }
+
+ rule.onNodeWithTag("199").performScrollTo()
+ rule.onNodeWithTag("199").assertIsDisplayed()
+ }
+
private fun createViewComposeActivity(
enableInterop: Boolean = true,
content: @Composable () -> Unit,
@@ -627,7 +644,7 @@
@Composable
private fun TestItem(item: String) {
Box(
- modifier = Modifier.padding(16.dp).height(56.dp).fillMaxWidth().testTag(item),
+ modifier = Modifier.padding(16.dp).height(96.dp).fillMaxWidth().testTag(item),
contentAlignment = Alignment.Center,
) {
BasicText(item)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index 54335b9..da6fe86 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.tests.R
-import androidx.compose.ui.unit.round
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -34,6 +33,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import kotlin.math.absoluteValue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -140,7 +140,10 @@
// assert
rule.runOnIdle {
assertThat(allConsumingConnection.offeredFromChild).isNotEqualTo(Offset.Zero)
- assertThat(connection.consumedDownChain.round()).isEqualTo(Offset.Zero.round())
+ assertThat(connection.consumedDownChain.x.absoluteValue)
+ .isAtMost(ScrollRoundingErrorTolerance)
+ assertThat(connection.consumedDownChain.y.absoluteValue)
+ .isAtMost(ScrollRoundingErrorTolerance)
}
}
@@ -242,3 +245,5 @@
)
}
}
+
+private const val ScrollRoundingErrorTolerance = 1f
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 01965d3..3068fcb 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
@@ -485,6 +485,15 @@
findFocus()?.calculateFocusRectRelativeTo(this)
}
+ // Lets the owner know that a new focus target is available.
+ override fun focusTargetAvailable() {
+ // This signal is used to assign default focus, we don't need to report anything if some
+ // component already has focus.
+ if (focusOwner.rootState.hasFocus) return
+
+ focusableViewAvailable(this@AndroidComposeView)
+ }
+
// TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
// that this common logic can be used by all owners.
private val keyInputModifier =
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
index cbcc468..28652c8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
@@ -191,18 +191,26 @@
/**
* Since our conversion from Float to Int may result in overflow not being reported correctly we
* need to re-add the overflow when passing the consumption data back to compose. We will assume
- * that the overflow was also consumed.
+ * that the overflow was also consumed if something else was consumed.
*/
val overflowX =
if (isNestedScrollInteropIntegerPropagationEnabled) {
- available.x - dx.reverseAxis()
+ if (consumed[0].absoluteValue == 0) {
+ 0f
+ } else {
+ available.x - dx.reverseAxis()
+ }
} else {
0f
}
val overflowY =
if (isNestedScrollInteropIntegerPropagationEnabled) {
- available.y - dy.reverseAxis()
+ if (consumed[1].absoluteValue == 0) {
+ 0f
+ } else {
+ available.y - dy.reverseAxis()
+ }
} else {
0f
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
index 91f8941..e7097ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -154,6 +154,9 @@
@JvmField
var isClearFocusOnResetEnabled: Boolean = false
+ /** Enable initial focus when a focusable is added to a screen with no focusable content. */
+ @Suppress("MutableBareField") @JvmField var isInitialFocusOnFocusableAvailable: Boolean = false
+
/**
* With this flag on, the adaptive refresh rate (ARR) feature will be enabled. A preferred frame
* rate can be set on a Composable through frame rate modifier: [Modifier.preferredFrameRate]
@@ -183,7 +186,7 @@
*/
@Suppress("MutableBareField")
@JvmField
- var isNestedScrollInteropIntegerPropagationEnabled: Boolean = false
+ var isNestedScrollInteropIntegerPropagationEnabled: Boolean = true
/** This flag enables clearing focus on pointer down by default. */
@Suppress("MutableBareField") @JvmField var isClearFocusOnPointerDownEnabled: Boolean = false
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 a945c24..555187c 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
@@ -164,6 +164,9 @@
onFocusedItem: () -> Boolean = { false },
): Boolean
+ /** Lets the FocusOwner know that a focus target is placed. */
+ fun focusTargetAvailable()
+
/** Schedule a FocusTarget node to be invalidated after onApplyChanges. */
fun scheduleInvalidation(node: FocusTargetNode)
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 0dbc209..b9c1f1b 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
@@ -423,6 +423,10 @@
return false
}
+ override fun focusTargetAvailable() {
+ platformFocusOwner.focusTargetAvailable()
+ }
+
override fun scheduleInvalidation(node: FocusTargetNode) {
focusInvalidationManager.scheduleInvalidation(node)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
index 45bd864..62fa1ce 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.focus
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
import androidx.compose.ui.focus.CustomDestinationResult.None
@@ -36,6 +38,7 @@
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.ObserverModifierNode
@@ -55,6 +58,7 @@
private val onDispatchEventsCompleted: ((FocusTargetNode) -> Unit)? = null,
) :
CompositionLocalConsumerModifierNode,
+ LayoutAwareModifierNode,
FocusTargetModifierNode,
ObserverModifierNode,
ModifierLocalModifierNode,
@@ -198,6 +202,13 @@
committedFocusState = null
}
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isInitialFocusOnFocusableAvailable) {
+ node.requireOwner().focusOwner.focusTargetAvailable()
+ }
+ }
+
/**
* Visits parent [FocusPropertiesModifierNode]s and runs
* [FocusPropertiesModifierNode.applyFocusProperties] on each parent. This effectively collects
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
index 6705ad8..3cd4eb5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
@@ -42,4 +42,11 @@
* view that has focus.
*/
fun getEmbeddedViewFocusRect(): Rect?
+
+ // TODO(b/438567275) Add a default implementation that could be used for non-android devices.
+ /**
+ * Let's the owner know that a new focus target is available. The owner can use this as a signal
+ * to run initial focus.
+ */
+ fun focusTargetAvailable() {}
}
diff --git a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
index dba096d..88ce5b3 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -92,9 +92,9 @@
if (value is IBinder) {
this.putBinder(key, value)
} else if (value is Size) {
- BundleApi21ImplKt.putSize(this, key, value)
+ putSize(key, value)
} else if (value is SizeF) {
- BundleApi21ImplKt.putSizeF(this, key, value)
+ putSizeF(key, value)
} else {
val valueType = value.javaClass.canonicalName
throw IllegalArgumentException(
@@ -108,10 +108,3 @@
/** Returns a new empty [Bundle]. */
public fun bundleOf(): Bundle = Bundle(0)
-
-private object BundleApi21ImplKt {
- @JvmStatic fun putSize(bundle: Bundle, key: String, value: Size?) = bundle.putSize(key, value)
-
- @JvmStatic
- fun putSizeF(bundle: Bundle, key: String, value: SizeF?) = bundle.putSizeF(key, value)
-}
diff --git a/core/core-splashscreen/lint-baseline.xml b/core/core-splashscreen/lint-baseline.xml
index e8aaa94..132e0bc 100644
--- a/core/core-splashscreen/lint-baseline.xml
+++ b/core/core-splashscreen/lint-baseline.xml
@@ -19,11 +19,4 @@
file="src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt"/>
</issue>
- <issue
- id="ObsoleteSdkInt"
- message="This folder configuration (`v23`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `drawable`.">
- <location
- file="src/main/res/drawable-v23"/>
- </issue>
-
</issues>
diff --git a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
deleted file mode 100644
index e627d39..0000000
--- a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:gravity="fill">
- <color android:color="?attr/windowSplashScreenBackground" />
- </item>
- <item
- android:gravity="center"
- android:width="@dimen/splashscreen_icon_size_with_background"
- android:height="@dimen/splashscreen_icon_size_with_background"
- android:drawable="@drawable/icon_background">
- </item>
- <item
- android:drawable="?windowSplashScreenAnimatedIcon"
- android:gravity="center"
- android:width="@dimen/splashscreen_icon_size_with_background"
- android:height="@dimen/splashscreen_icon_size_with_background" />
-
- <!-- We mask the outer bounds of the icon like we do on Android 12 -->
- <item
- android:gravity="center"
- android:width="@dimen/splashscreen_icon_mask_size_with_background"
- android:height="@dimen/splashscreen_icon_mask_size_with_background">
- <shape android:shape="oval">
- <solid android:color="@android:color/transparent" />
- <stroke
- android:width="@dimen/splashscreen_icon_mask_stroke_with_background"
- android:color="?windowSplashScreenBackground" />
- </shape>
- </item>
-</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml
deleted file mode 100644
index b0818ef..0000000
--- a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:gravity="fill">
- <color android:color="?attr/windowSplashScreenBackground" />
- </item>
- <item
- android:drawable="?windowSplashScreenAnimatedIcon"
- android:gravity="center"
- android:width="@dimen/splashscreen_icon_size_no_background"
- android:height="@dimen/splashscreen_icon_size_no_background" />
-
- <!-- We mask the outer bounds of the icon like we do on Android 12 -->
- <item
- android:gravity="center"
- android:width="@dimen/splashscreen_icon_mask_size_no_background"
- android:height="@dimen/splashscreen_icon_mask_size_no_background">
- <shape android:shape="oval">
- <solid android:color="@android:color/transparent" />
- <stroke
- android:width="@dimen/splashscreen_icon_mask_stroke_no_background"
- android:color="?windowSplashScreenBackground" />
- </shape>
- </item>
-</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
index 28bd4d5..e627d39 100644
--- a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
+++ b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
@@ -14,5 +14,32 @@
limitations under the License.
-->
-<color xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="?attr/windowSplashScreenBackground" />
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:gravity="fill">
+ <color android:color="?attr/windowSplashScreenBackground" />
+ </item>
+ <item
+ android:gravity="center"
+ android:width="@dimen/splashscreen_icon_size_with_background"
+ android:height="@dimen/splashscreen_icon_size_with_background"
+ android:drawable="@drawable/icon_background">
+ </item>
+ <item
+ android:drawable="?windowSplashScreenAnimatedIcon"
+ android:gravity="center"
+ android:width="@dimen/splashscreen_icon_size_with_background"
+ android:height="@dimen/splashscreen_icon_size_with_background" />
+
+ <!-- We mask the outer bounds of the icon like we do on Android 12 -->
+ <item
+ android:gravity="center"
+ android:width="@dimen/splashscreen_icon_mask_size_with_background"
+ android:height="@dimen/splashscreen_icon_mask_size_with_background">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/transparent" />
+ <stroke
+ android:width="@dimen/splashscreen_icon_mask_stroke_with_background"
+ android:color="?windowSplashScreenBackground" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
index 28bd4d5..b0818ef 100644
--- a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
+++ b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
@@ -14,5 +14,26 @@
limitations under the License.
-->
-<color xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="?attr/windowSplashScreenBackground" />
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:gravity="fill">
+ <color android:color="?attr/windowSplashScreenBackground" />
+ </item>
+ <item
+ android:drawable="?windowSplashScreenAnimatedIcon"
+ android:gravity="center"
+ android:width="@dimen/splashscreen_icon_size_no_background"
+ android:height="@dimen/splashscreen_icon_size_no_background" />
+
+ <!-- We mask the outer bounds of the icon like we do on Android 12 -->
+ <item
+ android:gravity="center"
+ android:width="@dimen/splashscreen_icon_mask_size_no_background"
+ android:height="@dimen/splashscreen_icon_mask_size_no_background">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/transparent" />
+ <stroke
+ android:width="@dimen/splashscreen_icon_mask_stroke_no_background"
+ android:color="?windowSplashScreenBackground" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index c30ee58..cdac8ff 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -758,27 +758,6 @@
</issue>
<issue
- id="ObsoleteSdkInt"
- message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `drawable`.">
- <location
- file="src/main/res/drawable-v21"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `layout`.">
- <location
- file="src/main/res/layout-v21"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `values`.">
- <location
- file="src/main/res/values-v21"/>
- </issue>
-
- <issue
id="KotlinPropertyAccess"
message="The getter return type (`AccessibilityNodeInfoCompat`) and setter parameter type (`View`) getter and setter methods for property `parent` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
errorLine1=" public AccessibilityNodeInfoCompat getParent() {"
diff --git a/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml b/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml
deleted file mode 100644
index 9d0137e..0000000
--- a/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item
- android:color="?android:attr/colorForeground"
- android:state_enabled="false"
- android:alpha="0.5"/>
- <item android:color="?android:attr/colorForeground"/>
-</selector>
diff --git a/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml b/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
index a324c86..9d0137e 100644
--- a/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
+++ b/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
@@ -14,11 +14,10 @@
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:color="?android:attr/colorForeground"
android:state_enabled="false"
- app:alpha="0.5" />
- <item android:color="?android:attr/colorForeground" />
+ android:alpha="0.5"/>
+ <item android:color="?android:attr/colorForeground"/>
</selector>
diff --git a/core/core/src/androidTest/res/values-v21/styles.xml b/core/core/src/androidTest/res/values-v21/styles.xml
deleted file mode 100644
index 9801f42..0000000
--- a/core/core/src/androidTest/res/values-v21/styles.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 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.
--->
-<resources>
- <style name="TestActivityTheme" parent="@android:style/Theme.Material">
- <item name="android:windowAnimationStyle">@null</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/values/styles.xml b/core/core/src/androidTest/res/values/styles.xml
index 02c4f70..734c7df 100644
--- a/core/core/src/androidTest/res/values/styles.xml
+++ b/core/core/src/androidTest/res/values/styles.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
- <style name="TestActivityTheme" parent="@android:style/Theme.Holo">
+ <style name="TestActivityTheme" parent="@android:style/Theme.Material">
<item name="android:windowAnimationStyle">@null</item>
</style>
<style name="TextMediumStyle" parent="@android:style/TextAppearance.Medium">
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index 51c60be..0ee1e0a 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -61,8 +61,8 @@
@SuppressLint("MissingPermission")
public static void setAlarmClock(@NonNull AlarmManager alarmManager, long triggerTime,
@NonNull PendingIntent showIntent, @NonNull PendingIntent operation) {
- Api21Impl.setAlarmClock(alarmManager,
- Api21Impl.createAlarmClockInfo(triggerTime, showIntent), operation);
+ AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(triggerTime, showIntent);
+ alarmManager.setAlarmClock(info, operation);
}
/**
@@ -251,22 +251,6 @@
private AlarmManagerCompat() {
}
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static void setAlarmClock(AlarmManager alarmManager, Object info,
- PendingIntent operation) {
- alarmManager.setAlarmClock((AlarmManager.AlarmClockInfo) info, operation);
- }
-
- static AlarmManager.AlarmClockInfo createAlarmClockInfo(long triggerTime,
- PendingIntent showIntent) {
- return new AlarmManager.AlarmClockInfo(triggerTime, showIntent);
- }
- }
-
@RequiresApi(31)
static class Api31Impl {
private Api31Impl() {
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index ca651d6..5b67006 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -7329,7 +7329,8 @@
if (parcelables != null) {
Action[] actions = new Action[parcelables.size()];
for (int i = 0; i < actions.length; i++) {
- actions[i] = Api20Impl.getActionCompatFromAction(parcelables, i);
+ actions[i] = getActionCompatFromAction(
+ (Notification.Action) parcelables.get(i));
}
Collections.addAll(mActions, (Action[]) actions);
}
@@ -8079,22 +8080,6 @@
/**
* A class for wrapping calls to {@link Notification.WearableExtender} methods which
- * were added in API 20; these calls must be wrapped to avoid performance issues.
- * See the UnsafeNewApiCall lint rule for more details.
- */
- static class Api20Impl {
- private Api20Impl() { }
-
- public static Action getActionCompatFromAction(ArrayList<Parcelable> parcelables,
- int i) {
- // Cast to Notification.Action (added in API 19) must happen in static inner class.
- return NotificationCompat.getActionCompatFromAction(
- (Notification.Action) parcelables.get(i));
- }
- }
-
- /**
- * A class for wrapping calls to {@link Notification.WearableExtender} methods which
* were added in API 24; these calls must be wrapped to avoid performance issues.
* See the UnsafeNewApiCall lint rule for more details.
*/
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 133d549..7a8be8f 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -534,7 +534,7 @@
target = externalCacheDirs[0];
}
} else if (TAG_EXTERNAL_MEDIA.equals(tag)) {
- File[] externalMediaDirs = Api21Impl.getExternalMediaDirs(context);
+ File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
@@ -972,15 +972,4 @@
return filePath.startsWith(rootPath + '/');
}
}
-
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static File[] getExternalMediaDirs(Context context) {
- // Deprecated, otherwise this would belong on ContextCompat as a public method.
- return context.getExternalMediaDirs();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
index 0e70196..bd53921 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -156,7 +156,7 @@
final String[] tags = list.split(",", -1);
final Locale[] localeArray = new Locale[tags.length];
for (int i = 0; i < localeArray.length; i++) {
- localeArray[i] = Api21Impl.forLanguageTag(tags[i]);
+ localeArray[i] = Locale.forLanguageTag(tags[i]);
}
return create(localeArray);
}
@@ -289,10 +289,6 @@
}
return false;
}
-
- static Locale forLanguageTag(String languageTag) {
- return Locale.forLanguageTag(languageTag);
- }
}
@Override
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
index 52e44c1..5438e6b 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
@@ -156,7 +156,7 @@
}
private static String getLikelyScript(Locale locale) {
- final String script = Api21Impl.getScript(locale);
+ final String script = locale.getScript();
if (!script.isEmpty()) {
return script;
} else {
@@ -261,14 +261,4 @@
return computeFirstMatch(Arrays.asList(supportedLocales),
false /* assume English is not supported */);
}
-
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static String getScript(Locale locale) {
- return locale.getScript();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/text/ICUCompat.java b/core/core/src/main/java/androidx/core/text/ICUCompat.java
index c1168e1..bd56e91 100644
--- a/core/core/src/main/java/androidx/core/text/ICUCompat.java
+++ b/core/core/src/main/java/androidx/core/text/ICUCompat.java
@@ -76,15 +76,16 @@
} else {
try {
final Object[] args = new Object[] { locale };
+ Locale localeWithSubtags = (Locale) sAddLikelySubtagsMethod.invoke(null, args);
// ULocale.addLikelySubtags(ULocale) is @NonNull
//noinspection ConstantConditions
- return Api21Impl.getScript((Locale) sAddLikelySubtagsMethod.invoke(null, args));
+ return localeWithSubtags.getScript();
} catch (InvocationTargetException e) {
Log.w(TAG, e);
} catch (IllegalAccessException e) {
Log.w(TAG, e);
}
- return Api21Impl.getScript(locale);
+ return locale.getScript();
}
}
private ICUCompat() {
@@ -108,14 +109,4 @@
return ((ULocale) uLocale).getScript();
}
}
-
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static String getScript(Locale locale) {
- return locale.getScript();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/util/SizeFCompat.java b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
index 3259283..35decbb 100644
--- a/core/core/src/main/java/androidx/core/util/SizeFCompat.java
+++ b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
@@ -72,23 +72,11 @@
/** Converts this {@link SizeFCompat} into a {@link SizeF}. */
public @NonNull SizeF toSizeF() {
- return Api21Impl.toSizeF(this);
+ return new SizeF(getWidth(), getHeight());
}
/** Converts this {@link SizeF} into a {@link SizeFCompat}. */
public static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
- return Api21Impl.toSizeFCompat(size);
- }
-
- private static final class Api21Impl {
- static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
- Preconditions.checkNotNull(size);
- return new SizeFCompat(size.getWidth(), size.getHeight());
- }
-
- static @NonNull SizeF toSizeF(@NonNull SizeFCompat size) {
- Preconditions.checkNotNull(size);
- return new SizeF(size.getWidth(), size.getHeight());
- }
+ return new SizeFCompat(size.getWidth(), size.getHeight());
}
}
diff --git a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
index f3f9d8a..0b3ff60 100644
--- a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
@@ -23,9 +23,8 @@
import org.jspecify.annotations.NonNull;
/**
- * Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the
- * platform implementation will be used and on older platforms a compatible alternative
- * implementation will be used.
+ * Helper for creating path-based {@link Interpolator} instances. The platform implementation will
+ * be used.
*/
public final class PathInterpolatorCompat {
@@ -46,7 +45,7 @@
* @return the {@link Interpolator} representing the {@link Path}
*/
public static @NonNull Interpolator create(@NonNull Path path) {
- return Api21Impl.createPathInterpolator(path);
+ return new PathInterpolator(path);
}
/**
@@ -58,7 +57,7 @@
* @return the {@link Interpolator} representing the quadratic Bezier curve
*/
public static @NonNull Interpolator create(float controlX, float controlY) {
- return Api21Impl.createPathInterpolator(controlX, controlY);
+ return new PathInterpolator(controlX, controlY);
}
/**
@@ -73,25 +72,6 @@
*/
public static @NonNull Interpolator create(float controlX1, float controlY1,
float controlX2, float controlY2) {
- return Api21Impl.createPathInterpolator(controlX1, controlY1, controlX2, controlY2);
- }
-
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static Interpolator createPathInterpolator(Path path) {
- return new PathInterpolator(path);
- }
-
- static Interpolator createPathInterpolator(float controlX, float controlY) {
- return new PathInterpolator(controlX, controlY);
- }
-
- static Interpolator createPathInterpolator(float controlX1, float controlY1,
- float controlX2, float controlY2) {
- return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
- }
+ return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
}
}
diff --git a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
index 100c77f..3bd2dd0 100644
--- a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
@@ -48,7 +48,7 @@
*/
public static void setCheckMarkTintList(@NonNull CheckedTextView textView,
@Nullable ColorStateList tint) {
- Api21Impl.setCheckMarkTintList(textView, tint);
+ textView.setCheckMarkTintList(tint);
}
/**
@@ -57,7 +57,7 @@
* @see #setCheckMarkTintList(CheckedTextView, ColorStateList)
*/
public static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
- return Api21Impl.getCheckMarkTintList(textView);
+ return textView.getCheckMarkTintList();
}
/**
@@ -73,7 +73,7 @@
*/
public static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
PorterDuff.@Nullable Mode tintMode) {
- Api21Impl.setCheckMarkTintMode(textView, tintMode);
+ textView.setCheckMarkTintMode(tintMode);
}
/**
@@ -83,7 +83,7 @@
*/
public static PorterDuff.@Nullable Mode getCheckMarkTintMode(
@NonNull CheckedTextView textView) {
- return Api21Impl.getCheckMarkTintMode(textView);
+ return textView.getCheckMarkTintMode();
}
/**
@@ -97,28 +97,4 @@
public static @Nullable Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
return textView.getCheckMarkDrawable();
}
-
- private static class Api21Impl {
-
- private Api21Impl() {
- }
-
- static void setCheckMarkTintList(@NonNull CheckedTextView textView,
- @Nullable ColorStateList tint) {
- textView.setCheckMarkTintList(tint);
- }
-
- static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
- return textView.getCheckMarkTintList();
- }
-
- static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
- PorterDuff.@Nullable Mode tintMode) {
- textView.setCheckMarkTintMode(tintMode);
- }
-
- static PorterDuff.@Nullable Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
- return textView.getCheckMarkTintMode();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
index 89cc49f..2376edb 100644
--- a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
@@ -47,7 +47,7 @@
*/
public static void setButtonTintList(@NonNull CompoundButton button,
@Nullable ColorStateList tint) {
- Api21Impl.setButtonTintList(button, tint);
+ button.setButtonTintList(tint);
}
/**
@@ -56,7 +56,7 @@
* @see #setButtonTintList(CompoundButton, ColorStateList)
*/
public static @Nullable ColorStateList getButtonTintList(@NonNull CompoundButton button) {
- return Api21Impl.getButtonTintList(button);
+ return button.getButtonTintList();
}
/**
@@ -73,7 +73,7 @@
*/
public static void setButtonTintMode(@NonNull CompoundButton button,
PorterDuff.@Nullable Mode tintMode) {
- Api21Impl.setButtonTintMode(button, tintMode);
+ button.setButtonTintMode(tintMode);
}
/**
@@ -82,7 +82,7 @@
* @see #setButtonTintMode(CompoundButton, PorterDuff.Mode)
*/
public static PorterDuff.@Nullable Mode getButtonTintMode(@NonNull CompoundButton button) {
- return Api21Impl.getButtonTintMode(button);
+ return button.getButtonTintMode();
}
/**
@@ -93,26 +93,4 @@
public static @Nullable Drawable getButtonDrawable(@NonNull CompoundButton button) {
return button.getButtonDrawable();
}
-
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- static void setButtonTintList(CompoundButton compoundButton, ColorStateList tint) {
- compoundButton.setButtonTintList(tint);
- }
-
- static ColorStateList getButtonTintList(CompoundButton compoundButton) {
- return compoundButton.getButtonTintList();
- }
-
- static void setButtonTintMode(CompoundButton compoundButton, PorterDuff.Mode tintMode) {
- compoundButton.setButtonTintMode(tintMode);
- }
-
- static PorterDuff.Mode getButtonTintMode(CompoundButton compoundButton) {
- return compoundButton.getButtonTintMode();
- }
- }
}
diff --git a/core/core/src/main/res/drawable-v21/notification_action_background.xml b/core/core/src/main/res/drawable/notification_action_background.xml
similarity index 100%
rename from core/core/src/main/res/drawable-v21/notification_action_background.xml
rename to core/core/src/main/res/drawable/notification_action_background.xml
diff --git a/core/core/src/main/res/layout-v21/notification_action.xml b/core/core/src/main/res/layout-v21/notification_action.xml
deleted file mode 100644
index 7199c25..0000000
--- a/core/core/src/main/res/layout-v21/notification_action.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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
- -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/Widget.Compat.NotificationActionContainer"
- android:id="@+id/action_container"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="48dp"
- android:paddingStart="4dp"
- android:orientation="horizontal">
- <ImageView
- android:id="@+id/action_image"
- android:layout_width="@dimen/notification_action_icon_size"
- android:layout_height="@dimen/notification_action_icon_size"
- android:layout_gravity="center|start"
- android:scaleType="centerInside"/>
- <TextView
- style="@style/Widget.Compat.NotificationActionText"
- android:id="@+id/action_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center|start"
- android:paddingStart="4dp"
- android:singleLine="true"
- android:ellipsize="end"
- android:clickable="false"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_action_tombstone.xml b/core/core/src/main/res/layout-v21/notification_action_tombstone.xml
deleted file mode 100644
index 7ef38fa..0000000
--- a/core/core/src/main/res/layout-v21/notification_action_tombstone.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/Widget.Compat.NotificationActionContainer"
- android:id="@+id/action_container"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="48dp"
- android:paddingStart="4dp"
- android:orientation="horizontal"
- android:enabled="false"
- android:background="@null">
- <ImageView
- android:id="@+id/action_image"
- android:layout_width="@dimen/notification_action_icon_size"
- android:layout_height="@dimen/notification_action_icon_size"
- android:layout_gravity="center|start"
- android:scaleType="centerInside"
- android:enabled="false"
- android:alpha="0.5"/>
- <TextView
- style="@style/Widget.Compat.NotificationActionText"
- android:id="@+id/action_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center|start"
- android:paddingStart="4dp"
- android:singleLine="true"
- android:ellipsize="end"
- android:clickable="false"
- android:enabled="false"
- android:alpha="0.5"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_template_custom_big.xml b/core/core/src/main/res/layout-v21/notification_template_custom_big.xml
deleted file mode 100644
index 9e3666e..0000000
--- a/core/core/src/main/res/layout-v21/notification_template_custom_big.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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
- -->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/notification_background"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
- <include layout="@layout/notification_template_icon_group"
- android:layout_width="@dimen/notification_large_icon_width"
- android:layout_height="@dimen/notification_large_icon_height"
- />
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="top"
- android:layout_marginStart="@dimen/notification_large_icon_width"
- android:orientation="vertical" >
- <LinearLayout
- android:id="@+id/notification_main_column_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="@dimen/notification_large_icon_height"
- android:orientation="horizontal">
- <FrameLayout
- android:id="@+id/notification_main_column"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_marginEnd="8dp"
- android:layout_marginBottom="8dp"/>
- <FrameLayout
- android:id="@+id/right_side"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:paddingTop="@dimen/notification_right_side_padding_top">
- <ViewStub android:id="@+id/time"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="end|top"
- android:visibility="gone"
- android:layout="@layout/notification_template_part_time" />
- <ViewStub android:id="@+id/chronometer"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="end|top"
- android:visibility="gone"
- android:layout="@layout/notification_template_part_chronometer" />
- <TextView android:id="@+id/info"
- android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="20dp"
- android:layout_gravity="end|bottom"
- android:singleLine="true"
- />
- </FrameLayout>
- </LinearLayout>
- <ImageView
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:id="@+id/action_divider"
- android:visibility="gone"
- android:background="#29000000" />
- <LinearLayout
- android:id="@+id/actions"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="-8dp"
- android:orientation="horizontal"
- android:visibility="gone"
- >
- <!-- actions will be added here -->
- </LinearLayout>
- </LinearLayout>
-</FrameLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_template_icon_group.xml b/core/core/src/main/res/layout-v21/notification_template_icon_group.xml
deleted file mode 100644
index 8fadd67..0000000
--- a/core/core/src/main/res/layout-v21/notification_template_icon_group.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2017 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.
- -->
-
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/notification_large_icon_width"
- android:layout_height="@dimen/notification_large_icon_height"
- android:id="@+id/icon_group"
->
- <ImageView android:id="@+id/icon"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/notification_big_circle_margin"
- android:layout_marginBottom="@dimen/notification_big_circle_margin"
- android:layout_marginStart="@dimen/notification_big_circle_margin"
- android:layout_marginEnd="@dimen/notification_big_circle_margin"
- android:scaleType="centerInside"
- />
- <ImageView android:id="@+id/right_icon"
- android:layout_width="@dimen/notification_right_icon_size"
- android:layout_height="@dimen/notification_right_icon_size"
- android:layout_gravity="end|bottom"
- android:scaleType="centerInside"
- android:visibility="gone"
- android:layout_marginEnd="8dp"
- android:layout_marginBottom="8dp"
- />
-</FrameLayout>
diff --git a/core/core/src/main/res/layout/notification_action.xml b/core/core/src/main/res/layout/notification_action.xml
index 3ffd9a1..7199c25 100644
--- a/core/core/src/main/res/layout/notification_action.xml
+++ b/core/core/src/main/res/layout/notification_action.xml
@@ -20,7 +20,6 @@
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"
- android:paddingLeft="4dp"
android:paddingStart="4dp"
android:orientation="horizontal">
<ImageView
@@ -32,11 +31,9 @@
<TextView
style="@style/Widget.Compat.NotificationActionText"
android:id="@+id/action_text"
- android:textColor="#ccc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
- android:paddingLeft="4dp"
android:paddingStart="4dp"
android:singleLine="true"
android:ellipsize="end"
diff --git a/core/core/src/main/res/layout/notification_action_tombstone.xml b/core/core/src/main/res/layout/notification_action_tombstone.xml
index e9d4e37..7ef38fa 100644
--- a/core/core/src/main/res/layout/notification_action_tombstone.xml
+++ b/core/core/src/main/res/layout/notification_action_tombstone.xml
@@ -21,7 +21,6 @@
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"
- android:paddingLeft="4dp"
android:paddingStart="4dp"
android:orientation="horizontal"
android:enabled="false"
@@ -40,8 +39,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
- android:textColor="#ccc"
- android:paddingLeft="4dp"
android:paddingStart="4dp"
android:singleLine="true"
android:ellipsize="end"
diff --git a/core/core/src/main/res/layout/notification_template_custom_big.xml b/core/core/src/main/res/layout/notification_template_custom_big.xml
index d0519dc..6b8f7cf 100644
--- a/core/core/src/main/res/layout/notification_template_custom_big.xml
+++ b/core/core/src/main/res/layout/notification_template_custom_big.xml
@@ -19,23 +19,20 @@
android:id="@+id/notification_background"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
- <ImageView android:id="@+id/icon"
+ <include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
android:layout_height="@dimen/notification_large_icon_height"
- android:scaleType="center"
- />
+ />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
+ android:layout_marginStart="@dimen/notification_large_icon_width"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/notification_main_column_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/notification_large_icon_width"
- android:layout_marginStart="@dimen/notification_large_icon_width"
- android:paddingTop="@dimen/notification_main_column_padding_top"
android:minHeight="@dimen/notification_large_icon_height"
android:orientation="horizontal">
<FrameLayout
@@ -43,16 +40,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:layout_marginLeft="@dimen/notification_content_margin_start"
- android:layout_marginStart="@dimen/notification_content_margin_start"
- android:layout_marginBottom="8dp"
- android:layout_marginRight="8dp"
- android:layout_marginEnd="8dp" />
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"/>
<FrameLayout
android:id="@+id/right_side"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:paddingTop="@dimen/notification_right_side_padding_top">
<ViewStub android:id="@+id/time"
@@ -67,50 +60,30 @@
android:layout_gravity="end|top"
android:visibility="gone"
android:layout="@layout/notification_template_part_chronometer" />
- <LinearLayout
- android:layout_width="match_parent"
+ <TextView android:id="@+id/info"
+ android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
+ android:layout_marginTop="20dp"
android:layout_gravity="end|bottom"
- android:layout_marginTop="20dp">
- <TextView android:id="@+id/info"
- android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:singleLine="true"
+ android:singleLine="true"
/>
- <ImageView android:id="@+id/right_icon"
- android:layout_width="16dp"
- android:layout_height="16dp"
- android:layout_gravity="center"
- android:layout_marginLeft="8dp"
- android:layout_marginStart="8dp"
- android:scaleType="centerInside"
- android:visibility="gone"
- android:alpha="0.6"
- />
- </LinearLayout>
</FrameLayout>
</LinearLayout>
<ImageView
android:layout_width="match_parent"
- android:layout_height="1px"
+ android:layout_height="1dp"
android:id="@+id/action_divider"
android:visibility="gone"
- android:layout_marginLeft="@dimen/notification_large_icon_width"
- android:layout_marginStart="@dimen/notification_large_icon_width"
- android:background="?android:attr/dividerHorizontal" />
+ android:background="#29000000" />
<LinearLayout
android:id="@+id/actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginStart="-8dp"
android:orientation="horizontal"
android:visibility="gone"
- android:showDividers="middle"
- android:divider="?android:attr/listDivider"
- android:dividerPadding="12dp"
- android:layout_marginLeft="@dimen/notification_large_icon_width"
- android:layout_marginStart="@dimen/notification_large_icon_width" >
+ >
<!-- actions will be added here -->
</LinearLayout>
</LinearLayout>
diff --git a/core/core/src/main/res/layout/notification_template_icon_group.xml b/core/core/src/main/res/layout/notification_template_icon_group.xml
index f225737..ddb4587 100644
--- a/core/core/src/main/res/layout/notification_template_icon_group.xml
+++ b/core/core/src/main/res/layout/notification_template_icon_group.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2016 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -12,13 +12,31 @@
~ 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
+ ~ limitations under the License.
-->
-<ImageView
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/icon"
android:layout_width="@dimen/notification_large_icon_width"
android:layout_height="@dimen/notification_large_icon_height"
- android:scaleType="centerCrop"
-/>
+ android:id="@+id/icon_group"
+ >
+ <ImageView android:id="@+id/icon"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/notification_big_circle_margin"
+ android:layout_marginBottom="@dimen/notification_big_circle_margin"
+ android:layout_marginStart="@dimen/notification_big_circle_margin"
+ android:layout_marginEnd="@dimen/notification_big_circle_margin"
+ android:scaleType="centerInside"
+ />
+ <ImageView android:id="@+id/right_icon"
+ android:layout_width="@dimen/notification_right_icon_size"
+ android:layout_height="@dimen/notification_right_icon_size"
+ android:layout_gravity="end|bottom"
+ android:scaleType="centerInside"
+ android:visibility="gone"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"
+ />
+</FrameLayout>
diff --git a/core/core/src/main/res/values-v21/colors.xml b/core/core/src/main/res/values-v21/colors.xml
deleted file mode 100644
index b093f0d..0000000
--- a/core/core/src/main/res/values-v21/colors.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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
- -->
-<resources>
- <color name="notification_action_color_filter">@color/androidx_core_secondary_text_default_material_light</color>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/main/res/values-v21/dimens.xml b/core/core/src/main/res/values-v21/dimens.xml
deleted file mode 100644
index b56faf1..0000000
--- a/core/core/src/main/res/values-v21/dimens.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<resources>
- <!-- the margin at the beginning of the notification content -->
- <dimen name="notification_content_margin_start">0dp</dimen>
- <!-- image margin on the large icon in the narrow media template -->
- <dimen name="notification_media_narrow_margin">12dp</dimen>
- <!-- the top padding of the notification content -->
- <dimen name="notification_main_column_padding_top">0dp</dimen>
-</resources>
diff --git a/core/core/src/main/res/values-v21/styles.xml b/core/core/src/main/res/values-v21/styles.xml
deleted file mode 100644
index a142274..0000000
--- a/core/core/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<!--
- ~ Copyright (C) 2017 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.
- -->
-
-<resources>
-
- <!-- Use platform styles -->
- <style name="TextAppearance.Compat.Notification"
- parent="@android:style/TextAppearance.Material.Notification"/>
-
- <style name="TextAppearance.Compat.Notification.Title"
- parent="@android:style/TextAppearance.Material.Notification.Title"/>
-
- <style name="TextAppearance.Compat.Notification.Info"
- parent="@android:style/TextAppearance.Material.Notification.Info"/>
-
- <style name="TextAppearance.Compat.Notification.Time"
- parent="@android:style/TextAppearance.Material.Notification.Time"/>
-
- <style name="Widget.Compat.NotificationActionText" parent="">
- <item name="android:textAppearance">?android:attr/textAppearanceButton</item>
- <item name="android:textColor">@color/androidx_core_secondary_text_default_material_light</item>
- <item name="android:textSize">@dimen/notification_action_text_size</item>
- </style>
-
- <style name="Widget.Compat.NotificationActionContainer" parent="">
- <item name="android:background">@drawable/notification_action_background</item>
- </style>
-
-</resources>
diff --git a/core/core/src/main/res/values/colors.xml b/core/core/src/main/res/values/colors.xml
index cad8425..2ad5208 100644
--- a/core/core/src/main/res/values/colors.xml
+++ b/core/core/src/main/res/values/colors.xml
@@ -17,7 +17,7 @@
<resources>
<drawable name="notification_template_icon_bg">#3333B5E5</drawable>
<drawable name="notification_template_icon_low_bg">#0cffffff</drawable>
- <color name="notification_action_color_filter">#ffffffff</color>
+ <color name="notification_action_color_filter">@color/androidx_core_secondary_text_default_material_light</color>
<color name="notification_icon_bg_color">#ff9e9e9e</color>
<!-- The color of the Decline and Hang Up actions on a CallStyle notification -->
diff --git a/core/core/src/main/res/values/dimens.xml b/core/core/src/main/res/values/dimens.xml
index 36644cb..bae54ab 100644
--- a/core/core/src/main/res/values/dimens.xml
+++ b/core/core/src/main/res/values/dimens.xml
@@ -60,13 +60,13 @@
<dimen name="notification_small_icon_size_as_large">24dp</dimen>
<!-- the margin at the beginning of the notification content -->
- <dimen name="notification_content_margin_start">8dp</dimen>
+ <dimen name="notification_content_margin_start">0dp</dimen>
<!-- image margin on the large icon in the narrow media template -->
- <dimen name="notification_media_narrow_margin">@dimen/notification_content_margin_start</dimen>
+ <dimen name="notification_media_narrow_margin">12dp</dimen>
<!-- the top padding of the notification content -->
- <dimen name="notification_main_column_padding_top">10dp</dimen>
+ <dimen name="notification_main_column_padding_top">0dp</dimen>
<!-- the paddingtop on the right side of the notification (for time etc.) -->
<dimen name="notification_right_side_padding_top">4dp</dimen>
diff --git a/core/core/src/main/res/values/styles.xml b/core/core/src/main/res/values/styles.xml
index 3503e77..0060e41 100644
--- a/core/core/src/main/res/values/styles.xml
+++ b/core/core/src/main/res/values/styles.xml
@@ -17,22 +17,27 @@
<resources>
<style name="TextAppearance.Compat.Notification"
- parent="@android:style/TextAppearance.StatusBar.EventContent"/>
+ parent="@android:style/TextAppearance.Material.Notification"/>
<style name="TextAppearance.Compat.Notification.Title"
- parent="@android:style/TextAppearance.StatusBar.EventContent.Title"/>
+ parent="@android:style/TextAppearance.Material.Notification.Title"/>
- <style name="TextAppearance.Compat.Notification.Info">
- <item name="android:textSize">12sp</item>
- <item name="android:textColor">?android:attr/textColorSecondary</item>
- </style>
- <style name="TextAppearance.Compat.Notification.Time">
- <item name="android:textSize">12sp</item>
- <item name="android:textColor">?android:attr/textColorSecondary</item>
- </style>
+ <style name="TextAppearance.Compat.Notification.Info"
+ parent="@android:style/TextAppearance.Material.Notification.Info"/>
+
+ <style name="TextAppearance.Compat.Notification.Time"
+ parent="@android:style/TextAppearance.Material.Notification.Time"/>
+
<style name="TextAppearance.Compat.Notification.Line2"
parent="TextAppearance.Compat.Notification.Info" />
- <style name="Widget.Compat.NotificationActionText" parent=""/>
- <style name="Widget.Compat.NotificationActionContainer" parent=""/>
+ <style name="Widget.Compat.NotificationActionText" parent="">
+ <item name="android:textAppearance">?android:attr/textAppearanceButton</item>
+ <item name="android:textColor">@color/androidx_core_secondary_text_default_material_light</item>
+ <item name="android:textSize">@dimen/notification_action_text_size</item>
+ </style>
+
+ <style name="Widget.Compat.NotificationActionContainer" parent="">
+ <item name="android:background">@drawable/notification_action_background</item>
+ </style>
</resources>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5266e42..f636c27 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -215,16 +215,10 @@
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" }
kotlinStdlibCommon = { module = "org.jetbrains.kotlin:kotlin-stdlib-common" }
-# The Kotlin/JS and Kotlin/Wasm standard library must match the compiler
-kotlinStdlibJs = { module = "org.jetbrains.kotlin:kotlin-stdlib-js", version.ref = "kotlin" }
-kotlinStdlibWasm = { module = "org.jetbrains.kotlin:kotlin-stdlib-wasm-js", version.ref = "kotlin" }
kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test" }
-kotlinTestForWasmTests = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinTestAnnotationsCommon = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common" }
kotlinTestCommon = { module = "org.jetbrains.kotlin:kotlin-test-common" }
kotlinTestJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit" }
-kotlinTestJs = { module = "org.jetbrains.kotlin:kotlin-test-js" }
-kotlinTestWasm = { module = "org.jetbrains.kotlin:kotlin-test-wasm-js" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
kotlinPoet = { module = "com.squareup:kotlinpoet", version = "2.1.0" }
kotlinPoetJavaPoet = { module = "com.squareup:kotlinpoet-javapoet", version = "2.1.0" }
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index 88b6bef..11366d0 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -57,8 +57,7 @@
commonTest {
dependencies {
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinTestForWasmTests)
+ implementation(libs.kotlinTest)
implementation(project(":kruth:kruth"))
}
}
@@ -112,38 +111,13 @@
dependsOn(nonJvmTest)
}
- jsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
- wasmJsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestWasm)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(nonJvmTest)
}
}
}
diff --git a/kruth/kruth/build.gradle b/kruth/kruth/build.gradle
index 9f95fac..7a7d860 100644
--- a/kruth/kruth/build.gradle
+++ b/kruth/kruth/build.gradle
@@ -104,33 +104,11 @@
webTest {
dependsOn(nonJvmTest)
dependencies {
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinTestForWasmTests)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
}
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
- jsTest {
- dependsOn(webTest)
- }
-
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
- wasmJsTest {
- dependsOn(webTest)
- }
-
nativeMain {
dependsOn(nonJvmMain)
}
@@ -140,12 +118,11 @@
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
- target.compilations["main"].defaultSourceSet {
- dependsOn(nativeMain)
- }
- target.compilations["test"].defaultSourceSet {
- dependsOn(nativeTest)
- }
+ target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(webTest)
}
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 324d1f1..d10a78c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -46,7 +46,7 @@
CORE_REMOTEVIEWS = "1.1.0-rc01"
CORE_ROLE = "1.2.0-alpha01"
CORE_SPLASHSCREEN = "1.2.0-rc01"
-CORE_TELECOM = "1.0.0-beta01"
+CORE_TELECOM = "1.1.0-alpha01"
CORE_UWB = "1.0.0-alpha10"
CORE_VIEWTREE = "1.1.0-alpha01"
CREDENTIALS = "1.6.0-alpha05"
@@ -183,18 +183,17 @@
WEAR_WATCHFACEPUSH = "1.0.0-alpha01"
WEBKIT = "1.15.0-alpha02"
# Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.5.0-beta02"
+WINDOW = "1.5.0-rc01"
WINDOW_EXTENSIONS = "1.5.0"
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
WORK = "2.11.0-alpha01"
-XR = "1.0.0-alpha05"
-XR_ARCORE = "1.0.0-alpha05"
-XR_COMPOSE = "1.0.0-alpha06"
+XR_ARCORE = "1.0.0-alpha06"
+XR_COMPOSE = "1.0.0-alpha07"
XR_GLIMMER = "1.0.0-alpha01"
XR_PROJECTED = "1.0.0-alpha01"
-XR_RUNTIME = "1.0.0-alpha05"
-XR_SCENECORE = "1.0.0-alpha06"
+XR_RUNTIME = "1.0.0-alpha06"
+XR_SCENECORE = "1.0.0-alpha07"
[groups]
ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
@@ -336,7 +335,6 @@
WINDOW_EXTENSIONS_CORE = { group = "androidx.window.extensions.core", atomicGroupVersion = "versions.WINDOW_EXTENSIONS_CORE" }
WINDOW_SIDECAR = { group = "androidx.window.sidecar", atomicGroupVersion = "versions.WINDOW_SIDECAR" }
WORK = { group = "androidx.work", atomicGroupVersion = "versions.WORK" }
-XR = { group = "androidx.xr", atomicGroupVersion = "versions.XR" }
XR_ARCORE = { group = "androidx.xr.arcore", atomicGroupVersion = "versions.XR_ARCORE" }
XR_COMPOSE = { group = "androidx.xr.compose", atomicGroupVersion = "versions.XR_COMPOSE" }
XR_GLIMMER = { group = "androidx.xr.glimmer", atomicGroupVersion = "versions.XR_GLIMMER" }
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index d6ae520..71ebbbe 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -73,20 +73,6 @@
}
}
- wasmJsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- jsMain {
- dependsOn(nonJvmMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType !in [KotlinPlatformType.jvm, KotlinPlatformType.common]) {
target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 452984a..dd0c22a 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -122,36 +122,6 @@
dependsOn(nonAndroidTest)
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestWasm)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/lifecycle/lifecycle-runtime-testing/build.gradle b/lifecycle/lifecycle-runtime-testing/build.gradle
index 5907e29..958c4af 100644
--- a/lifecycle/lifecycle-runtime-testing/build.gradle
+++ b/lifecycle/lifecycle-runtime-testing/build.gradle
@@ -117,36 +117,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index a8eedbb..7c0846f 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -146,36 +146,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet {
diff --git a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
index 798d9da..c962d7a 100644
--- a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
+++ b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
@@ -123,6 +123,7 @@
if (state == next) {
return
}
+ @Suppress("NewApi") // b/437073246
checkLifecycleStateTransition(lifecycleOwner.get(), state, next)
state = next
@@ -176,6 +177,7 @@
if (previous != null) {
return
}
+ @Suppress("NewApi") // b/437073246
val lifecycleOwner =
lifecycleOwner.get()
?: // it is null we should be destroyed. Fallback quickly
@@ -272,6 +274,7 @@
// happens only on the top of stack (never in reentrance),
// so it doesn't have to take in account parents
private fun sync() {
+ @Suppress("NewApi") // b/437073246
val lifecycleOwner =
lifecycleOwner.get()
?: throw IllegalStateException(
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 08615f5..4c2a6fe 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -56,7 +56,7 @@
commonMain {
dependencies {
api("androidx.annotation:annotation:1.9.1")
- api(project(":savedstate:savedstate"))
+ api("androidx.savedstate:savedstate:1.4.0-alpha02")
api(project(":lifecycle:lifecycle-viewmodel"))
implementation(project(":lifecycle:lifecycle-common"))
api(libs.kotlinStdlib)
@@ -172,36 +172,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
diff --git a/lifecycle/lifecycle-viewmodel-testing/build.gradle b/lifecycle/lifecycle-viewmodel-testing/build.gradle
index 01062cf..86b8630 100644
--- a/lifecycle/lifecycle-viewmodel-testing/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-testing/build.gradle
@@ -163,36 +163,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 5636018..d2c81ec 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -144,36 +144,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
diff --git a/mediarouter/mediarouter/lint-baseline.xml b/mediarouter/mediarouter/lint-baseline.xml
index be36a5a..19e2e94 100644
--- a/mediarouter/mediarouter/lint-baseline.xml
+++ b/mediarouter/mediarouter/lint-baseline.xml
@@ -46,22 +46,4 @@
file="src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java"/>
</issue>
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 23"
- errorLine1=" if (Build.VERSION.SDK_INT >= 23) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 23"
- errorLine1=" @RequiresApi(23)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java"/>
- </issue>
-
</issues>
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index 4a4e65c..768857d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -203,10 +203,26 @@
private static boolean isSuitableDeviceAlreadyConnectedAsAudioOutput(
@NonNull Context context) {
- if (Build.VERSION.SDK_INT >= 23) {
- return Api23Impl.isSuitableDeviceAlreadyConnectedAsAudioOutput(context);
+ AudioManager audioManager = context.getSystemService(AudioManager.class);
+ AudioDeviceInfo[] audioDeviceInfos = audioManager.getDevices(
+ AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : audioDeviceInfos) {
+ switch (device.getType()) {
+ case AudioDeviceInfo.TYPE_BLE_BROADCAST:
+ case AudioDeviceInfo.TYPE_BLE_HEADSET:
+ case AudioDeviceInfo.TYPE_BLE_SPEAKER:
+ case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+ case AudioDeviceInfo.TYPE_HEARING_AID:
+ case AudioDeviceInfo.TYPE_LINE_ANALOG:
+ case AudioDeviceInfo.TYPE_LINE_DIGITAL:
+ case AudioDeviceInfo.TYPE_USB_DEVICE:
+ case AudioDeviceInfo.TYPE_USB_HEADSET:
+ case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+ case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+ return true;
+ }
}
- return true;
+ return false;
}
@RequiresApi(30)
@@ -230,33 +246,4 @@
return mediaRouter2.showSystemOutputSwitcher();
}
}
-
- @RequiresApi(23)
- static final class Api23Impl {
- private Api23Impl() {
- }
-
- public static boolean isSuitableDeviceAlreadyConnectedAsAudioOutput(Context context) {
- AudioManager audioManager = context.getSystemService(AudioManager.class);
- AudioDeviceInfo[] audioDeviceInfos = audioManager.getDevices(
- AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : audioDeviceInfos) {
- switch (device.getType()) {
- case AudioDeviceInfo.TYPE_BLE_BROADCAST:
- case AudioDeviceInfo.TYPE_BLE_HEADSET:
- case AudioDeviceInfo.TYPE_BLE_SPEAKER:
- case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
- case AudioDeviceInfo.TYPE_HEARING_AID:
- case AudioDeviceInfo.TYPE_LINE_ANALOG:
- case AudioDeviceInfo.TYPE_LINE_DIGITAL:
- case AudioDeviceInfo.TYPE_USB_DEVICE:
- case AudioDeviceInfo.TYPE_USB_HEADSET:
- case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
- case AudioDeviceInfo.TYPE_WIRED_HEADSET:
- return true;
- }
- }
- return false;
- }
- }
}
diff --git a/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java b/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
index 66554c9..d175ada 100644
--- a/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
+++ b/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
@@ -35,7 +35,6 @@
import android.provider.Settings;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
@@ -96,7 +95,6 @@
}
}
- @RequiresApi(23)
private static void setupConnectedAudioOutput(int... deviceTypes) {
ShadowAudioManager shadowAudioManager = shadowOf(
ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
diff --git a/navigation/navigation-safe-args-gradle-plugin/build.gradle b/navigation/navigation-safe-args-gradle-plugin/build.gradle
index 478b598..895151a 100644
--- a/navigation/navigation-safe-args-gradle-plugin/build.gradle
+++ b/navigation/navigation-safe-args-gradle-plugin/build.gradle
@@ -39,7 +39,7 @@
}
dependencies {
- compileOnly("com.android.tools.build:gradle:8.1.1")
+ compileOnly("com.android.tools.build:gradle:8.4.2")
implementation(libs.kotlinStdlib)
implementation(project(":navigation:navigation-safe-args-generator"))
implementation(gradleApi())
@@ -49,10 +49,10 @@
testImplementation(project(":internal-testutils-gradle-plugin"))
testImplementation(libs.hamcrestCore)
testImplementation(libs.junit)
- testPlugin("com.android.tools.build:gradle:8.1.1")
- testPlugin("com.android.tools.build:aapt2:8.1.1-10154469")
- testPlugin("com.android.tools.build:aapt2:8.1.1-10154469:linux")
- testPlugin("com.android.tools.build:aapt2:8.1.1-10154469:osx")
+ testPlugin("com.android.tools.build:gradle:8.4.2")
+ testPlugin("com.android.tools.build:aapt2:8.4.2-11315950")
+ testPlugin("com.android.tools.build:aapt2:8.4.2-11315950:linux")
+ testPlugin("com.android.tools.build:aapt2:8.4.2-11315950:osx")
testPlugin(libs.kotlinGradlePlugin)
}
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
index 67a1adf..7e33e09 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -97,10 +97,10 @@
val componentsExtension =
project.extensions.findByType(AndroidComponentsExtension::class.java)
?: throw GradleException("safeargs plugin must be used with android plugin")
- if (componentsExtension.pluginVersion < AndroidPluginVersion(7, 3)) {
+ if (componentsExtension.pluginVersion < AndroidPluginVersion(8, 4)) {
throw GradleException(
"safeargs Gradle plugin is only compatible with Android " +
- "Gradle plugin (AGP) version 7.3.0 or higher (found " +
+ "Gradle plugin (AGP) version 8.4.0 or higher (found " +
"${componentsExtension.pluginVersion})."
)
}
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
index c99274e..6964918 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
@@ -75,8 +75,6 @@
.withPluginClasspath()
// b/175897186 set explicit metaspace size in hopes of fewer crashes
.withArguments("-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m", *args)
- // Run tests using Gradle 8.14 to support AGP version used for the tests, b/431847270
- projectSetup.setUpGradleVersion(runner, "8.14")
return runner
}
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
index 3e8a0a8..8b071d5 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
@@ -14,5 +14,4 @@
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="safe.gradle.test.app.library" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/navigation3/navigation3-runtime/lint-baseline.xml b/navigation3/navigation3-runtime/lint-baseline.xml
deleted file mode 100644
index cf555c9..0000000
--- a/navigation3/navigation3-runtime/lint-baseline.xml
+++ /dev/null
@@ -1,130 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.13.0-alpha01" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-alpha01)" variant="all" version="8.13.0-alpha01">
-
- <issue
- id="MutableCollectionMutableState"
- message="Creating a MutableState object with a mutable collection type"
- errorLine1=" currStateMap = remember { mutableStateOf(stateMap1) }"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" backStack.mapIndexed { index, key ->"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" latestDecorators.reversed().forEach { it.onPop(contentKey) }"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" latestDecorators.reversed().forEach { it.onPop(contentKey) }"
- errorLine2=" ~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" val latestBackStack by rememberUpdatedState(entries.map { it.contentKey })"
- errorLine2=" ~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" latestBackStack.forEach { contentKey ->"
- errorLine2=" ~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" latestDecorators.reversed().forEach { it.onPop(contentKey) }"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" latestDecorators.reversed().forEach { it.onPop(contentKey) }"
- errorLine2=" ~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" save = { list -> list.map { encodeToSavedState(serializer, it) } },"
- errorLine2=" ~~~">
- <location
- file="src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" restore = { list -> list.map { decodeFromSavedState(serializer, it) } },"
- errorLine2=" ~~~">
- <location
- file="src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt"/>
- </issue>
-
- <issue
- id="ListIterator"
- message="Creating an unnecessary Iterator to iterate through a List"
- errorLine1=" .distinct()"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt"/>
- </issue>
-
- <issue
- id="UnnecessaryLambdaCreation"
- message="Creating an unnecessary lambda to emit a captured lambda"
- errorLine1=" CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo) { content() }"
- errorLine2=" ~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="field popCallbacks with type LinkedHashMap<Integer, Function1<Object, Unit>>: replace with IntObjectMap"
- errorLine1=" val popCallbacks: LinkedHashMap<Int, (key: Any) -> Unit> = LinkedHashMap()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="return type LinkedHashMap<Integer, Function1<Object, Unit>> of getPopCallbacks: replace with IntObjectMap"
- errorLine1=" val popCallbacks: LinkedHashMap<Int, (key: Any) -> Unit> = LinkedHashMap()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
- </issue>
-
-</issues>
diff --git a/navigation3/navigation3-runtime/samples/build.gradle b/navigation3/navigation3-runtime/samples/build.gradle
index 2c5af81..4ff119f 100644
--- a/navigation3/navigation3-runtime/samples/build.gradle
+++ b/navigation3/navigation3-runtime/samples/build.gradle
@@ -50,7 +50,7 @@
implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation(libs.kotlinSerializationCore)
- implementation(project(":savedstate:savedstate-ktx"))
+ implementation("androidx.savedstate:savedstate-ktx:1.4.0-alpha02")
implementation(project(":navigation3:navigation3-runtime"))
}
diff --git a/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt b/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
index 1947cff..d78f601 100644
--- a/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
+++ b/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
@@ -16,6 +16,7 @@
package androidx.navigation3.runtime
+import android.annotation.SuppressLint
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -714,6 +715,7 @@
entry.Content()
}
+ @SuppressLint("MutableCollectionMutableState")
composeTestRule.setContent {
currStateMap = remember { mutableStateOf(stateMap1) }
backStackState = remember { mutableStateOf(1) }
diff --git a/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt b/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
index f8cd97d..e9d8902 100644
--- a/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
+++ b/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
@@ -60,8 +60,8 @@
serializer: KSerializer<T> = UnsafePolymorphicSerializer()
) =
listSaver<List<T>, SavedState>(
- save = { list -> list.map { encodeToSavedState(serializer, it) } },
- restore = { list -> list.map { decodeFromSavedState(serializer, it) } },
+ save = { list -> list.fastMapOrMap { encodeToSavedState(serializer, it) } },
+ restore = { list -> list.fastMapOrMap { decodeFromSavedState(serializer, it) } },
)
@Suppress("UNCHECKED_CAST")
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
index 647a2b8..bf483dd 100644
--- a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
@@ -52,7 +52,7 @@
// to ensure our lambda below takes the correct type
entryProvider as (T) -> NavEntry<T>
val entries =
- backStack.map { key ->
+ backStack.fastMapOrMap { key ->
val entry = entryProvider.invoke(key)
decorateEntry(entry, entryDecorators)
}
@@ -94,7 +94,9 @@
// onDispose
// calls for clean up
// convert to mutableList first for backwards compat.
- latestDecorators.reversed().forEach { it.onPop(contentKey) }
+ latestDecorators.fastForEachReversedOrForEachReversed {
+ it.onPop(contentKey)
+ }
}
}
}
@@ -125,13 +127,13 @@
// if an entry has been popped
val latestEntries by rememberUpdatedState(entries)
val latestDecorators by rememberUpdatedState(decorators)
- entries.forEach {
+ entries.fastForEachOrForEach {
val contentKey = it.contentKey
contentKeys.add(contentKey)
DisposableEffect(contentKey) {
onDispose {
- val latestBackStack = latestEntries.map { it.contentKey }
+ val latestBackStack = latestEntries.fastMapOrMap { entry -> entry.contentKey }
val popped =
if (!latestBackStack.contains(contentKey)) {
contentKeys.remove(contentKey)
@@ -141,18 +143,17 @@
// we reverse the order before popping to imitate the order
// of onDispose calls if each scope/decorator had their own onDispose
// calls for clean up
- latestDecorators.reversed().forEach { it.onPop(contentKey) }
+ latestDecorators.fastForEachReversedOrForEachReversed { it.onPop(contentKey) }
}
}
}
}
- CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo) { content() }
+ CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo, content)
}
private class NavEntryDecoratorLocalInfo {
val contentKeys: MutableSet<Any> = mutableSetOf()
val idsInComposition: MutableSet<Any> = mutableSetOf()
- val popCallbacks: LinkedHashMap<Int, (key: Any) -> Unit> = LinkedHashMap()
}
private val LocalNavEntryDecoratorLocalInfo =
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt
new file mode 100644
index 0000000..4ffaf4c
--- /dev/null
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.navigation3.runtime
+
+import androidx.collection.MutableScatterSet
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+/** Internal util to use compose ui's fastX iterator apis whenever possible */
+internal inline fun <T, R> List<T>.fastMapOrMap(transform: (T) -> R) =
+ if (this is RandomAccess) {
+ this.fastMap(transform)
+ } else {
+ @Suppress("ListIterator") this.map(transform)
+ }
+
+internal inline fun <T> List<T>.fastForEachReversedOrForEachReversed(action: (T) -> Unit) {
+ if (this is RandomAccess) {
+ this.fastForEachReversed(action)
+ } else {
+ @Suppress("ListIterator") this.reversed().forEach(action)
+ }
+}
+
+internal inline fun <T> List<T>.fastForEachOrForEach(action: (T) -> Unit) {
+ if (this is RandomAccess) {
+ this.fastForEach(action)
+ } else {
+ @Suppress("ListIterator") this.forEach(action)
+ }
+}
+
+internal fun <T> List<T>.fastDistinctOrDistinct(): List<T> =
+ if (this is RandomAccess) {
+ this.fastDistinctBy { it }
+ } else {
+ @Suppress("ListIterator") this.distinct()
+ }
+
+/** Helpers copied from compose:ui:ui-util to prevent adding dep on ui-util */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+ contract { callsInPlace(action) }
+ for (index in indices) {
+ val item = get(index)
+ action(item)
+ }
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+ contract { callsInPlace(transform) }
+ val target = ArrayList<R>(size)
+ fastForEach { target += transform(it) }
+ return target
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.fastForEachReversed(action: (T) -> Unit) {
+ contract { callsInPlace(action) }
+ for (index in indices.reversed()) {
+ val item = get(index)
+ action(item)
+ }
+}
+
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+private inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
+ contract { callsInPlace(selector) }
+ val set = MutableScatterSet<K>(size)
+ val target = ArrayList<T>(size)
+ fastForEach { e ->
+ val key = selector(e)
+ if (set.add(key)) target += e
+ }
+ return target
+}
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
index 0aa542b..6e54a59 100644
--- a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
@@ -83,7 +83,7 @@
) {
@Suppress("UNCHECKED_CAST")
(entryDecorators as List<@JvmSuppressWildcards NavEntryDecorator<T>>)
- .distinct()
+ .fastDistinctOrDistinct()
.foldRight(initial = entry) { decorator, wrappedEntry ->
object : NavEntryWrapper<T>(wrappedEntry) {
@Composable
diff --git a/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt b/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
index 2a126db..27e810b 100644
--- a/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
+++ b/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
@@ -31,7 +31,7 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.kruth.assertThat
import androidx.navigation3.runtime.NavEntry
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEvent
import androidx.navigationevent.NavigationEventDispatcher
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -131,13 +131,13 @@
lateinit var numberOnScreen1: MutableState<Int>
lateinit var numberOnScreen2: MutableState<Int>
lateinit var navEventDispatcher: NavigationEventDispatcher
- lateinit var inputHandler: DirectNavigationEventInputHandler
+ lateinit var input: DirectNavigationEventInput
lateinit var backStack: MutableList<Any>
composeTestRule.setContent {
navEventDispatcher =
LocalNavigationEventDispatcherOwner.current!!.navigationEventDispatcher
- inputHandler = DirectNavigationEventInputHandler()
- navEventDispatcher.addInputHandler(inputHandler)
+ input = DirectNavigationEventInput()
+ navEventDispatcher.addInput(input)
backStack = remember { mutableStateListOf(first) }
NavDisplay(
backStack = backStack,
@@ -188,13 +188,13 @@
assertThat(composeTestRule.onNodeWithText("numberOnScreen2: 4").isDisplayed()).isTrue()
composeTestRule.runOnIdle {
- inputHandler.handleOnStarted(NavigationEvent(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
- inputHandler.handleOnProgressed(NavigationEvent(0.1F, 0.1F, 0.5F, BackEvent.EDGE_LEFT))
+ input.handleOnStarted(NavigationEvent(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+ input.handleOnProgressed(NavigationEvent(0.1F, 0.1F, 0.5F, BackEvent.EDGE_LEFT))
}
composeTestRule.waitForIdle()
- composeTestRule.runOnIdle { inputHandler.handleOnCompleted() }
+ composeTestRule.runOnIdle { input.handleOnCompleted() }
composeTestRule.runOnIdle {
assertWithMessage("The number should be restored")
diff --git a/navigationevent/navigationevent-compose/build.gradle b/navigationevent/navigationevent-compose/build.gradle
index 344f1be..0dc103e 100644
--- a/navigationevent/navigationevent-compose/build.gradle
+++ b/navigationevent/navigationevent-compose/build.gradle
@@ -24,6 +24,8 @@
import androidx.build.SoftwareType
import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
plugins {
id("AndroidXPlugin")
@@ -153,50 +155,27 @@
}
wasmJsMain {
- dependsOn(webMain)
dependencies {
- implementation(libs.kotlinStdlibWasm)
implementation(libs.kotlinXw3c)
}
}
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
- if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+ if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+ } else if (target.konanTarget.family == Family.LINUX) {
target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+ } else if (target.konanTarget.family == Family.MINGW) {
target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
} else {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
}
- } else if (target.platformType in [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.js, org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.wasm]) {
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
target.compilations["main"].defaultSourceSet.dependsOn(webMain)
target.compilations["test"].defaultSourceSet.dependsOn(webTest)
}
diff --git a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
index 6ff65d7..039623e 100644
--- a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
+++ b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEventDispatcherOwner
import androidx.navigationevent.testing.TestNavigationEventCallback
import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
@@ -58,9 +58,9 @@
}
childOwner.navigationEventDispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = DirectNavigationEventInput()
+ childOwner.navigationEventDispatcher.addInput(input)
+ input.handleOnCompleted()
// Verify that the child created its own, separate owner and dispatcher.
assertThat(childOwner).isNotEqualTo(parentOwner)
@@ -99,10 +99,8 @@
// Verify that attempting to use the disposed dispatcher now throws an
// IllegalStateException, preventing use-after-dispose bugs.
- val inputHandler = DirectNavigationEventInputHandler()
- assertThrows<IllegalStateException> {
- childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- }
+ val input = DirectNavigationEventInput()
+ assertThrows<IllegalStateException> { childOwner.navigationEventDispatcher.addInput(input) }
.hasMessageThat()
.contains("has already been disposed")
}
@@ -133,9 +131,9 @@
// Attempt to dispatch an event while the dispatcher is disabled.
childOwner.navigationEventDispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = DirectNavigationEventInput()
+ childOwner.navigationEventDispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(childOwner).isNotEqualTo(parentOwner)
assertThat(childOwner.navigationEventDispatcher.isEnabled).isFalse()
@@ -164,9 +162,9 @@
// Verify the root dispatcher can operate independently.
rootOwner.navigationEventDispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = DirectNavigationEventInput()
+ rootOwner.navigationEventDispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(rootOwner.navigationEventDispatcher.isEnabled).isTrue()
@@ -198,10 +196,8 @@
// Verify that using the disposed dispatcher throws the expected exception.
// This prevents use-after-dispose bugs.
- val inputHandler = DirectNavigationEventInputHandler()
- assertThrows<IllegalStateException> {
- rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- }
+ val input = DirectNavigationEventInput()
+ assertThrows<IllegalStateException> { rootOwner.navigationEventDispatcher.addInput(input) }
.hasMessageThat()
.contains("has already been disposed")
}
@@ -227,9 +223,9 @@
// Attempt to dispatch an event while disabled.
rootOwner.navigationEventDispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = DirectNavigationEventInput()
+ rootOwner.navigationEventDispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(rootOwner.navigationEventDispatcher.isEnabled).isFalse()
diff --git a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
index cd5d435..19f833b6 100644
--- a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
+++ b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.kruth.assertThat
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
import androidx.navigationevent.NavigationEvent
import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -47,8 +47,7 @@
private val owner = TestNavigationEventDispatcherOwner()
private val dispatcher = owner.navigationEventDispatcher
- private val inputHandler =
- DirectNavigationEventInputHandler().also { dispatcher.addInputHandler(it) }
+ private val input = DirectNavigationEventInput().also { dispatcher.addInput(it) }
@Test
fun navigationEventHandler_whenOnStartDispatched_invokesHandler() {
@@ -61,7 +60,7 @@
progress.collect()
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
Text(text = "backPress")
}
}
@@ -81,14 +80,14 @@
progress.collect()
counter++
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
Text(text = "backPress")
}
}
}
rule.onNodeWithText("backPress").performClick()
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
rule.runOnIdle { assertThat(counter).isEqualTo(1) }
}
@@ -105,8 +104,8 @@
}
Button(
onClick = {
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
}
) {
Text(text = "backPress")
@@ -140,8 +139,8 @@
// Phase 1: Test when enabled
// The handler should be called.
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
rule.runOnIdle {
assertThat(result).isEqualTo(listOf("onBack"))
assertThat(owner.fallbackOnBackPressedInvocations).isEqualTo(0)
@@ -151,8 +150,8 @@
// The fallback should be invoked instead of the handler.
enabled = false
rule.runOnIdle {
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
assertThat(result).isEqualTo(listOf("onBack")) // Unchanged
assertThat(owner.fallbackOnBackPressedInvocations).isEqualTo(1)
}
@@ -161,8 +160,8 @@
// The handler should work again.
enabled = true
rule.runOnIdle {
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
assertThat(result).isEqualTo(listOf("onBack", "onBack"))
}
}
@@ -201,12 +200,12 @@
// The 'enabled' check happens inside the callback. Disabling right before the
// dispatch means the handler will start but see that it's disabled.
count = 1
- inputHandler.handleOnStarted(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
// The launched effect for the handler might still run, but it should not prevent
// the gesture from completing normally.
rule.runOnIdle { assertThat(wasStartedWhenDisabled).isTrue() }
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
// It should not be cancelled because the gesture completes successfully.
@@ -231,13 +230,13 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
// Disable after the gesture has already started. The `enabled` check has already passed,
// so the gesture should continue to be handled.
count = 1
rule.runOnIdle { assertThat(wasStartedWhenDisabled).isFalse() }
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
}
@@ -259,10 +258,10 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
// The exception is thrown on completion because the handler's coroutine finishes
// prematurely without having suspended for the gesture's result.
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
rule.waitUntil(1000) { result.size >= 3 }
rule.runOnIdle { assertThat(result).isEqualTo(listOf("start", "async", "complete")) }
@@ -285,14 +284,14 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
rule.waitUntil { asyncStarted } // failing
// Start a new gesture. This should cancel the scope of the previous handler,
// including the async job it launched.
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
rule.waitUntil(1000) { result.size >= 3 }
rule.runOnIdle {
@@ -310,7 +309,7 @@
result += "parent"
progress.collect()
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
// When handlers are nested, only the deepest, last-composed handler is active.
NavigationEventHandler { progress ->
result += "child"
@@ -335,7 +334,7 @@
result += "parent"
progress.collect()
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
NavigationEventHandler(enabled = false) { progress ->
result += "child"
progress.collect()
@@ -363,7 +362,7 @@
result += "second"
progress.collect()
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
Text(text = "backPress")
}
}
@@ -387,7 +386,7 @@
result += "second"
progress.collect()
}
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
Text(text = "backPress")
}
}
@@ -411,7 +410,7 @@
// The key of the handler is its `onEvent` lambda. Changing it should
// correctly replace the old handler with the new one.
NavigationEventHandler(onEvent = handler)
- Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+ Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
Text(text = "backPress")
}
}
@@ -441,10 +440,10 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
rule.waitForIdle()
assertThat(result).isEqualTo(listOf(0, 1, 2))
@@ -470,9 +469,9 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnCancelled()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnCancelled()
rule.runOnIdle {
assertThat(result).isEqualTo(listOf("start", "progress", "navEvent cancelled"))
@@ -495,9 +494,9 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnCancelled()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnCancelled()
rule.runOnIdle { assertThat(result).isEqualTo(listOf("start", "progress")) }
}
@@ -520,9 +519,9 @@
}
// Simulate a cancelled gesture
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnCancelled()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnCancelled()
rule.runOnIdle {
assertThat(result).isEqualTo(listOf("progress", "navEvent cancelled"))
@@ -530,10 +529,10 @@
}
// The handler should reset and be ready for a new gesture.
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnProgressed(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnProgressed(NavigationEvent())
+ input.handleOnCompleted()
rule.runOnIdle { assertThat(result).isEqualTo(listOf("progress", "progress", "complete")) }
}
@@ -554,11 +553,11 @@
}
}
- inputHandler.handleOnStarted(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
rule.waitUntil { asyncStarted }
// Cancelling the gesture should cancel the handler's scope, which in turn
// cancels the async job.
- inputHandler.handleOnCancelled()
+ input.handleOnCancelled()
rule.runOnIdle {
runBlocking { delay(700) } // allow time for the cancelled async job to not complete
@@ -589,7 +588,7 @@
}
// 1. Start the back gesture. The handler's coroutine is now running.
- inputHandler.handleOnStarted(NavigationEvent())
+ input.handleOnStarted(NavigationEvent())
rule.runOnIdle { assertThat(result).isEqualTo(listOf("start")) }
// 2. Remove the handler from the composition.
@@ -600,7 +599,7 @@
// 3. Attempt to complete the original gesture.
// Since the handler was removed, it should not receive this event.
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
// The handler should have been cancelled when it was disposed.
// It should not have completed.
diff --git a/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt b/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
index fa45ce8..359f4c0 100644
--- a/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
+++ b/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.remember
import androidx.navigationevent.NavigationEventDispatcher
import androidx.navigationevent.NavigationEventDispatcherOwner
-import androidx.navigationevent.NavigationEventInputHandler
+import androidx.navigationevent.NavigationEventInput
/**
* Creates a new navigation scope by providing a [NavigationEventDispatcher] to descendant
@@ -37,8 +37,8 @@
* The dispatcher's lifecycle is automatically managed. It is created only once and automatically
* disposed of when the composable leaves the composition, preventing memory leaks.
*
- * When used to create a root dispatcher, you must use a [NavigationEventInputHandler] to send it
- * events. Otherwise, the dispatcher will be detached and will not receive events.
+ * When used to create a root dispatcher, you must use a [NavigationEventInput] to send it events.
+ * Otherwise, the dispatcher will be detached and will not receive events.
*
* **Null parent:** If [parent] is **EXPLICITLY** `null`, this creates a root dispatcher that runs
* independently. By default, it requires a parent from the [LocalNavigationEventDispatcherOwner]
diff --git a/navigationevent/navigationevent-testing/build.gradle b/navigationevent/navigationevent-testing/build.gradle
index 4b216f9..e4d05a3 100644
--- a/navigationevent/navigationevent-testing/build.gradle
+++ b/navigationevent/navigationevent-testing/build.gradle
@@ -53,20 +53,6 @@
api(project(":navigationevent:navigationevent"))
}
}
-
- wasmJsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- jsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
}
}
diff --git a/navigationevent/navigationevent/api/current.txt b/navigationevent/navigationevent/api/current.txt
index 25c7809..1570c8f 100644
--- a/navigationevent/navigationevent/api/current.txt
+++ b/navigationevent/navigationevent/api/current.txt
@@ -1,8 +1,8 @@
// Signature format: 4.0
package androidx.navigationevent {
- public final class DirectNavigationEventInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
- ctor public DirectNavigationEventInputHandler();
+ public final class DirectNavigationEventInput extends androidx.navigationevent.NavigationEventInput {
+ ctor public DirectNavigationEventInput();
method @MainThread public void handleOnCancelled();
method @MainThread public void handleOnCompleted();
method @MainThread public void handleOnProgressed(androidx.navigationevent.NavigationEvent event);
@@ -53,13 +53,13 @@
ctor public NavigationEventDispatcher(optional kotlin.jvm.functions.Function0<kotlin.Unit>? fallbackOnBackPressed);
method @KotlinOnly @MainThread public void addCallback(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object?> callback, optional androidx.navigationevent.NavigationEventPriority priority);
method @BytecodeOnly @MainThread public void addCallback-3owFMvg(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object!>, int);
- method @MainThread public void addInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+ method @MainThread public void addInput(androidx.navigationevent.NavigationEventInput input);
method @MainThread public void dispose();
method public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> getState();
method @KotlinOnly public inline <reified T extends androidx.navigationevent.NavigationEventInfo> kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<T>> getState(kotlinx.coroutines.CoroutineScope scope, T initialInfo);
method public boolean hasEnabledCallbacks();
method public boolean isEnabled();
- method @MainThread public void removeInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+ method @MainThread public void removeInput(androidx.navigationevent.NavigationEventInput input);
method public void setEnabled(boolean);
property public boolean isEnabled;
property public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> state;
@@ -77,15 +77,15 @@
field public static final androidx.navigationevent.NavigationEventInfo.NotProvided INSTANCE;
}
- public abstract class NavigationEventInputHandler {
- ctor public NavigationEventInputHandler();
+ public abstract class NavigationEventInput {
+ ctor public NavigationEventInput();
method @MainThread protected final void dispatchOnCancelled();
method @MainThread protected final void dispatchOnCompleted();
method @MainThread protected final void dispatchOnProgressed(androidx.navigationevent.NavigationEvent event);
method @MainThread protected final void dispatchOnStarted(androidx.navigationevent.NavigationEvent event);
- method @EmptySuper @MainThread protected void onAttach(androidx.navigationevent.NavigationEventDispatcher dispatcher);
- method @EmptySuper @MainThread protected void onDetach();
+ method @EmptySuper @MainThread protected void onAdded(androidx.navigationevent.NavigationEventDispatcher dispatcher);
method @EmptySuper @MainThread protected void onHasEnabledCallbacksChanged(boolean hasEnabledCallbacks);
+ method @EmptySuper @MainThread protected void onRemoved();
}
@kotlin.jvm.JvmInline public final value class NavigationEventPriority {
@@ -122,8 +122,8 @@
property public T? previousInfo;
}
- @RequiresApi(33) public final class OnBackInvokedInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
- ctor public OnBackInvokedInputHandler(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
+ @RequiresApi(33) public final class OnBackInvokedInput extends androidx.navigationevent.NavigationEventInput {
+ ctor public OnBackInvokedInput(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
}
public final class ViewTreeNavigationEventDispatcherOwner_androidKt {
diff --git a/navigationevent/navigationevent/api/restricted_current.txt b/navigationevent/navigationevent/api/restricted_current.txt
index 9054dba..eb72518 100644
--- a/navigationevent/navigationevent/api/restricted_current.txt
+++ b/navigationevent/navigationevent/api/restricted_current.txt
@@ -1,8 +1,8 @@
// Signature format: 4.0
package androidx.navigationevent {
- public final class DirectNavigationEventInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
- ctor public DirectNavigationEventInputHandler();
+ public final class DirectNavigationEventInput extends androidx.navigationevent.NavigationEventInput {
+ ctor public DirectNavigationEventInput();
method @MainThread public void handleOnCancelled();
method @MainThread public void handleOnCompleted();
method @MainThread public void handleOnProgressed(androidx.navigationevent.NavigationEvent event);
@@ -53,13 +53,13 @@
ctor public NavigationEventDispatcher(optional kotlin.jvm.functions.Function0<kotlin.Unit>? fallbackOnBackPressed);
method @KotlinOnly @MainThread public void addCallback(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object?> callback, optional androidx.navigationevent.NavigationEventPriority priority);
method @BytecodeOnly @MainThread public void addCallback-3owFMvg(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object!>, int);
- method @MainThread public void addInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+ method @MainThread public void addInput(androidx.navigationevent.NavigationEventInput input);
method @MainThread public void dispose();
method public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> getState();
method @KotlinOnly public inline <reified T extends androidx.navigationevent.NavigationEventInfo> kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<T>> getState(kotlinx.coroutines.CoroutineScope scope, T initialInfo);
method public boolean hasEnabledCallbacks();
method public boolean isEnabled();
- method @MainThread public void removeInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+ method @MainThread public void removeInput(androidx.navigationevent.NavigationEventInput input);
method public void setEnabled(boolean);
property public boolean isEnabled;
property public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> state;
@@ -77,15 +77,15 @@
field public static final androidx.navigationevent.NavigationEventInfo.NotProvided INSTANCE;
}
- public abstract class NavigationEventInputHandler {
- ctor public NavigationEventInputHandler();
+ public abstract class NavigationEventInput {
+ ctor public NavigationEventInput();
method @MainThread protected final void dispatchOnCancelled();
method @MainThread protected final void dispatchOnCompleted();
method @MainThread protected final void dispatchOnProgressed(androidx.navigationevent.NavigationEvent event);
method @MainThread protected final void dispatchOnStarted(androidx.navigationevent.NavigationEvent event);
- method @EmptySuper @MainThread protected void onAttach(androidx.navigationevent.NavigationEventDispatcher dispatcher);
- method @EmptySuper @MainThread protected void onDetach();
+ method @EmptySuper @MainThread protected void onAdded(androidx.navigationevent.NavigationEventDispatcher dispatcher);
method @EmptySuper @MainThread protected void onHasEnabledCallbacksChanged(boolean hasEnabledCallbacks);
+ method @EmptySuper @MainThread protected void onRemoved();
}
@kotlin.jvm.JvmInline public final value class NavigationEventPriority {
@@ -123,8 +123,8 @@
property public T? previousInfo;
}
- @RequiresApi(33) public final class OnBackInvokedInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
- ctor public OnBackInvokedInputHandler(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
+ @RequiresApi(33) public final class OnBackInvokedInput extends androidx.navigationevent.NavigationEventInput {
+ ctor public OnBackInvokedInput(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
}
public final class ViewTreeNavigationEventDispatcherOwner_androidKt {
diff --git a/navigationevent/navigationevent/bcv/native/current.txt b/navigationevent/navigationevent/bcv/native/current.txt
index 10829fb..728ea81 100644
--- a/navigationevent/navigationevent/bcv/native/current.txt
+++ b/navigationevent/navigationevent/bcv/native/current.txt
@@ -30,25 +30,25 @@
open fun onEventStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventCallback.onEventStarted|onEventStarted(androidx.navigationevent.NavigationEvent){}[0]
}
-abstract class androidx.navigationevent/NavigationEventInputHandler { // androidx.navigationevent/NavigationEventInputHandler|null[0]
- constructor <init>() // androidx.navigationevent/NavigationEventInputHandler.<init>|<init>(){}[0]
+abstract class androidx.navigationevent/NavigationEventInput { // androidx.navigationevent/NavigationEventInput|null[0]
+ constructor <init>() // androidx.navigationevent/NavigationEventInput.<init>|<init>(){}[0]
- final fun dispatchOnCancelled() // androidx.navigationevent/NavigationEventInputHandler.dispatchOnCancelled|dispatchOnCancelled(){}[0]
- final fun dispatchOnCompleted() // androidx.navigationevent/NavigationEventInputHandler.dispatchOnCompleted|dispatchOnCompleted(){}[0]
- final fun dispatchOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInputHandler.dispatchOnProgressed|dispatchOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
- final fun dispatchOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInputHandler.dispatchOnStarted|dispatchOnStarted(androidx.navigationevent.NavigationEvent){}[0]
- open fun onAttach(androidx.navigationevent/NavigationEventDispatcher) // androidx.navigationevent/NavigationEventInputHandler.onAttach|onAttach(androidx.navigationevent.NavigationEventDispatcher){}[0]
- open fun onDetach() // androidx.navigationevent/NavigationEventInputHandler.onDetach|onDetach(){}[0]
- open fun onHasEnabledCallbacksChanged(kotlin/Boolean) // androidx.navigationevent/NavigationEventInputHandler.onHasEnabledCallbacksChanged|onHasEnabledCallbacksChanged(kotlin.Boolean){}[0]
+ final fun dispatchOnCancelled() // androidx.navigationevent/NavigationEventInput.dispatchOnCancelled|dispatchOnCancelled(){}[0]
+ final fun dispatchOnCompleted() // androidx.navigationevent/NavigationEventInput.dispatchOnCompleted|dispatchOnCompleted(){}[0]
+ final fun dispatchOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInput.dispatchOnProgressed|dispatchOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
+ final fun dispatchOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInput.dispatchOnStarted|dispatchOnStarted(androidx.navigationevent.NavigationEvent){}[0]
+ open fun onAdded(androidx.navigationevent/NavigationEventDispatcher) // androidx.navigationevent/NavigationEventInput.onAdded|onAdded(androidx.navigationevent.NavigationEventDispatcher){}[0]
+ open fun onHasEnabledCallbacksChanged(kotlin/Boolean) // androidx.navigationevent/NavigationEventInput.onHasEnabledCallbacksChanged|onHasEnabledCallbacksChanged(kotlin.Boolean){}[0]
+ open fun onRemoved() // androidx.navigationevent/NavigationEventInput.onRemoved|onRemoved(){}[0]
}
-final class androidx.navigationevent/DirectNavigationEventInputHandler : androidx.navigationevent/NavigationEventInputHandler { // androidx.navigationevent/DirectNavigationEventInputHandler|null[0]
- constructor <init>() // androidx.navigationevent/DirectNavigationEventInputHandler.<init>|<init>(){}[0]
+final class androidx.navigationevent/DirectNavigationEventInput : androidx.navigationevent/NavigationEventInput { // androidx.navigationevent/DirectNavigationEventInput|null[0]
+ constructor <init>() // androidx.navigationevent/DirectNavigationEventInput.<init>|<init>(){}[0]
- final fun handleOnCancelled() // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnCancelled|handleOnCancelled(){}[0]
- final fun handleOnCompleted() // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnCompleted|handleOnCompleted(){}[0]
- final fun handleOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnProgressed|handleOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
- final fun handleOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnStarted|handleOnStarted(androidx.navigationevent.NavigationEvent){}[0]
+ final fun handleOnCancelled() // androidx.navigationevent/DirectNavigationEventInput.handleOnCancelled|handleOnCancelled(){}[0]
+ final fun handleOnCompleted() // androidx.navigationevent/DirectNavigationEventInput.handleOnCompleted|handleOnCompleted(){}[0]
+ final fun handleOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInput.handleOnProgressed|handleOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
+ final fun handleOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInput.handleOnStarted|handleOnStarted(androidx.navigationevent.NavigationEvent){}[0]
}
final class androidx.navigationevent/NavigationEvent { // androidx.navigationevent/NavigationEvent|null[0]
@@ -95,10 +95,10 @@
final fun <set-isEnabled>(kotlin/Boolean) // androidx.navigationevent/NavigationEventDispatcher.isEnabled.<set-isEnabled>|<set-isEnabled>(kotlin.Boolean){}[0]
final fun addCallback(androidx.navigationevent/NavigationEventCallback<*>, androidx.navigationevent/NavigationEventPriority = ...) // androidx.navigationevent/NavigationEventDispatcher.addCallback|addCallback(androidx.navigationevent.NavigationEventCallback<*>;androidx.navigationevent.NavigationEventPriority){}[0]
- final fun addInputHandler(androidx.navigationevent/NavigationEventInputHandler) // androidx.navigationevent/NavigationEventDispatcher.addInputHandler|addInputHandler(androidx.navigationevent.NavigationEventInputHandler){}[0]
+ final fun addInput(androidx.navigationevent/NavigationEventInput) // androidx.navigationevent/NavigationEventDispatcher.addInput|addInput(androidx.navigationevent.NavigationEventInput){}[0]
final fun dispose() // androidx.navigationevent/NavigationEventDispatcher.dispose|dispose(){}[0]
final fun hasEnabledCallbacks(): kotlin/Boolean // androidx.navigationevent/NavigationEventDispatcher.hasEnabledCallbacks|hasEnabledCallbacks(){}[0]
- final fun removeInputHandler(androidx.navigationevent/NavigationEventInputHandler) // androidx.navigationevent/NavigationEventDispatcher.removeInputHandler|removeInputHandler(androidx.navigationevent.NavigationEventInputHandler){}[0]
+ final fun removeInput(androidx.navigationevent/NavigationEventInput) // androidx.navigationevent/NavigationEventDispatcher.removeInput|removeInput(androidx.navigationevent.NavigationEventInput){}[0]
final inline fun <#A1: reified androidx.navigationevent/NavigationEventInfo> getState(kotlinx.coroutines/CoroutineScope, #A1): kotlinx.coroutines.flow/StateFlow<androidx.navigationevent/NavigationEventState<#A1>> // androidx.navigationevent/NavigationEventDispatcher.getState|getState(kotlinx.coroutines.CoroutineScope;0:0){0§<androidx.navigationevent.NavigationEventInfo>}[0]
}
diff --git a/navigationevent/navigationevent/build.gradle b/navigationevent/navigationevent/build.gradle
index 0eb1604..1c2ad5a 100644
--- a/navigationevent/navigationevent/build.gradle
+++ b/navigationevent/navigationevent/build.gradle
@@ -24,6 +24,8 @@
import androidx.build.PlatformIdentifier
import androidx.build.SoftwareType
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
plugins {
id("AndroidXPlugin")
@@ -171,50 +173,27 @@
}
wasmJsMain {
- dependsOn(webMain)
dependencies {
- implementation(libs.kotlinStdlibWasm)
implementation(libs.kotlinXw3c)
}
}
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
- if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+ if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+ } else if (target.konanTarget.family == Family.LINUX) {
target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+ } else if (target.konanTarget.family == Family.MINGW) {
target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
} else {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
}
- } else if (target.platformType in [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.js, org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.wasm]) {
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
target.compilations["main"].defaultSourceSet.dependsOn(webMain)
target.compilations["test"].defaultSourceSet.dependsOn(webTest)
}
diff --git a/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt b/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
similarity index 85%
rename from navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt
rename to navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
index b31d0c1..eca9cd6 100644
--- a/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt
+++ b/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
@@ -27,15 +27,15 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 33)
-class OnBackInvokedInputHandlerTest {
+class OnBackInvokedInputTest {
@Test
fun testSimpleInvoker() {
val invoker = TestOnBackInvokedDispatcher()
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback()
@@ -54,8 +54,8 @@
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback()
@@ -92,8 +92,8 @@
val callback = TestNavigationEventCallback(isEnabled = false)
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
dispatcher.addCallback(callback)
@@ -118,8 +118,8 @@
dispatcher.addCallback(callback)
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
assertThat(invoker.registerCount).isEqualTo(1)
@@ -135,8 +135,8 @@
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback()
@@ -165,8 +165,8 @@
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback()
@@ -189,8 +189,8 @@
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback(onEventStarted = { remove() })
@@ -211,8 +211,8 @@
val invoker = TestOnBackInvokedDispatcher()
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback = TestNavigationEventCallback()
@@ -235,8 +235,8 @@
val invoker = TestOnBackInvokedDispatcher()
val dispatcher = NavigationEventDispatcher {}
- val inputHandler = OnBackInvokedInputHandler(invoker)
- dispatcher.addInputHandler(inputHandler)
+ val input = OnBackInvokedInput(invoker)
+ dispatcher.addInput(input)
val callback1 = TestNavigationEventCallback()
diff --git a/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt b/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
index 1456757..740cd81 100644
--- a/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
+++ b/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
@@ -25,9 +25,8 @@
/** Provides input from OnBackInvokedCallback to the given [NavigationEventDispatcher]. */
@RequiresApi(33)
-public class OnBackInvokedInputHandler(
- private val onBackInvokedDispatcher: OnBackInvokedDispatcher
-) : NavigationEventInputHandler() {
+public class OnBackInvokedInput(private val onBackInvokedDispatcher: OnBackInvokedDispatcher) :
+ NavigationEventInput() {
private val onBackInvokedCallback: OnBackInvokedCallback =
if (Build.VERSION.SDK_INT == 33) {
OnBackInvokedCallback { dispatchOnCompleted() }
@@ -37,11 +36,11 @@
private var backInvokedCallbackRegistered = false
- override fun onAttach(dispatcher: NavigationEventDispatcher) {
+ override fun onAdded(dispatcher: NavigationEventDispatcher) {
updateBackInvokedCallbackState(dispatcher.hasEnabledCallbacks())
}
- override fun onDetach() {
+ override fun onRemoved() {
updateBackInvokedCallbackState(false)
}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
similarity index 80%
rename from navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt
rename to navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
index 610f1ce..521759b 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
@@ -18,8 +18,11 @@
import androidx.annotation.MainThread
-/** An input handler that can send events to a [NavigationEventDispatcher]. */
-public class DirectNavigationEventInputHandler() : NavigationEventInputHandler() {
+/**
+ * An input that can send events to a [NavigationEventDispatcher]. Instead of subclassing
+ * [NavigationEventInput], users can create instances of this class and use it directly.
+ */
+public class DirectNavigationEventInput() : NavigationEventInput() {
@MainThread
public fun handleOnStarted(event: NavigationEvent) {
dispatchOnStarted(event)
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
index a2eb072..b899fb4 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
@@ -27,7 +27,7 @@
*
* @param isEnabled The initial enabled state for this callback. Defaults to `true`.
* @see NavigationEventDispatcher
- * @see NavigationEventInputHandler
+ * @see NavigationEventInput
*/
public abstract class NavigationEventCallback<T : NavigationEventInfo>(isEnabled: Boolean = true) {
@@ -106,7 +106,7 @@
* @see onEventStarted
* @see NavigationEventDispatcher.dispatchOnStarted
*/
- internal fun doEventStarted(event: NavigationEvent) {
+ internal fun doOnEventStarted(event: NavigationEvent) {
onEventStarted(event)
}
@@ -126,7 +126,7 @@
* @see onEventProgressed
* @see NavigationEventDispatcher.dispatchOnProgressed
*/
- internal fun doEventProgressed(event: NavigationEvent) {
+ internal fun doOnEventProgressed(event: NavigationEvent) {
onEventProgressed(event)
}
@@ -146,7 +146,7 @@
* @see onEventCompleted
* @see NavigationEventDispatcher.dispatchOnCompleted
*/
- internal fun doEventCompleted() {
+ internal fun doOnEventCompleted() {
onEventCompleted()
}
@@ -164,7 +164,7 @@
* @see onEventCancelled
* @see NavigationEventDispatcher.dispatchOnCancelled
*/
- internal fun doEventCancelled() {
+ internal fun doOnEventCancelled() {
onEventCancelled()
}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
index b9d4d88..87bdd06 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
@@ -173,16 +173,14 @@
private val callbacks = mutableSetOf<NavigationEventCallback<*>>()
/**
- * A set of [NavigationEventInputHandler] instances that are directly managed by this
- * dispatcher.
+ * A set of [NavigationEventInput] instances that are directly managed by this dispatcher.
*
* This dispatcher controls the lifecycle of its registered handlers, calling
- * [NavigationEventInputHandler.doAttach] and [NavigationEventInputHandler.doDetach] as its own
- * state changes.
+ * [NavigationEventInput.onAdded] and [NavigationEventInput.onRemoved] as its own state changes.
*
* **This is primarily for cleanup when this dispatcher is no longer needed.**
*/
- private val inputs = mutableSetOf<NavigationEventInputHandler>()
+ private val inputs = mutableSetOf<NavigationEventInput>()
/**
* The [StateFlow] from the highest-priority, enabled navigation callback.
@@ -284,50 +282,56 @@
}
/**
- * Adds an input handler, registering it with the shared processor and binding it to this
- * dispatcher's lifecycle.
+ * Adds an input, registering it with the shared processor and binding it to this dispatcher's
+ * lifecycle.
*
- * The handler is registered globally with the [sharedProcessor] to receive system-wide state
+ * The input is registered globally with the [sharedProcessor] to receive system-wide state
* updates (e.g., whether any callbacks are enabled). It is also tracked locally by this
* dispatcher for lifecycle management.
*
- * The handler's [NavigationEventInputHandler.onAttach] method is invoked immediately upon
- * addition. It will be automatically detached when this dispatcher [dispose] is called.
+ * The input's [NavigationEventInput.onAdded] method is invoked immediately upon addition. It
+ * will be automatically detached when this dispatcher [dispose] is called.
*
- * @param inputHandler The handler to add.
- * @throws IllegalStateException if the dispatcher has already been disposed.
- * @see removeInputHandler
- * @see NavigationEventInputHandler.onDetach
+ * @param input The input to add.
+ * @throws IllegalStateException if the dispatcher has already been disposed or if [input] is
+ * already added to a dispatcher.
+ * @see removeInput
+ * @see NavigationEventInput.onRemoved
*/
@MainThread
- public fun addInputHandler(inputHandler: NavigationEventInputHandler) {
+ public fun addInput(input: NavigationEventInput) {
checkInvariants()
- if (inputs.add(inputHandler)) {
- sharedProcessor.inputs += inputHandler
- inputHandler.doAttach(dispatcher = this)
+ if (inputs.add(input)) {
+ check(input.dispatcher == null) {
+ "This input is already added to dispatcher ${input.dispatcher}."
+ }
+ sharedProcessor.inputs += input
+ input.dispatcher = this
+ input.doOnAdded(this)
}
}
/**
- * Removes and detaches an input handler from this dispatcher and the shared processor.
+ * Removes and detaches an input from this dispatcher and the shared processor.
*
- * This severs the handler's lifecycle link to the dispatcher. Its
- * [NavigationEventInputHandler.onDetach] method is invoked, and it will no longer receive
- * lifecycle calls or global state updates from the processor.
+ * This severs the input's lifecycle link to the dispatcher. Its
+ * [NavigationEventInput.onRemoved] method is invoked, and it will no longer receive lifecycle
+ * calls or global state updates from the processor.
*
- * @param inputHandler The handler to remove.
+ * @param input The input to remove.
* @throws IllegalStateException if the dispatcher has already been disposed.
- * @see addInputHandler
- * @see NavigationEventInputHandler.onAttach
+ * @see addInput
+ * @see NavigationEventInput.onAdded
*/
@MainThread
- public fun removeInputHandler(inputHandler: NavigationEventInputHandler) {
+ public fun removeInput(input: NavigationEventInput) {
checkInvariants()
- if (inputs.remove(inputHandler)) {
- sharedProcessor.inputs -= inputHandler
- inputHandler.doDetach()
+ if (inputs.remove(input)) {
+ sharedProcessor.inputs -= input
+ input.dispatcher = null
+ input.doOnRemoved()
}
}
@@ -335,80 +339,80 @@
* Dispatch an [NavigationEventCallback.onEventStarted] event with the given event. This call is
* delegated to the shared [NavigationEventProcessor].
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @param event [NavigationEvent] to dispatch to the callbacks.
* @throws IllegalStateException if the dispatcher has already been disposed.
*/
@MainThread
internal fun dispatchOnStarted(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
event: NavigationEvent,
) {
checkInvariants()
if (!isEnabled) return
- sharedProcessor.dispatchOnStarted(inputHandler, direction, event)
+ sharedProcessor.dispatchOnStarted(input, direction, event)
}
/**
* Dispatch an [NavigationEventCallback.onEventProgressed] event with the given event. This call
* is delegated to the shared [NavigationEventProcessor].
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @param event [NavigationEvent] to dispatch to the callbacks.
* @throws IllegalStateException if the dispatcher has already been disposed.
*/
@MainThread
internal fun dispatchOnProgressed(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
event: NavigationEvent,
) {
checkInvariants()
if (!isEnabled) return
- sharedProcessor.dispatchOnProgressed(inputHandler, direction, event)
+ sharedProcessor.dispatchOnProgressed(input, direction, event)
}
/**
* Dispatch an [NavigationEventCallback.onEventCompleted] event. This call is delegated to the
* shared [NavigationEventProcessor], passing the fallback action.
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @throws IllegalStateException if the dispatcher has already been disposed.
*/
@MainThread
internal fun dispatchOnCompleted(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
) {
checkInvariants()
if (!isEnabled) return
- sharedProcessor.dispatchOnCompleted(inputHandler, direction, fallbackOnBackPressed)
+ sharedProcessor.dispatchOnCompleted(input, direction, fallbackOnBackPressed)
}
/**
* Dispatch an [NavigationEventCallback.onEventCancelled] event. This call is delegated to the
* shared [NavigationEventProcessor].
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @throws IllegalStateException if the dispatcher has already been disposed.
*/
@MainThread
internal fun dispatchOnCancelled(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
) {
checkInvariants()
if (!isEnabled) return
- sharedProcessor.dispatchOnCancelled(inputHandler, direction)
+ sharedProcessor.dispatchOnCancelled(input, direction)
}
/**
@@ -422,9 +426,9 @@
* Calling this method triggers a comprehensive, iterative cleanup:
* 1. It iteratively processes and disposes of all child dispatchers and their descendants,
* ensuring a complete top-down cleanup of the entire sub-hierarchy without recursion.
- * 2. For each dispatcher, it first detaches all registered [NavigationEventInputHandler]
- * instances by calling [NavigationEventInputHandler.onDetach]. This severs their lifecycle
- * link to the dispatcher and allows them to release any tied resources.
+ * 2. For each dispatcher, it first detaches all registered [NavigationEventInput] instances by
+ * calling [NavigationEventInput.onRemoved]. This severs their lifecycle link to the
+ * dispatcher and allows them to release any tied resources.
* 3. It then removes all [NavigationEventCallback] instances registered with that dispatcher
* from the shared processor, preventing memory leaks.
* 4. Finally, it removes the dispatcher from its parent's list of children, fully dismantling
@@ -452,12 +456,13 @@
// own cleanup. This ensures a complete traversal of the sub-hierarchy.
dispatchersToDispose += currentDispatcher.childDispatchers
- // Notify all registered input handlers that this dispatcher is being disposed.
+ // Notify all registered inputs that this dispatcher is being disposed.
// This gives them a chance to clean up their own state, severing the lifecycle link
// and preventing them from interacting with a disposed object.
for (input in currentDispatcher.inputs) {
sharedProcessor.inputs -= input
- input.doDetach()
+ input.dispatcher = null
+ input.doOnRemoved()
}
inputs.clear()
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt
new file mode 100644
index 0000000..e3d6df2
--- /dev/null
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.navigationevent
+
+import androidx.annotation.EmptySuper
+import androidx.annotation.MainThread
+import androidx.navigationevent.NavigationEventDirection.Companion.Backward
+
+/** A class that can send events to a [NavigationEventDispatcher]. */
+public abstract class NavigationEventInput() {
+
+ /** The [NavigationEventDispatcher] that this input is connected to. */
+ internal var dispatcher: NavigationEventDispatcher? = null
+
+ @MainThread
+ internal fun doOnAdded(dispatcher: NavigationEventDispatcher) {
+ onAdded(dispatcher)
+ }
+
+ @MainThread
+ internal fun doOnRemoved() {
+ onRemoved()
+ }
+
+ /**
+ * Called after this [NavigationEventInput] is added to [dispatcher]. This can happen when
+ * calling [NavigationEventDispatcher.addInput]. A [NavigationEventInput] can only be added to
+ * one [NavigationEventDispatcher] at a time.
+ *
+ * @param dispatcher The [NavigationEventDispatcher] that this input is now added to.
+ */
+ @MainThread @EmptySuper protected open fun onAdded(dispatcher: NavigationEventDispatcher) {}
+
+ /**
+ * Called after this [NavigationEventInput] is removed from a [NavigationEventDispatcher]. This
+ * can happen when calling [NavigationEventDispatcher.removeInput] or
+ * [NavigationEventDispatcher.dispose] on the containing [NavigationEventDispatcher].
+ */
+ @MainThread @EmptySuper protected open fun onRemoved() {}
+
+ @MainThread
+ internal fun doHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
+ onHasEnabledCallbacksChanged(hasEnabledCallbacks)
+ }
+
+ /**
+ * Callback that will be notified when the connected dispatcher's `hasEnabledCallbacks` changes.
+ *
+ * @param hasEnabledCallbacks Whether the connected dispatcher has any enabled callbacks.
+ */
+ @MainThread
+ @EmptySuper
+ protected open fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {}
+
+ /**
+ * Call `dispatchOnStarted` on the connected dispatcher.
+ *
+ * @param event The event to dispatch.
+ */
+ @MainThread
+ protected fun dispatchOnStarted(event: NavigationEvent) {
+ // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+ dispatcher?.dispatchOnStarted(input = this, direction = Backward, event)
+ ?: error("This input is not added to any dispatcher.")
+ }
+
+ /**
+ * Call `dispatchOnProgressed` on the connected dispatcher.
+ *
+ * @param event The event to dispatch.
+ */
+ @MainThread
+ protected fun dispatchOnProgressed(event: NavigationEvent) {
+ // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+ dispatcher?.dispatchOnProgressed(input = this, direction = Backward, event)
+ ?: error("This input is not added to any dispatcher.")
+ }
+
+ /** Call `dispatchOnCancelled` on the connected dispatcher. */
+ @MainThread
+ protected fun dispatchOnCancelled() {
+ // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+ dispatcher?.dispatchOnCancelled(input = this, direction = Backward)
+ ?: error("This input is not added to any dispatcher.")
+ }
+
+ /** Call `dispatchOnCompleted` on the connected dispatcher. */
+ @MainThread
+ protected fun dispatchOnCompleted() {
+ // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+ dispatcher?.dispatchOnCompleted(input = this, direction = Backward)
+ ?: error("This input is not added to any dispatcher.")
+ }
+}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt
deleted file mode 100644
index 4cd2ffa..0000000
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.navigationevent
-
-import androidx.annotation.EmptySuper
-import androidx.annotation.MainThread
-import androidx.navigationevent.NavigationEventDirection.Companion.Backward
-
-/** An input handler that can send events to a [NavigationEventDispatcher]. */
-public abstract class NavigationEventInputHandler() {
-
- private var dispatcher: NavigationEventDispatcher? = null
-
- /**
- * Attaches this [NavigationEventInputHandler] to [dispatcher].
- *
- * @param dispatcher The [NavigationEventDispatcher] to attach to.
- * @throws IllegalStateException if it's already attached to a dispatcher.
- */
- @MainThread
- internal fun doAttach(dispatcher: NavigationEventDispatcher) {
- check(this.dispatcher == null) {
- "This input handler is already attached to dispatcher ${this.dispatcher}."
- }
- this.dispatcher = dispatcher
-
- onAttach(dispatcher)
- }
-
- /**
- * Detaches this [NavigationEventInputHandler] from the attached [NavigationEventDispatcher]. If
- * it's not attached to a dispatcher, this function does nothing.
- */
- @MainThread
- internal fun doDetach() {
- if (this.dispatcher == null) return
- this.dispatcher = null
- onDetach()
- }
-
- /**
- * Called after this [NavigationEventInputHandler] is attached to [dispatcher]. This can happen
- * when calling [NavigationEventDispatcher.addInputHandler]. A [NavigationEventInputHandler] can
- * only be attached to one [NavigationEventDispatcher] at a time.
- *
- * @param dispatcher The [NavigationEventDispatcher] that this input handler is now attached to.
- */
- @MainThread @EmptySuper protected open fun onAttach(dispatcher: NavigationEventDispatcher) {}
-
- /**
- * Called after this [NavigationEventInputHandler] is detached from a
- * [NavigationEventDispatcher]. This can happen when calling
- * [NavigationEventDispatcher.removeInputHandler] or [NavigationEventDispatcher.dispose] on the
- * attached [NavigationEventDispatcher].
- */
- @MainThread @EmptySuper protected open fun onDetach() {}
-
- @MainThread
- internal fun doHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
- onHasEnabledCallbacksChanged(hasEnabledCallbacks)
- }
-
- /**
- * Callback that will be notified when the connected dispatcher's `hasEnabledCallbacks` changes.
- *
- * @param hasEnabledCallbacks Whether the connected dispatcher has any enabled callbacks.
- */
- @MainThread
- @EmptySuper
- protected open fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {}
-
- /**
- * Call `dispatchOnStarted` on the connected dispatcher.
- *
- * @param event The event to dispatch.
- */
- @MainThread
- protected fun dispatchOnStarted(event: NavigationEvent) {
- // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
- dispatcher?.dispatchOnStarted(inputHandler = this, direction = Backward, event)
- ?: error("This input handler is not attached to a dispatcher.")
- }
-
- /**
- * Call `dispatchOnProgressed` on the connected dispatcher.
- *
- * @param event The event to dispatch.
- */
- @MainThread
- protected fun dispatchOnProgressed(event: NavigationEvent) {
- // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
- dispatcher?.dispatchOnProgressed(inputHandler = this, direction = Backward, event)
- ?: error("This input handler is not attached to a dispatcher.")
- }
-
- /** Call `dispatchOnCancelled` on the connected dispatcher. */
- @MainThread
- protected fun dispatchOnCancelled() {
- // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
- dispatcher?.dispatchOnCancelled(inputHandler = this, direction = Backward)
- ?: error("This input handler is not attached to a dispatcher.")
- }
-
- /** Call `dispatchOnCompleted` on the connected dispatcher. */
- @MainThread
- protected fun dispatchOnCompleted() {
- // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
- dispatcher?.dispatchOnCompleted(inputHandler = this, direction = Backward)
- ?: error("This input handler is not attached to a dispatcher.")
- }
-}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
index 4d2569a..c9e6d7b 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
@@ -83,7 +83,7 @@
private var inProgressCallback: NavigationEventCallback<*>? = null
/**
- * A central registry of all active [NavigationEventInputHandler] instances associated with this
+ * A central registry of all active [NavigationEventInput] instances associated with this
* processor.
*
* This set is managed by the [NavigationEventDispatcher] and allows the processor to
@@ -92,7 +92,7 @@
*
* It is not intended for direct public use and is exposed internally for the dispatcher.
*/
- val inputs = mutableSetOf<NavigationEventInputHandler>()
+ val inputs = mutableSetOf<NavigationEventInput>()
/**
* Represents whether there is at least one enabled callback registered across all dispatchers.
@@ -102,8 +102,7 @@
* `OnBackPressedDispatcher.setEnabled()`.
*
* It is updated automatically when callbacks are added, removed, or their enabled state
- * changes. When its value changes, it notifies all registered [NavigationEventInputHandler]
- * instances.
+ * changes. When its value changes, it notifies all registered [NavigationEventInput] instances.
*/
private var hasEnabledCallbacks: Boolean = false
set(value) {
@@ -232,7 +231,7 @@
// If the callback is the one currently being processed, it needs to be notified of
// cancellation and then cleared from the in-progress state.
if (callback == inProgressCallback) {
- callback.doEventCancelled()
+ callback.doOnEventCancelled()
inProgressCallback = null
}
@@ -255,23 +254,23 @@
* the new event. Only the single, highest-priority enabled callback is notified and becomes the
* `inProgressCallback`.
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @param event [NavigationEvent] to dispatch to the callback.
*/
@MainThread
fun dispatchOnStarted(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
event: NavigationEvent,
) {
- // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+ // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
// TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
if (inProgressCallback != null) {
// It's important to ensure that any ongoing operations from previous events are
// properly cancelled before starting new ones to maintain a consistent state.
- dispatchOnCancelled(inputHandler, direction)
+ dispatchOnCancelled(input, direction)
}
// Find the highest-priority enabled callback to handle this event.
@@ -281,7 +280,7 @@
// `onCancelled` can be correctly handled if the callback removes itself during
// `onEventStarted`.
inProgressCallback = callback
- callback.doEventStarted(event)
+ callback.doOnEventStarted(event)
_state.update {
InProgress(callback.currentInfo ?: NotProvided, callback.previousInfo, event)
}
@@ -295,17 +294,17 @@
* will be notified. Otherwise, the highest-priority enabled callback will receive the progress
* event. This is not a terminal event, so `inProgressCallback` is not cleared.
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @param event [NavigationEvent] to dispatch to the callback.
*/
@MainThread
fun dispatchOnProgressed(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
event: NavigationEvent,
) {
- // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+ // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
// TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
// If there is a callback in progress, only that one is notified.
@@ -314,7 +313,7 @@
// Progressed is not a terminal event, so `inProgressCallback` is not cleared.
if (callback != null) {
- callback.doEventProgressed(event)
+ callback.doOnEventProgressed(event)
_state.update {
InProgress(callback.currentInfo ?: NotProvided, callback.previousInfo, event)
}
@@ -329,17 +328,17 @@
* `inProgressCallback`. If no callback handles the event, the `fallbackOnBackPressed` action is
* invoked.
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
* @param fallbackOnBackPressed The action to invoke if no callback handles the completion.
*/
@MainThread
fun dispatchOnCompleted(
- inputHandler: NavigationEventInputHandler,
+ input: NavigationEventInput,
direction: NavigationEventDirection,
fallbackOnBackPressed: (() -> Unit)?,
) {
- // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+ // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
// TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
// If there is a callback in progress, only that one is notified.
@@ -351,7 +350,7 @@
if (callback == null) {
fallbackOnBackPressed?.invoke()
} else {
- callback.doEventCompleted()
+ callback.doOnEventCompleted()
_state.update { Idle(callback.currentInfo ?: NotProvided) }
}
}
@@ -363,15 +362,12 @@
* highest-priority enabled callback will be notified. This is a terminal event, clearing the
* `inProgressCallback`.
*
- * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+ * @param input The [NavigationEventInput] that sourced this event.
* @param direction The direction of the navigation event being started.
*/
@MainThread
- fun dispatchOnCancelled(
- inputHandler: NavigationEventInputHandler,
- direction: NavigationEventDirection,
- ) {
- // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+ fun dispatchOnCancelled(input: NavigationEventInput, direction: NavigationEventDirection) {
+ // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
// TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
// If there is a callback in progress, only that one is notified.
@@ -380,7 +376,7 @@
inProgressCallback = null // Clear in-progress, as 'cancelled' is a terminal event.
if (callback != null) {
- callback.doEventCancelled()
+ callback.doOnEventCancelled()
_state.update { Idle(callback.currentInfo ?: NotProvided) }
}
}
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt
deleted file mode 100644
index ceab18c..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * 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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package androidx.navigationevent
-
-import androidx.kruth.assertThat
-import androidx.navigationevent.NavigationEventInfo.NotProvided
-import androidx.navigationevent.NavigationEventState.Idle
-import androidx.navigationevent.NavigationEventState.InProgress
-import androidx.navigationevent.testing.TestNavigationEventCallback
-import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-
-class NavigationEventDispatcherStateTest {
-
- private val dispatcherOwner = TestNavigationEventDispatcherOwner()
- private val dispatcher = dispatcherOwner.navigationEventDispatcher
- private val inputHandler =
- DirectNavigationEventInputHandler().also { dispatcher.addInputHandler(it) }
-
- @Test
- fun state_whenMultipleCallbacksAreAdded_thenReflectsInfoFromLastAddedCallback() = runTest {
- val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
- val detailsCallback =
- TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
-
- assertThat(dispatcher.state.value).isEqualTo(Idle(NotProvided))
-
- dispatcher.addCallback(homeCallback)
- assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("home")))
-
- // Callbacks are prioritized like a stack (LIFO), so adding a new one makes it active.
- dispatcher.addCallback(detailsCallback)
- assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
- }
-
- @Test
- fun state_whenSetInfoIsCalledOnActiveCallback_thenStateIsUpdated() = runTest {
- val callback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("initial"))
- dispatcher.addCallback(callback)
-
- assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("initial")))
-
- // Calling setInfo on the active callback should immediately update the dispatcher's state.
- callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
-
- assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("updated")))
- }
-
- @Test
- fun state_whenSetInfoIsCalledOnInactiveCallback_thenStateIsUnchanged() = runTest {
- val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
- val detailsCallback =
- TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
- dispatcher.addCallback(homeCallback)
- dispatcher.addCallback(detailsCallback)
-
- // The state should reflect the last-added (active) callback.
- assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
-
- // Calling setInfo on an inactive callback should NOT affect the global state.
- // This confirms our logic in `onCallbackInfoChanged` is working correctly.
- homeCallback.setInfo(currentInfo = HomeScreenInfo("home-updated"), previousInfo = null)
-
- // The state should remain unchanged because the update came from a non-active callback.
- assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
- }
-
- @Test
- fun state_whenFullGestureLifecycleIsDispatched_thenTransitionsToInProgressAndBackToIdle() {
- val callbackInfo = HomeScreenInfo("home")
- val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
- dispatcher.addCallback(callback)
-
- val startEvent = NavigationEvent(touchX = 0.1F)
- val progressEvent = NavigationEvent(touchX = 0.3f)
-
- assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-
- // Starting a gesture should move the state to InProgress with the start event.
- inputHandler.handleOnStarted(startEvent)
- var state = dispatcher.state.value as InProgress
- assertThat(state.currentInfo).isEqualTo(callbackInfo)
- assertThat(state.previousInfo).isNull()
- assertThat(state.latestEvent).isEqualTo(startEvent)
-
- // Progressing the gesture should keep it InProgress but update to the latest event.
- inputHandler.handleOnProgressed(progressEvent)
- state = dispatcher.state.value as InProgress
- assertThat(state.latestEvent).isEqualTo(progressEvent)
-
- // Completing the gesture should return the state to Idle.
- inputHandler.handleOnCompleted()
- assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
- }
-
- @Test
- fun state_whenGestureIsCancelled_thenReturnsToIdleState() {
- val callbackInfo = HomeScreenInfo("home")
- val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
- dispatcher.addCallback(callback)
-
- val startEvent = NavigationEvent()
-
- assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-
- // Starting a gesture moves the state to InProgress.
- inputHandler.handleOnStarted(startEvent)
- assertThat(dispatcher.state.value).isEqualTo(InProgress(callbackInfo, null, startEvent))
-
- // Cancelling the gesture should also return the state to Idle.
- inputHandler.handleOnCancelled()
- assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
- }
-
- @Test
- fun inProgressState_whenInfoIsUpdatedDuringGesture_thenReflectsCorrectStateProperties() {
- val firstInfo = HomeScreenInfo("initial")
- val callback = TestNavigationEventCallback(currentInfo = firstInfo)
- dispatcher.addCallback(callback)
-
- val startEvent = NavigationEvent(touchX = 0.1F)
-
- // Start the gesture.
- inputHandler.handleOnStarted(startEvent)
-
- // At the start, previousInfo is null.
- var state = dispatcher.state.value as InProgress
- assertThat(state.currentInfo).isEqualTo(firstInfo)
- assertThat(state.previousInfo).isNull()
- assertThat(state.latestEvent).isEqualTo(startEvent)
-
- // Update the info mid-gesture. This triggers our updated `onCallbackInfoChanged` logic.
- val secondInfo = HomeScreenInfo("updated")
- callback.setInfo(currentInfo = secondInfo, previousInfo = firstInfo)
-
- // The state should now reflect the updated info. The `previousInfo` is now captured.
- state = dispatcher.state.value as InProgress
- assertThat(state.currentInfo).isEqualTo(secondInfo)
- assertThat(state.previousInfo).isEqualTo(firstInfo)
- assertThat(state.latestEvent).isEqualTo(startEvent) // Event hasn't changed yet.
-
- // Complete the gesture.
- inputHandler.handleOnCompleted()
- assertThat(dispatcher.state.value).isEqualTo(Idle(secondInfo))
- }
-
- @Test
- fun inProgressState_whenNewGestureStartsAfterAnotherCompletes_thenPreviousInfoIsNotStale() {
- val initialInfo = HomeScreenInfo("initial")
- val callback = TestNavigationEventCallback(currentInfo = initialInfo)
- dispatcher.addCallback(callback)
-
- // FIRST GESTURE: Create a complex state.
- inputHandler.handleOnStarted(NavigationEvent(touchX = 0.1f))
- callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
- inputHandler.handleOnCompleted()
-
- // After the first gesture, the final state is Idle with the updated info.
- val finalInfo = HomeScreenInfo("updated")
- assertThat(dispatcher.state.value).isEqualTo(Idle(finalInfo))
-
- // SECOND GESTURE: Verify that previousInfo was cleared by `clearPreviousInfo()`.
- val event2 = NavigationEvent(touchX = 0.3f)
- inputHandler.handleOnStarted(event2)
-
- // When a new gesture starts, `previousInfo` should be null, not stale data.
- val state = dispatcher.state.value as InProgress
- assertThat(state.currentInfo).isEqualTo(finalInfo)
- assertThat(state.previousInfo).isNull()
- assertThat(state.latestEvent).isEqualTo(event2)
- }
-
- @Test
- fun state_whenActiveDispatcherIsDisabled_fallsBackToSiblingDispatcherCallback() {
- // Create two sibling dispatchers sharing the same owner and processor.
- val childDispatcher = NavigationEventDispatcher(parentDispatcher = dispatcher)
-
- val callbackA = TestNavigationEventCallback(currentInfo = HomeScreenInfo("A"))
- dispatcher.addCallback(callbackA)
- assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
-
- val callbackB = TestNavigationEventCallback(currentInfo = DetailsScreenInfo("B"))
- childDispatcher.addCallback(callbackB)
- // Assert that state reflects callbackB, which was added last and is now active.
- assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
-
- // Disable the dispatcher that hosts the currently active callback.
- childDispatcher.isEnabled = false
-
- // The processor should now ignore callbackB and the state should
- // fall back to the next-highest priority callback, which is callbackA.
- assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
-
- // Re-enable the dispatcher.
- childDispatcher.isEnabled = true
-
- // The state should once again reflect callbackB, as it's now enabled
- // and has higher priority (due to being added last).
- assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
- }
-
- @Test
- fun getState_whenFilteredForSpecificType_onlyEmitsMatchingStates() =
- runTest(UnconfinedTestDispatcher()) {
- val initialHomeInfo = HomeScreenInfo("initial")
- val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
- val detailsCallback =
- TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
- val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
-
- dispatcher
- .getState(backgroundScope, initialHomeInfo)
- .onEach { collectedStates.add(it) }
- .launchIn(backgroundScope)
- advanceUntilIdle()
-
- // The flow must start with the initial value provided.
- assertThat(collectedStates).hasSize(1)
- assertThat(collectedStates.last()).isEqualTo(Idle(initialHomeInfo))
-
- // A new state with a matching type should be collected.
- dispatcher.addCallback(homeCallback)
- advanceUntilIdle()
- assertThat(collectedStates).hasSize(2)
- assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
-
- // A state with a non-matching type should be filtered out and not collected.
- dispatcher.addCallback(detailsCallback)
- advanceUntilIdle()
- assertThat(collectedStates).hasSize(2)
-
- // When the active callback is removed, since a non-matching type should be filtered out
- // and not collected.
- detailsCallback.remove()
- advanceUntilIdle()
- assertThat(collectedStates).hasSize(2)
- assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
- }
-
- @Test
- fun getState_whenTypeDoesNotMatch_emitsOnlyInitialInfo() =
- runTest(UnconfinedTestDispatcher()) {
- val initialHomeInfo = HomeScreenInfo("initial")
- val detailsCallback =
- TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
- val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
-
- dispatcher
- .getState(backgroundScope, initialHomeInfo)
- .onEach { collectedStates.add(it) }
- .launchIn(backgroundScope)
- advanceUntilIdle()
-
- // The flow must start with its initial value.
- assertThat(collectedStates).hasSize(1)
- assertThat(collectedStates.first()).isEqualTo(Idle(initialHomeInfo))
-
- // Add a callback with a non-matching type.
- dispatcher.addCallback(detailsCallback)
- advanceUntilIdle()
-
- // The collector should not have emitted a new value.
- assertThat(collectedStates).hasSize(1)
-
- // Update the non-matching callback's info.
- detailsCallback.setInfo(
- currentInfo = DetailsScreenInfo("details-updated"),
- previousInfo = null,
- )
- advanceUntilIdle()
-
- // The collector should still not have emitted a new value.
- assertThat(collectedStates).hasSize(1)
- }
-
- @Test
- fun progress_whenIdleOrInProgress_returnsCorrectValue() {
- val callbackInfo = HomeScreenInfo("home")
- val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
- dispatcher.addCallback(callback)
-
- // Before any gesture, the state is Idle and progress should be 0.
- assertThat(dispatcher.state.value.progress).isEqualTo(0f)
-
- // Start a gesture.
- inputHandler.handleOnStarted(NavigationEvent(progress = 0.1f))
- assertThat(dispatcher.state.value.progress).isEqualTo(0.1f)
-
- // InProgress state should reflect the event's progress.
- inputHandler.handleOnProgressed(NavigationEvent(progress = 0.5f))
- assertThat(dispatcher.state.value.progress).isEqualTo(0.5f)
-
- // Complete the gesture.
- inputHandler.handleOnCompleted()
-
- // After the gesture, the state is Idle again and progress should be 0.
- assertThat(dispatcher.state.value.progress).isEqualTo(0f)
- }
-}
-
-/** A sealed interface for type-safe navigation information. */
-sealed interface TestInfo : NavigationEventInfo
-
-data class HomeScreenInfo(val id: String) : TestInfo
-
-data class DetailsScreenInfo(val id: String) : TestInfo
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
index 386ab96..83e4da8 100644
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
+++ b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
@@ -13,26 +13,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
package androidx.navigationevent
+import androidx.annotation.MainThread
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
-import androidx.navigationevent.internal.TestNavigationEventInput
+import androidx.navigationevent.NavigationEventInfo.NotProvided
+import androidx.navigationevent.NavigationEventState.Idle
+import androidx.navigationevent.NavigationEventState.InProgress
import androidx.navigationevent.testing.TestNavigationEventCallback
import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
class NavigationEventDispatcherTest {
+ // region Core API
+
@Test
- fun dispatch_onStarted_thenOnStartedIsSent() {
+ fun dispatch_onStarted_sendsEventToCallback() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
assertThat(callback.startedInvocations).isEqualTo(1)
assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -41,14 +53,14 @@
}
@Test
- fun dispatch_onProgressed_thenOnProgressedIsSent() {
+ fun dispatch_onProgressed_sendsEventToCallback() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnProgressed(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnProgressed(NavigationEvent())
assertThat(callback.startedInvocations).isEqualTo(0)
assertThat(callback.progressedInvocations).isEqualTo(1)
@@ -57,14 +69,14 @@
}
@Test
- fun dispatch_onCompleted_theOnCompletedIsSent() {
+ fun dispatch_onCompleted_sendsEventToCallback() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(callback.startedInvocations).isEqualTo(0)
assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -73,14 +85,14 @@
}
@Test
- fun dispatch_onCancelled_theOnCancelledIsSent() {
+ fun dispatch_onCancelled_sendsEventToCallback() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCancelled()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCancelled()
assertThat(callback.startedInvocations).isEqualTo(0)
assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -89,26 +101,24 @@
}
@Test
- fun removeCallback_whenNavigationIsInProgress_thenOnCancelledIsSent() {
+ fun removeCallback_duringInProgressNavigation_sendsCancellation() {
val dispatcher = NavigationEventDispatcher()
// We need to capture the state when onEventCancelled is called to verify the order.
var startedInvocationsAtCancelTime = 0
val callback =
TestNavigationEventCallback(
- onEventCancelled = {
- // Capture the count of 'started' invocations when 'cancelled' is called.
- startedInvocationsAtCancelTime = this.startedInvocations
- }
+ onEventCancelled = { startedInvocationsAtCancelTime = this.startedInvocations }
)
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
- // Sanity check that navigation has started.
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
assertThat(callback.startedInvocations).isEqualTo(1)
+ // Removing a callback that is handling an in-progress navigation
+ // must trigger a cancellation event on that callback first.
callback.remove()
// Assert that onEventCancelled was called once, and it happened after onEventStarted.
@@ -117,15 +127,15 @@
}
@Test
- fun dispatch_whenCallbackDisablesItself_thenOnCancelledIsNotSent() {
+ fun dispatch_callbackDisablesItself_doesNotSendCancellation() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback(onEventStarted = { isEnabled = false })
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
// The callback was disabled, but cancellation should not be triggered.
// The 'completed' event should still be received because the navigation was in progress.
@@ -135,16 +145,18 @@
}
@Test
- fun setEnabled_whenNavigationIsInProgress_thenOnCancelledIsNotSent() {
+ fun setEnabled_duringInProgressNavigation_doesNotSendCancellation() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
assertThat(callback.startedInvocations).isEqualTo(1)
+ // Disabling a callback should not automatically cancel an in-progress navigation.
+ // This allows UI to be disabled without disrupting an ongoing user action.
callback.isEnabled = false
// Assert that disabling the callback does not trigger a cancellation.
@@ -152,22 +164,21 @@
}
@Test
- fun dispatch_whenCallbackRemovesItselfOnStarted_thenOnCancelledIsSent() {
+ fun dispatch_callbackRemovesItselfOnStarted_sendsCancellation() {
val dispatcher = NavigationEventDispatcher()
var cancelledInvocationsAtStartTime = 0
val callback =
TestNavigationEventCallback(
onEventStarted = {
- // Capture the 'cancelled' count before removing to ensure it was 0.
cancelledInvocationsAtStartTime = this.cancelledInvocations
remove()
}
)
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
// Assert that 'onEventStarted' was called.
assertThat(callback.startedInvocations).isEqualTo(1)
@@ -178,93 +189,91 @@
}
@Test
- fun dispatch_whenAnotherNavigationIsInProgress_thenPreviousIsCancelled() {
+ fun dispatch_newNavigationDuringExisting_cancelsPrevious() {
val dispatcher = NavigationEventDispatcher()
val callback1 = TestNavigationEventCallback()
dispatcher.addCallback(callback1)
- // Start the first navigation.
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
assertThat(callback1.startedInvocations).isEqualTo(1)
val callback2 = TestNavigationEventCallback()
dispatcher.addCallback(callback2)
- // Start the second navigation, which should cancel the first.
- inputHandler.handleOnStarted(NavigationEvent())
+ // Starting a new navigation must implicitly cancel any gesture already in progress
+ // to ensure a predictable state.
+ input.handleOnStarted(NavigationEvent())
- // Assert callback1 was cancelled and callback2 was started.
assertThat(callback1.cancelledInvocations).isEqualTo(1)
assertThat(callback2.startedInvocations).isEqualTo(1)
- // Complete the second navigation.
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
assertThat(callback2.completedInvocations).isEqualTo(1)
- // Ensure callback1 was not affected by the completion of the second navigation.
+ // Verify the cancelled callback receives no further events.
assertThat(callback1.completedInvocations).isEqualTo(0)
}
@Test
- fun addCallback_whenNavigationIsInProgress_thenNewCallbackIsIgnoredForCurrentNavigation() {
+ fun addCallback_duringInProgressNavigation_ignoresNewCallbackForCurrentEvent() {
val dispatcher = NavigationEventDispatcher()
val callback1 = TestNavigationEventCallback()
dispatcher.addCallback(callback1)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
assertThat(callback1.startedInvocations).isEqualTo(1)
- // Add a second callback while the first navigation is in progress.
+ // Add a new callback while a navigation is active.
val callback2 = TestNavigationEventCallback()
dispatcher.addCallback(callback2)
- // Complete the first navigation.
- inputHandler.handleOnCompleted()
+ // The dispatcher should be locked to the callback that started the navigation.
+ // The new callback should not receive the completion event for the current navigation.
+ input.handleOnCompleted()
- // Assert that only the first callback was affected.
assertThat(callback1.completedInvocations).isEqualTo(1)
assertThat(callback2.startedInvocations).isEqualTo(0)
assertThat(callback2.completedInvocations).isEqualTo(0)
// Start and complete a second navigation.
- inputHandler.handleOnStarted(NavigationEvent())
- inputHandler.handleOnCompleted()
+ input.handleOnStarted(NavigationEvent())
+ input.handleOnCompleted()
- // Assert that the second navigation was handled by the new top callback (callback2).
- assertThat(callback1.startedInvocations).isEqualTo(1) // Unchanged
- assertThat(callback1.completedInvocations).isEqualTo(1) // Unchanged
+ // The second navigation should be handled by the new top callback (callback2).
+ assertThat(callback1.startedInvocations).isEqualTo(1)
+ assertThat(callback1.completedInvocations).isEqualTo(1)
assertThat(callback2.startedInvocations).isEqualTo(1)
assertThat(callback2.completedInvocations).isEqualTo(1)
}
@Test
- fun dispatch_whenNoEnabledCallbacksExist_thenFallbackIsInvoked() {
+ fun dispatch_withNoEnabledCallbacks_invokesFallback() {
var fallbackCalled = false
val dispatcher =
NavigationEventDispatcher(fallbackOnBackPressed = { fallbackCalled = true })
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(callback.completedInvocations).isEqualTo(1)
assertThat(fallbackCalled).isFalse()
- // After disabling the only callback, the fallback should be called.
+ // After disabling the only callback, the fallback should be triggered.
callback.isEnabled = false
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
assertThat(callback.completedInvocations).isEqualTo(1) // Unchanged
assertThat(fallbackCalled).isTrue()
}
@Test
- fun dispatch_whenOverlayCallbackExists_thenOverlaySupersedesDefault() {
+ fun dispatch_withOverlayCallback_prioritizesOverlay() {
val dispatcher = NavigationEventDispatcher()
val overlayCallback = TestNavigationEventCallback()
val normalCallback = TestNavigationEventCallback()
@@ -272,9 +281,9 @@
dispatcher.addCallback(overlayCallback, NavigationEventPriority.Overlay)
dispatcher.addCallback(normalCallback, NavigationEventPriority.Default)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
// The overlay callback should handle the event, and the normal one should not.
assertThat(overlayCallback.completedInvocations).isEqualTo(1)
@@ -282,7 +291,7 @@
}
@Test
- fun dispatch_whenDisabledOverlayCallbackExists_thenDefaultCallbackIsInvoked() {
+ fun dispatch_withDisabledOverlay_invokesDefaultCallback() {
val dispatcher = NavigationEventDispatcher()
val overlayCallback = TestNavigationEventCallback()
val normalCallback = TestNavigationEventCallback()
@@ -293,9 +302,9 @@
// The highest priority callback is disabled.
overlayCallback.isEnabled = false
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
// The event should skip the disabled overlay and be handled by the default.
assertThat(overlayCallback.completedInvocations).isEqualTo(0)
@@ -303,11 +312,13 @@
}
@Test
- fun addCallback_whenCallbackIsRegisteredWithAnotherDispatcher_thenThrowsException() {
+ fun addCallback_toSecondDispatcher_throwsException() {
val callback = TestNavigationEventCallback()
val dispatcher1 = NavigationEventDispatcher()
dispatcher1.addCallback(callback)
+ // A callback cannot be registered to more than one dispatcher at a time
+ // to prevent ambiguous state and ownership issues.
val dispatcher2 = NavigationEventDispatcher()
assertThrows<IllegalArgumentException> { dispatcher2.addCallback(callback) }
.hasMessageThat()
@@ -315,18 +326,19 @@
}
@Test
- fun addCallback_whenCallbackIsAlreadyRegistered_thenThrowsException() {
+ fun addCallback_withAlreadyRegisteredCallback_throwsException() {
val callback = TestNavigationEventCallback()
val dispatcher = NavigationEventDispatcher()
dispatcher.addCallback(callback)
+ // Adding the same callback instance twice is a developer error and should fail fast.
assertThrows<IllegalArgumentException> { dispatcher.addCallback(callback) }
.hasMessageThat()
.contains("is already registered with a dispatcher")
}
@Test
- fun addCallback_whenMultipleOverlayCallbacksExist_thenLastAddedIsInvoked() {
+ fun addCallback_multipleOverlays_prioritizesLastAdded() {
val dispatcher = NavigationEventDispatcher()
val firstOverlayCallback = TestNavigationEventCallback()
val secondOverlayCallback = TestNavigationEventCallback()
@@ -336,9 +348,9 @@
dispatcher.addCallback(firstOverlayCallback, NavigationEventPriority.Overlay)
dispatcher.addCallback(secondOverlayCallback, NavigationEventPriority.Overlay)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
// Only the last-added overlay callback should handle the event.
assertThat(secondOverlayCallback.completedInvocations).isEqualTo(1)
@@ -347,53 +359,54 @@
}
@Test
- fun dispatch_whenNoCallbacksExist_thenFallbackIsInvoked() {
+ fun dispatch_withNoCallbacks_invokesFallback() {
var fallbackCalled = false
val dispatcher =
NavigationEventDispatcher(fallbackOnBackPressed = { fallbackCalled = true })
// With no callbacks registered at all, the fallback should still work.
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(fallbackCalled).isTrue()
}
@Test
- fun setEnabled_whenDisabledCallbackIsReEnabled_thenItReceivesEvents() {
+ fun setEnabled_onDisabledCallback_reenablesEventReceiving() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
// Disable the callback and confirm it doesn't receive an event.
callback.isEnabled = false
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(callback.completedInvocations).isEqualTo(0)
// Re-enable the callback.
callback.isEnabled = true
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
// It should now receive the event.
assertThat(callback.completedInvocations).isEqualTo(1)
}
@Test
- fun dispatch_whenNoNavigationIsInProgress_thenDispatchesToTopCallback() {
+ fun dispatch_withoutStart_sendsToTopCallback() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
// Dispatching progress or completed without a start should still notify the top callback.
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnProgressed(NavigationEvent())
+ // This handles simple, non-gesture back events.
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnProgressed(NavigationEvent())
assertThat(callback.progressedInvocations).isEqualTo(1)
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
assertThat(callback.completedInvocations).isEqualTo(1)
// Ensure no cancellation was ever triggered.
@@ -401,167 +414,1108 @@
}
@Test
- fun addCallback_whenCallbackIsRemovedAndReAdded_thenBehavesAsNew() {
+ fun addCallback_removedAndReadded_actsAsNew() {
val dispatcher = NavigationEventDispatcher()
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
assertThat(callback.completedInvocations).isEqualTo(1)
// Remove the callback.
callback.remove()
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
// Invocations should not increase.
assertThat(callback.completedInvocations).isEqualTo(1)
- // Re-add the same callback instance. It should be treated as a new callback.
+ // Re-adding the same callback instance should treat it as a new registration.
dispatcher.addCallback(callback)
- inputHandler.handleOnCompleted()
+ input.handleOnCompleted()
// Invocations should increase again.
assertThat(callback.completedInvocations).isEqualTo(2)
}
@Test
- fun addInputHandler_whenAdded_onAttachIsCalled() {
+ fun addInput_onAdd_callsOnAttach() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
+ val input = TestNavigationEventInput()
- dispatcher.addInputHandler(handler)
+ dispatcher.addInput(input)
- assertThat(handler.addedInvocations).isEqualTo(1)
- assertThat(handler.currentDispatcher).isEqualTo(dispatcher)
+ assertThat(input.addedInvocations).isEqualTo(1)
+ assertThat(input.currentDispatcher).isEqualTo(dispatcher)
}
@Test
- fun addInputHandler_whenAddedTwice_onAttachIsCalledOnce() {
+ fun addInput_twice_callsOnAttachOnce() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
+ val input = TestNavigationEventInput()
- dispatcher.addInputHandler(handler)
- dispatcher.addInputHandler(handler)
+ dispatcher.addInput(input)
+ // Adding the same input again should be idempotent and not re-trigger onAdded.
+ dispatcher.addInput(input)
- assertThat(handler.addedInvocations).isEqualTo(1)
+ assertThat(input.addedInvocations).isEqualTo(1)
}
@Test
- fun removeInputHandler_whenRemoved_onDetachIsCalled() {
+ fun removeInput_onRemove_callsOnDetach() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
- dispatcher.addInputHandler(handler)
- assertThat(handler.addedInvocations).isEqualTo(1)
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ assertThat(input.addedInvocations).isEqualTo(1)
- dispatcher.removeInputHandler(handler)
+ dispatcher.removeInput(input)
- assertThat(handler.removedInvocations).isEqualTo(1)
- assertThat(handler.currentDispatcher).isNull()
+ assertThat(input.removedInvocations).isEqualTo(1)
+ assertThat(input.currentDispatcher).isNull()
}
@Test
- fun removeInputHandler_whenNotRegistered_onDetachIsNotCalled() {
+ fun removeInput_whenNotRegistered_doesNotCallOnDetach() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
+ val input = TestNavigationEventInput()
- // Try to remove a handler that was never added.
- dispatcher.removeInputHandler(handler)
+ // Try to remove an input that was never added.
+ dispatcher.removeInput(input)
- assertThat(handler.removedInvocations).isEqualTo(0)
+ assertThat(input.removedInvocations).isEqualTo(0)
}
@Test
- fun dispose_detachesInputHandlers() {
+ fun dispose_onCall_detachesInputs() {
val dispatcher = NavigationEventDispatcher()
- val handler1 = TestNavigationEventInput()
- val handler2 = TestNavigationEventInput()
- dispatcher.addInputHandler(handler1)
- dispatcher.addInputHandler(handler2)
+ val input1 = TestNavigationEventInput()
+ val input2 = TestNavigationEventInput()
+ dispatcher.addInput(input1)
+ dispatcher.addInput(input2)
dispatcher.dispose()
- assertThat(handler1.removedInvocations).isEqualTo(1)
- assertThat(handler2.removedInvocations).isEqualTo(1)
- assertThat(handler1.currentDispatcher).isNull()
- assertThat(handler2.currentDispatcher).isNull()
+ assertThat(input1.removedInvocations).isEqualTo(1)
+ assertThat(input2.removedInvocations).isEqualTo(1)
+ assertThat(input1.currentDispatcher).isNull()
+ assertThat(input2.currentDispatcher).isNull()
}
@Test
- fun addInputHandler_onDisposedDispatcher_throwsException() {
+ fun addInput_onDisposedDispatcher_throws() {
val dispatcher = NavigationEventDispatcher()
dispatcher.dispose()
- val handler = TestNavigationEventInput()
- assertThrows<IllegalStateException> { dispatcher.addInputHandler(handler) }
+ val input = TestNavigationEventInput()
+ assertThrows<IllegalStateException> { dispatcher.addInput(input) }
.hasMessageThat()
.contains("This NavigationEventDispatcher has already been disposed")
}
@Test
- fun removeInputHandler_onDisposedDispatcher_throwsException() {
+ fun removeInput_onDisposedDispatcher_throws() {
val dispatcher = NavigationEventDispatcher()
dispatcher.dispose()
- val handler = TestNavigationEventInput()
- assertThrows<IllegalStateException> { dispatcher.removeInputHandler(handler) }
+ val input = TestNavigationEventInput()
+ assertThrows<IllegalStateException> { dispatcher.removeInput(input) }
.hasMessageThat()
.contains("This NavigationEventDispatcher has already been disposed")
}
@Test
- fun onHasEnabledCallbacksChanged_whenHandlerIsRegistered_callbackIsFired() {
+ fun onHasEnabledCallbacksChanged_onCallbackChange_notifiesInput() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
- dispatcher.addInputHandler(handler)
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
- assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(0)
+ assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(0)
- // Add a callback to trigger the onHasEnabledCallbacksChanged listener
+ // Adding a callback should trigger the onHasEnabledCallbacksChanged listener.
val callback = TestNavigationEventCallback()
dispatcher.addCallback(callback)
- assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
+ assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
- // Disable the callback to trigger it again
+ // Disabling the callback should trigger it again.
callback.isEnabled = false
- assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(2)
+ assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(2)
}
@Test
- fun onHasEnabledCallbacksChanged_whenHandlerIsRemoved_callbackIsNotFired() {
+ fun onHasEnabledCallbacksChanged_afterInputRemoved_doesNotNotify() {
val dispatcher = NavigationEventDispatcher()
- val handler = TestNavigationEventInput()
- dispatcher.addInputHandler(handler)
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
val callback1 = TestNavigationEventCallback()
dispatcher.addCallback(callback1)
- assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
+ assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
- dispatcher.removeInputHandler(handler)
+ dispatcher.removeInput(input)
- // Add another callback; the removed handler should not be notified
+ // Add another callback; the removed input should not be notified.
val callback2 = TestNavigationEventCallback()
dispatcher.addCallback(callback2)
- assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1) // Unchanged
+ assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1) // Unchanged
}
@Test
- fun dispose_whenParentIsDisposed_detachesChildInputHandlers() {
+ fun dispose_onParent_detachesChildInputs() {
val parentDispatcher = NavigationEventDispatcher()
val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentHandler = TestNavigationEventInput()
- val childHandler = TestNavigationEventInput()
- parentDispatcher.addInputHandler(parentHandler)
- childDispatcher.addInputHandler(childHandler)
+ val parentInput = TestNavigationEventInput()
+ val childInput = TestNavigationEventInput()
+ parentDispatcher.addInput(parentInput)
+ childDispatcher.addInput(childInput)
- // Disposing the parent should cascade to the child
+ // Disposing the parent should cascade to the child.
parentDispatcher.dispose()
- assertThat(parentHandler.removedInvocations).isEqualTo(1)
- assertThat(childHandler.removedInvocations).isEqualTo(1)
- assertThat(parentHandler.currentDispatcher).isNull()
- assertThat(childHandler.currentDispatcher).isNull()
+ assertThat(parentInput.removedInvocations).isEqualTo(1)
+ assertThat(childInput.removedInvocations).isEqualTo(1)
+ assertThat(parentInput.currentDispatcher).isNull()
+ assertThat(childInput.currentDispatcher).isNull()
+ }
+
+ // endregion Core API
+
+ // region Passive Listeners API
+
+ @Test
+ fun state_onMultipleCallbacksAdded_reflectsLastAdded() = runTest {
+ val dispatcher = NavigationEventDispatcher()
+ val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+ val detailsCallback =
+ TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+
+ assertThat(dispatcher.state.value).isEqualTo(Idle(NotProvided))
+
+ dispatcher.addCallback(homeCallback)
+ assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("home")))
+
+ // Callbacks are prioritized like a stack (LIFO), so adding a new one makes it active.
+ dispatcher.addCallback(detailsCallback)
+ assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+ }
+
+ @Test
+ fun state_onSetInfoOnActiveCallback_updatesState() = runTest {
+ val dispatcher = NavigationEventDispatcher()
+ val callback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("initial"))
+ dispatcher.addCallback(callback)
+
+ assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("initial")))
+
+ // Calling setInfo on the active callback should immediately update the dispatcher's state.
+ callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
+
+ assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("updated")))
+ }
+
+ @Test
+ fun state_onSetInfoOnInactiveCallback_doesNotUpdateState() = runTest {
+ val dispatcher = NavigationEventDispatcher()
+ val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+ val detailsCallback =
+ TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+ dispatcher.addCallback(homeCallback)
+ dispatcher.addCallback(detailsCallback)
+
+ // The state should reflect the last-added (active) callback.
+ assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+
+ // Calling setInfo on an inactive callback should NOT affect the global state.
+ homeCallback.setInfo(currentInfo = HomeScreenInfo("home-updated"), previousInfo = null)
+
+ // The state should remain unchanged because the update came from a non-active callback.
+ assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+ }
+
+ @Test
+ fun state_onFullGestureLifecycle_transitionsToInProgressThenIdle() {
+ val dispatcher = NavigationEventDispatcher()
+ val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+ val callbackInfo = HomeScreenInfo("home")
+ val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+ dispatcher.addCallback(callback)
+
+ val startEvent = NavigationEvent(touchX = 0.1F)
+ val progressEvent = NavigationEvent(touchX = 0.3f)
+
+ assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+
+ // Starting a gesture should move the state to InProgress with the start event.
+ input.handleOnStarted(startEvent)
+ var state = dispatcher.state.value as InProgress
+ assertThat(state.currentInfo).isEqualTo(callbackInfo)
+ assertThat(state.previousInfo).isNull()
+ assertThat(state.latestEvent).isEqualTo(startEvent)
+
+ // Progressing the gesture should keep it InProgress but update to the latest event.
+ input.handleOnProgressed(progressEvent)
+ state = dispatcher.state.value as InProgress
+ assertThat(state.latestEvent).isEqualTo(progressEvent)
+
+ // Completing the gesture should return the state to Idle.
+ input.handleOnCompleted()
+ assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+ }
+
+ @Test
+ fun state_onGestureCancelled_returnsToIdle() {
+ val dispatcher = NavigationEventDispatcher()
+ val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+ val callbackInfo = HomeScreenInfo("home")
+ val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+ dispatcher.addCallback(callback)
+
+ val startEvent = NavigationEvent()
+
+ assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+
+ // Starting a gesture moves the state to InProgress.
+ input.handleOnStarted(startEvent)
+ assertThat(dispatcher.state.value).isEqualTo(InProgress(callbackInfo, null, startEvent))
+
+ // Cancelling the gesture should also return the state to Idle.
+ input.handleOnCancelled()
+ assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+ }
+
+ @Test
+ fun inProgressState_onInfoUpdateDuringGesture_reflectsNewInfo() {
+ val dispatcher = NavigationEventDispatcher()
+ val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+ val firstInfo = HomeScreenInfo("initial")
+ val callback = TestNavigationEventCallback(currentInfo = firstInfo)
+ dispatcher.addCallback(callback)
+
+ val startEvent = NavigationEvent(touchX = 0.1F)
+
+ input.handleOnStarted(startEvent)
+
+ // At the start, previousInfo is null.
+ var state = dispatcher.state.value as InProgress
+ assertThat(state.currentInfo).isEqualTo(firstInfo)
+ assertThat(state.previousInfo).isNull()
+ assertThat(state.latestEvent).isEqualTo(startEvent)
+
+ // Update the info mid-gesture.
+ val secondInfo = HomeScreenInfo("updated")
+ callback.setInfo(currentInfo = secondInfo, previousInfo = firstInfo)
+
+ // The state should now reflect the updated info. The `previousInfo` is now captured.
+ state = dispatcher.state.value as InProgress
+ assertThat(state.currentInfo).isEqualTo(secondInfo)
+ assertThat(state.previousInfo).isEqualTo(firstInfo)
+ assertThat(state.latestEvent).isEqualTo(startEvent) // Event hasn't changed yet.
+
+ // Complete the gesture.
+ input.handleOnCompleted()
+ assertThat(dispatcher.state.value).isEqualTo(Idle(secondInfo))
+ }
+
+ @Test
+ fun inProgressState_onNewGesture_clearsPreviousInfo() {
+ val dispatcher = NavigationEventDispatcher()
+ val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+ val initialInfo = HomeScreenInfo("initial")
+ val callback = TestNavigationEventCallback(currentInfo = initialInfo)
+ dispatcher.addCallback(callback)
+
+ // FIRST GESTURE: Create a complex state.
+ input.handleOnStarted(NavigationEvent(touchX = 0.1f))
+ callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
+ input.handleOnCompleted()
+
+ // After the first gesture, the final state is Idle with the updated info.
+ val finalInfo = HomeScreenInfo("updated")
+ assertThat(dispatcher.state.value).isEqualTo(Idle(finalInfo))
+
+ // SECOND GESTURE: Start a new gesture.
+ val event2 = NavigationEvent(touchX = 0.3f)
+ input.handleOnStarted(event2)
+
+ // When a new gesture starts, `previousInfo` must be null, not stale data
+ // from a previous, completed gesture.
+ val state = dispatcher.state.value as InProgress
+ assertThat(state.currentInfo).isEqualTo(finalInfo)
+ assertThat(state.previousInfo).isNull()
+ assertThat(state.latestEvent).isEqualTo(event2)
+ }
+
+ @Test
+ fun state_onDispatcherDisabled_fallsBackToSibling() {
+ val dispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher = dispatcher)
+
+ val callbackA = TestNavigationEventCallback(currentInfo = HomeScreenInfo("A"))
+ dispatcher.addCallback(callbackA)
+ assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
+
+ val callbackB = TestNavigationEventCallback(currentInfo = DetailsScreenInfo("B"))
+ childDispatcher.addCallback(callbackB)
+ // Assert that state reflects callbackB, which was added last and is now active.
+ assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
+
+ // Disable the dispatcher that hosts the currently active callback.
+ childDispatcher.isEnabled = false
+
+ // The state should now fall back to the next-highest priority callback (callbackA).
+ assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
+
+ // Re-enable the dispatcher.
+ childDispatcher.isEnabled = true
+
+ // The state should once again reflect callbackB.
+ assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
+ }
+
+ @Test
+ fun getState_withTypeFilter_emitsOnlyMatchingStates() =
+ runTest(UnconfinedTestDispatcher()) {
+ val dispatcher = NavigationEventDispatcher()
+ val initialHomeInfo = HomeScreenInfo("initial")
+ val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+ val detailsCallback =
+ TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+ val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
+
+ dispatcher
+ .getState(backgroundScope, initialHomeInfo)
+ .onEach { collectedStates.add(it) }
+ .launchIn(backgroundScope)
+ advanceUntilIdle()
+
+ // The flow must start with the initial value provided.
+ assertThat(collectedStates).hasSize(1)
+ assertThat(collectedStates.last()).isEqualTo(Idle(initialHomeInfo))
+
+ // A new state with a matching type should be collected.
+ dispatcher.addCallback(homeCallback)
+ advanceUntilIdle()
+ assertThat(collectedStates).hasSize(2)
+ assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
+
+ // A state with a non-matching type should be filtered out and not collected.
+ dispatcher.addCallback(detailsCallback)
+ advanceUntilIdle()
+ assertThat(collectedStates).hasSize(2)
+
+ // When the active callback is removed, the state falls back to a matching type,
+ // but since the info is the same as before, no new state is emitted.
+ detailsCallback.remove()
+ advanceUntilIdle()
+ assertThat(collectedStates).hasSize(2)
+ assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
+ }
+
+ @Test
+ fun getState_withNonMatchingType_emitsOnlyInitialInfo() =
+ runTest(UnconfinedTestDispatcher()) {
+ val dispatcher = NavigationEventDispatcher()
+ val initialHomeInfo = HomeScreenInfo("initial")
+ val detailsCallback =
+ TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+ val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
+
+ dispatcher
+ .getState(backgroundScope, initialHomeInfo)
+ .onEach { collectedStates.add(it) }
+ .launchIn(backgroundScope)
+ advanceUntilIdle()
+
+ // The flow must start with its initial value.
+ assertThat(collectedStates).hasSize(1)
+ assertThat(collectedStates.first()).isEqualTo(Idle(initialHomeInfo))
+
+ // Add a callback with a non-matching type.
+ dispatcher.addCallback(detailsCallback)
+ advanceUntilIdle()
+
+ // The collector should not have emitted a new value.
+ assertThat(collectedStates).hasSize(1)
+
+ // Update the non-matching callback's info.
+ detailsCallback.setInfo(
+ currentInfo = DetailsScreenInfo("details-updated"),
+ previousInfo = null,
+ )
+ advanceUntilIdle()
+
+ // The collector should still not have emitted a new value.
+ assertThat(collectedStates).hasSize(1)
+ }
+
+ @Test
+ fun progress_inIdleAndInProgress_returnsCorrectValue() {
+ val dispatcher = NavigationEventDispatcher()
+ val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+ val callbackInfo = HomeScreenInfo("home")
+ val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+ dispatcher.addCallback(callback)
+
+ // Before any gesture, the state is Idle and progress should be 0.
+ assertThat(dispatcher.state.value.progress).isEqualTo(0f)
+
+ // Start a gesture.
+ input.handleOnStarted(NavigationEvent(progress = 0.1f))
+ assertThat(dispatcher.state.value.progress).isEqualTo(0.1f)
+
+ // InProgress state should reflect the event's progress.
+ input.handleOnProgressed(NavigationEvent(progress = 0.5f))
+ assertThat(dispatcher.state.value.progress).isEqualTo(0.5f)
+
+ // Complete the gesture.
+ input.handleOnCompleted()
+
+ // After the gesture, the state is Idle again and progress should be 0.
+ assertThat(dispatcher.state.value.progress).isEqualTo(0f)
+ }
+
+ // endregion
+
+ // region Hierarchy APIs
+
+ @Test
+ fun init_withParent_sharesCallbacks() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val parentCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val childCallback = TestNavigationEventCallback()
+ childDispatcher.addCallback(childCallback)
+
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ // Callbacks from child dispatchers are prioritized over their parents (LIFO).
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun init_withoutParent_hasIndependentCallbacks() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher()
+
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ // Dispatch an event through the parent.
+ val event = NavigationEvent()
+ val parentInput = TestNavigationEventInput()
+ parentDispatcher.addInput(parentInput)
+ parentInput.handleOnStarted(event)
+
+ // Only the parent's callback should be invoked.
+ assertThat(parentCallback.startedInvocations).isEqualTo(1)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+ // Dispatch an event through the child.
+ val childInput = TestNavigationEventInput()
+ childDispatcher.addInput(childInput)
+ childInput.handleOnStarted(event)
+
+ // Only the child's callback should be invoked.
+ assertThat(parentCallback.startedInvocations).isEqualTo(1)
+ assertThat(childCallback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun addCallback_toChild_isDispatchedViaParent() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val callback = TestNavigationEventCallback()
+
+ childDispatcher.addCallback(callback)
+
+ // Events dispatched from a parent should propagate to callbacks in child dispatchers.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ assertThat(callback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun addCallback_toParentThenChild_ordersLIFO() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ // The last-added callback (child's) should be invoked first.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun addCallback_multipleDispatchers_prioritizesLastAdded() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val child1Dispatcher = NavigationEventDispatcher(parentDispatcher)
+ val child2Dispatcher = NavigationEventDispatcher(parentDispatcher)
+
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback1 = TestNavigationEventCallback()
+ val childCallback2 = TestNavigationEventCallback()
+
+ parentDispatcher.addCallback(parentCallback)
+ child2Dispatcher.addCallback(childCallback2)
+ child1Dispatcher.addCallback(childCallback1)
+
+ // Callbacks are processed in a LIFO manner across the entire hierarchy.
+ // The callback from child1 was added last, so it gets the event.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback2.startedInvocations).isEqualTo(0)
+ assertThat(childCallback1.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun dispose_onChild_parentStillReceivesEvents() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ // Disposing a child should not affect its parent.
+ childDispatcher.dispose()
+
+ // Dispatching an event from the parent should now trigger the parent's callback,
+ // as the child's (previously higher priority) callback is gone.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ assertThat(parentCallback.startedInvocations).isEqualTo(1)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun dispose_onParent_cascadesAndDisablesChildren() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+
+ parentDispatcher.dispose()
+
+ // Attempting to use either dispatcher should now fail.
+ val event = NavigationEvent()
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun dispose_onGrandparent_cascadesAndDisablesHierarchy() {
+ val grandparentDispatcher = NavigationEventDispatcher()
+ val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+
+ // Disposing the root dispatcher should disable the entire hierarchy.
+ grandparentDispatcher.dispose()
+
+ // Attempting to use any dispatcher in the hierarchy should fail.
+ val event = NavigationEvent()
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ grandparentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(event)
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun isEnabled_whenTrue_dispatchesEvents() {
+ val dispatcher = NavigationEventDispatcher()
+ val callback = TestNavigationEventCallback()
+ dispatcher.addCallback(callback)
+
+ dispatcher.isEnabled = true
+
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(event)
+ assertThat(callback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun isEnabled_whenFalse_doesNotDispatchEvents() {
+ val dispatcher = NavigationEventDispatcher()
+ val callback = TestNavigationEventCallback(isEnabled = true)
+ dispatcher.addCallback(callback)
+
+ dispatcher.isEnabled = false
+
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(event)
+ assertThat(callback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun isEnabled_parentDisabled_disablesChildDispatch() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ // Disabling the parent should effectively disable the entire sub-hierarchy.
+ parentDispatcher.isEnabled = false
+
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun isEnabled_childDisabled_doesNotDispatch() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ childDispatcher.isEnabled = false
+
+ // Events sent to the child dispatcher should not be processed by any callback.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun isEnabled_childDisabled_parentStillDispatches() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ childDispatcher.isEnabled = false
+
+ // Disabling a child should not affect the parent. Events sent directly to the
+ // parent should be handled by the parent's callbacks.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ assertThat(parentCallback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun isEnabled_parentReenabled_reenablesChildDispatch() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ parentDispatcher.isEnabled = false
+ val initialEvent = NavigationEvent()
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(initialEvent)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+ parentDispatcher.isEnabled = true
+
+ val reEnabledEvent = NavigationEvent()
+ input.handleOnStarted(reEnabledEvent)
+
+ assertThat(childCallback.startedInvocations).isEqualTo(1)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun isEnabled_parentReenabled_childCallbackReceivesEvents() {
+ val parentDispatcher = NavigationEventDispatcher()
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ parentDispatcher.isEnabled = false
+ val initialEvent = NavigationEvent()
+ val input = TestNavigationEventInput()
+ parentDispatcher.addInput(input)
+ input.handleOnStarted(initialEvent)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+ parentDispatcher.isEnabled = true
+
+ val reEnabledEvent = NavigationEvent()
+ input.handleOnStarted(reEnabledEvent)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun isEnabled_grandparentDisabled_disablesGrandchildDispatch() {
+ val grandparentDispatcher = NavigationEventDispatcher()
+ val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val grandparentCallback = TestNavigationEventCallback()
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+
+ grandparentDispatcher.addCallback(grandparentCallback)
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ grandparentDispatcher.isEnabled = false
+
+ // Disabling the grandparent should disable all descendants.
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ childDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun isEnabled_grandparentDisabled_grandchildCallbackDoesNotReceiveEvents() {
+ val grandparentDispatcher = NavigationEventDispatcher()
+ val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+ val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+ val grandparentCallback = TestNavigationEventCallback()
+ val parentCallback = TestNavigationEventCallback()
+ val childCallback = TestNavigationEventCallback()
+
+ grandparentDispatcher.addCallback(grandparentCallback)
+ parentDispatcher.addCallback(parentCallback)
+ childDispatcher.addCallback(childCallback)
+
+ grandparentDispatcher.isEnabled = false
+
+ val event = NavigationEvent()
+ val input = TestNavigationEventInput()
+ grandparentDispatcher.addInput(input)
+ input.handleOnStarted(event)
+
+ assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
+ assertThat(parentCallback.startedInvocations).isEqualTo(0)
+ assertThat(childCallback.startedInvocations).isEqualTo(0)
+ }
+
+ @Test
+ fun callbackIsEnabled_whenDispatcherDisabled_doesNotReceiveEvents() {
+ val dispatcher = NavigationEventDispatcher()
+ val callback = TestNavigationEventCallback()
+ dispatcher.addCallback(callback)
+ val preDisableEvent = NavigationEvent()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(preDisableEvent)
+ assertThat(callback.startedInvocations).isEqualTo(1)
+
+ dispatcher.isEnabled = false
+
+ // An enabled callback on a disabled dispatcher should not receive events.
+ val event = NavigationEvent()
+ input.handleOnStarted(event)
+
+ assertThat(callback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun callbackIsEnabled_whenDispatcherReenabled_receivesEvents() {
+ val dispatcher = NavigationEventDispatcher()
+ val callback = TestNavigationEventCallback()
+ dispatcher.addCallback(callback)
+ dispatcher.isEnabled = false
+
+ val preEnableEvent = NavigationEvent()
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(preEnableEvent)
+ assertThat(callback.startedInvocations).isEqualTo(0)
+
+ dispatcher.isEnabled = true
+
+ val reEnabledEvent = NavigationEvent()
+ input.handleOnStarted(reEnabledEvent)
+
+ assertThat(callback.startedInvocations).isEqualTo(1)
+ }
+
+ @Test
+ fun addCallback_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> {
+ dispatcher.addCallback(TestNavigationEventCallback())
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun handleOnStarted_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnStarted(NavigationEvent())
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun handleOnProgressed_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnProgressed(NavigationEvent())
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun handleOnCompleted_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCompleted()
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun handleOnCancelled_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> {
+ val input = TestNavigationEventInput()
+ dispatcher.addInput(input)
+ input.handleOnCancelled()
+ }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun dispose_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ // Disposing an already-disposed dispatcher should fail.
+ assertThrows<IllegalStateException> { dispatcher.dispose() }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun setEnabled_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ @Test
+ fun setDisabled_onDisposedDispatcher_throws() {
+ val dispatcher = NavigationEventDispatcher()
+ dispatcher.dispose()
+
+ assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
+ .hasMessageThat()
+ .contains("has already been disposed")
+ }
+
+ // endregion Hierarchy APIs
+}
+
+/** A sealed interface for type-safe navigation information. */
+sealed interface TestInfo : NavigationEventInfo
+
+data class HomeScreenInfo(val id: String) : TestInfo
+
+data class DetailsScreenInfo(val id: String) : TestInfo
+
+/**
+ * A test implementation of [NavigationEventInput] that records lifecycle events and invocation
+ * counts.
+ *
+ * Use this class in tests to verify that `onAdded`, `onRemoved`, and `onHasEnabledCallbacksChanged`
+ * are called correctly. It counts how many times each lifecycle method is invoked and stores a
+ * reference to the most recently added dispatcher. It also provides helper methods to simulate
+ * dispatching navigation events.
+ *
+ * @param onAdded An optional lambda to execute when [onAdded] is called.
+ * @param onRemoved An optional lambda to execute when [onRemoved] is called.
+ * @param onHasEnabledCallbacksChanged An optional lambda to execute when
+ * [onHasEnabledCallbacksChanged] is called.
+ */
+private class TestNavigationEventInput(
+ private val onAdded: (dispatcher: NavigationEventDispatcher) -> Unit = {},
+ private val onRemoved: () -> Unit = {},
+ private val onHasEnabledCallbacksChanged: (hasEnabledCallbacks: Boolean) -> Unit = {},
+) : NavigationEventInput() {
+
+ /** The number of times [onAdded] has been invoked. */
+ var addedInvocations: Int = 0
+ private set
+
+ /** The number of times [onRemoved] has been invoked. */
+ var removedInvocations: Int = 0
+ private set
+
+ /** The number of times [onHasEnabledCallbacksChanged] has been invoked. */
+ var onHasEnabledCallbacksChangedInvocations: Int = 0
+ private set
+
+ /**
+ * The most recently added [NavigationEventDispatcher].
+ *
+ * This is set by [onAdded] and cleared to `null` by [onRemoved].
+ */
+ var currentDispatcher: NavigationEventDispatcher? = null
+ private set
+
+ /**
+ * Test helper to simulate the start of a navigation event.
+ *
+ * This directly calls `dispatchOnStarted`, notifying any registered callbacks. Use this to
+ * trigger the beginning of a navigation flow in your tests.
+ *
+ * @param event The [NavigationEvent] to dispatch.
+ */
+ @MainThread
+ fun handleOnStarted(event: NavigationEvent = NavigationEvent()) {
+ dispatchOnStarted(event)
+ }
+
+ /**
+ * Test helper to simulate the progress of a navigation event.
+ *
+ * This directly calls `dispatchOnProgressed`, notifying any registered callbacks.
+ *
+ * @param event The [NavigationEvent] to dispatch.
+ */
+ @MainThread
+ fun handleOnProgressed(event: NavigationEvent = NavigationEvent()) {
+ dispatchOnProgressed(event)
+ }
+
+ /**
+ * Test helper to simulate the completion of a navigation event.
+ *
+ * This directly calls `dispatchOnCompleted`, notifying any registered callbacks.
+ */
+ @MainThread
+ fun handleOnCompleted() {
+ dispatchOnCompleted()
+ }
+
+ /**
+ * Test helper to simulate the cancellation of a navigation event.
+ *
+ * This directly calls `dispatchOnCancelled`, notifying any registered callbacks.
+ */
+ @MainThread
+ fun handleOnCancelled() {
+ dispatchOnCancelled()
+ }
+
+ override fun onAdded(dispatcher: NavigationEventDispatcher) {
+ addedInvocations++
+ currentDispatcher = dispatcher
+ onAdded.invoke(dispatcher)
+ }
+
+ override fun onRemoved() {
+ currentDispatcher = null
+ removedInvocations++
+ onRemoved.invoke()
+ }
+
+ override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
+ onHasEnabledCallbacksChangedInvocations++
+ onHasEnabledCallbacksChanged.invoke(hasEnabledCallbacks)
}
}
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt
deleted file mode 100644
index 111c186..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
- * 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.navigationevent
-
-import androidx.kruth.assertThat
-import androidx.kruth.assertThrows
-import androidx.navigationevent.testing.TestNavigationEventCallback
-import kotlin.test.Test
-
-class NavigationEventHierarchyTest {
-
- @Test
- fun init_whenChildIsCreatedWithParent_thenCallbacksAreSharedAndDispatched() {
- // Given a parent dispatcher and a callback for it
- val parentDispatcher = NavigationEventDispatcher()
- val parentCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
-
- // When a child dispatcher is created with the parent and a callback is added to the child
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val childCallback = TestNavigationEventCallback()
- childDispatcher.addCallback(childCallback)
-
- // Then, dispatching an event from the parent should also trigger the child's callback,
- // indicating the shared processing.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(parentCallback.startedInvocations)
- .isEqualTo(0) // Assuming LIFO, parent callback is skipped
- assertThat(childCallback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun init_whenChildIsCreatedWithNoParent_thenCallbacksAreIndependent() {
- // Given a parent dispatcher and a child dispatcher created without a parent
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher()
-
- // And a callback for each
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When an event is dispatched through the parent
- val event = NavigationEvent()
- val parentInputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(parentInputHandler)
- parentInputHandler.handleOnStarted(event)
-
- // Then only the parent's callback should be invoked, showing independent processing.
- assertThat(parentCallback.startedInvocations).isEqualTo(1)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
-
- // When an event is dispatched through the child
- val childInputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(childInputHandler)
- childInputHandler.handleOnStarted(event)
-
- // Then only the child's callback should be invoked, showing independent processing.
- assertThat(parentCallback.startedInvocations).isEqualTo(1)
- assertThat(childCallback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun addCallback_whenCalledOnChild_thenCallbackIsDispatchedViaParent() {
- // Given a parent and child dispatcher
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val callback = TestNavigationEventCallback()
-
- // When a new callback is added to the child
- childDispatcher.addCallback(callback)
-
- // Then dispatching an event from the parent should trigger the child's callback
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- assertThat(callback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun addCallback_whenAddedToParentThenChild_thenCallbacksAreOrderedLIFO() {
- // Given a parent and child dispatcher
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
-
- // When a callback is added to the parent, then to the child
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // Then when an event is dispatched, the last-added callback (child's) should be invoked
- // first.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun addCallback_whenMultipleDispatchersAndCallbacksAdded_thenLastAddedCallbackIsInvokedFirst() {
- // Given a parent NavigationEventDispatcher and two child dispatchers.
- val parentDispatcher = NavigationEventDispatcher()
- val child1Dispatcher = NavigationEventDispatcher(parentDispatcher)
- val child2Dispatcher = NavigationEventDispatcher(parentDispatcher)
-
- // And three TestNavigationCallbacks: one for the parent and one for each child.
- val parentCallback = TestNavigationEventCallback()
- val childCallback1 = TestNavigationEventCallback()
- val childCallback2 = TestNavigationEventCallback()
-
- // When callbacks are added to the parent, then child2, then child1.
- parentDispatcher.addCallback(parentCallback)
- child2Dispatcher.addCallback(childCallback2)
- child1Dispatcher.addCallback(childCallback1)
-
- // Then, when an event is dispatched through the parent, only the most recently added active
- // callback (callbackC1 from child1) receives the event. This demonstrates that callbacks
- // are processed in a LIFO manner across the dispatcher hierarchy and that subsequent
- // callbacks are not invoked if an earlier one does not pass through.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback2.startedInvocations).isEqualTo(0)
- assertThat(childCallback1.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun dispose_whenCalledOnChild_thenParentCallbackStillReceivesEvents() {
- // Given a parent and child, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When child is disposed
- childDispatcher.dispose()
-
- // Then dispatching an event from the parent should only trigger the parent's callback
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- assertThat(parentCallback.startedInvocations).isEqualTo(1)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- }
-
- @Test
- fun dispose_whenCalledOnParent_cascadesAndThrowsExceptionOnUse() {
- // Given a parent and child dispatcher
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-
- // When the parent is disposed
- parentDispatcher.dispose()
-
- // Then attempting to use either dispatcher throws an exception
- val event = NavigationEvent()
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- }
- .hasMessageThat()
- .contains("has already been disposed")
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun dispose_whenCalledOnGrandparent_cascadesAndThrowsExceptionOnUse() {
- // Given a three-level dispatcher hierarchy
- val grandparentDispatcher = NavigationEventDispatcher()
- val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-
- // When the grandparent is disposed
- grandparentDispatcher.dispose()
-
- // Then attempting to use any dispatcher in the hierarchy throws an exception
- val event = NavigationEvent()
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- grandparentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- }
- .hasMessageThat()
- .contains("has already been disposed")
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- }
- .hasMessageThat()
- .contains("has already been disposed")
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun isEnabled_whenSetToTrue_thenEventsAreDispatched() {
- // Given a dispatcher
- val dispatcher = NavigationEventDispatcher()
- val callback = TestNavigationEventCallback()
- dispatcher.addCallback(callback)
-
- // When the dispatcher is enabled
- dispatcher.isEnabled = true
-
- // Then dispatching an event should trigger the callback
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- assertThat(callback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun isEnabled_whenSetToFalse_thenNoCallbacksAreDispatched() {
- // Given a dispatcher with an enabled callback
- val dispatcher = NavigationEventDispatcher()
- val callback = TestNavigationEventCallback(isEnabled = true)
- dispatcher.addCallback(callback)
-
- // When the dispatcher is disabled
- dispatcher.isEnabled = false
-
- // Then dispatching an event should not trigger the callback
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
- assertThat(callback.startedInvocations).isEqualTo(0)
- }
-
- @Test
- fun isEnabled_whenParentIsDisabled_thenChildDoesNotDispatchEvents() {
- // Given a parent and child dispatcher, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When the parent is disabled
- parentDispatcher.isEnabled = false
-
- // Then dispatching an event from the child should not invoke any callbacks,
- // because the parent's disabled state propagates.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- }
-
- @Test
- fun isEnabled_whenChildIsLocallyDisabled_thenChildDoesNotDispatchEvents() {
- // Given a parent (enabled) and child, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When child is locally disabled
- childDispatcher.isEnabled = false
-
- // Then dispatching an event from the child should not trigger its callback.
- // The parent's callback should still be invokable via the parent directly.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- assertThat(parentCallback.startedInvocations)
- .isEqualTo(0) // Parent's callback should still fire via parent
- }
-
- @Test
- fun isEnabled_whenChildIsLocallyDisabled_thenChildCallbacksDoesNotReceiveEvents() {
- // Given a parent (enabled) and child, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When child is locally disabled
- childDispatcher.isEnabled = false
-
- // Then dispatching an event from the child should not trigger its callback.
- // The parent's callback should still be invokable via the parent directly.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event) // Confirm parent is still active
-
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- assertThat(parentCallback.startedInvocations)
- .isEqualTo(1) // Parent's callback should still fire via parent
- }
-
- @Test
- fun isEnabled_whenDisabledParentIsReEnabled_thenChildDispatchesEventsAgain() {
- // Given a disabled parent and a locally-enabled child, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- parentDispatcher.isEnabled = false // Initial state: parent (and thus child) disabled
- // Verify pre-condition (no dispatch before re-enabling)
- val initialEvent = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(initialEvent)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
-
- // When the parent is re-enabled
- parentDispatcher.isEnabled = true
-
- // Then the child should now dispatch events
- val reEnabledEvent = NavigationEvent()
- inputHandler.handleOnStarted(reEnabledEvent)
-
- assertThat(childCallback.startedInvocations).isEqualTo(1)
- assertThat(parentCallback.startedInvocations)
- .isEqualTo(0) // Parent's callback is still LIFO behind child
- }
-
- @Test
- fun isEnabled_whenDisabledParentIsReEnabled_thenChildCallbacksReceiveEventsAgain() {
- // Given a disabled parent and a locally-enabled child, both with callbacks
- val parentDispatcher = NavigationEventDispatcher()
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- parentDispatcher.isEnabled = false // Initial state: parent (and thus child) disabled
- // Verify pre-condition (no dispatch before re-enabling)
- val initialEvent = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- parentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(initialEvent)
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
-
- // When the parent is re-enabled
- parentDispatcher.isEnabled = true
-
- // Then the child should now dispatch events
- val reEnabledEvent = NavigationEvent()
- inputHandler.handleOnStarted(reEnabledEvent)
- assertThat(parentCallback.startedInvocations)
- .isEqualTo(0) // Parent's callback is still LIFO behind child
- assertThat(childCallback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun isEnabled_whenGrandparentIsDisabled_thenGrandchildDoesNotDispatchEvents() {
- // Given a three-level hierarchy, each with a callback
- val grandparentDispatcher = NavigationEventDispatcher()
- val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val grandparentCallback = TestNavigationEventCallback()
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
-
- grandparentDispatcher.addCallback(grandparentCallback)
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When the grandparent is disabled
- grandparentDispatcher.isEnabled = false
-
- // Then dispatching an event from the grandchild should result in no callbacks being
- // invoked, as the disabled state cascades down.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- childDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- }
-
- @Test
- fun isEnabled_whenGrandparentIsDisabled_thenGrandchildCallbackDoesNotReceiveEvents() {
- // Given a three-level hierarchy, each with a callback
- val grandparentDispatcher = NavigationEventDispatcher()
- val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
- val childDispatcher = NavigationEventDispatcher(parentDispatcher)
- val grandparentCallback = TestNavigationEventCallback()
- val parentCallback = TestNavigationEventCallback()
- val childCallback = TestNavigationEventCallback()
-
- grandparentDispatcher.addCallback(grandparentCallback)
- parentDispatcher.addCallback(parentCallback)
- childDispatcher.addCallback(childCallback)
-
- // When the grandparent is disabled
- grandparentDispatcher.isEnabled = false
-
- // Then dispatching an event from the grandparent should result in no callbacks being
- // invoked, as the disabled state cascades down.
- val event = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- grandparentDispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(event)
-
- assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
- assertThat(parentCallback.startedInvocations).isEqualTo(0)
- assertThat(childCallback.startedInvocations).isEqualTo(0)
- }
-
- @Test
- fun callbackIsEnabled_whenItsDispatcherIsDisabled_thenCallbackDoesNotReceiveEvents() {
- // Given a dispatcher and an enabled TestNavigationCallback
- val dispatcher = NavigationEventDispatcher()
- val callback = TestNavigationEventCallback()
- dispatcher.addCallback(callback)
- // Ensure callback is initially enabled
- val preDisableEvent = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(preDisableEvent)
- assertThat(callback.startedInvocations).isEqualTo(1)
-
- // When the dispatcher associated with the callback is disabled
- dispatcher.isEnabled = false
-
- // Then dispatching an event (even if the callback's local isEnabled is true)
- // should not trigger the callback because its dispatcher is disabled.
- val event = NavigationEvent()
- inputHandler.handleOnStarted(event)
-
- assertThat(callback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun callbackIsEnabled_whenItsDispatcherIsReEnabled_thenCallbackReceivesEventsAgain() {
- // Given a dispatcher and an enabled TestNavigationCallback, and the dispatcher is disabled
- val dispatcher = NavigationEventDispatcher()
- val callback = TestNavigationEventCallback()
- dispatcher.addCallback(callback)
- dispatcher.isEnabled = false // Disable dispatcher
-
- // Pre-condition: Callback does not receive events when dispatcher is disabled
- val preEnableEvent = NavigationEvent()
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(preEnableEvent)
- assertThat(callback.startedInvocations).isEqualTo(0)
-
- // When the dispatcher associated with the callback is re-enabled
- dispatcher.isEnabled = true
-
- // Then dispatching an event should now trigger the callback
- val reEnabledEvent = NavigationEvent()
- inputHandler.handleOnStarted(reEnabledEvent)
-
- assertThat(callback.startedInvocations).isEqualTo(1)
- }
-
- @Test
- fun addCallback_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose()
-
- // Adding a callback to a disposed dispatcher should fail.
- assertThrows<IllegalStateException> {
- dispatcher.addCallback(TestNavigationEventCallback())
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun handleOnStarted_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose()
-
- // Dispatching on a disposed dispatcher should fail.
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnStarted(NavigationEvent())
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun handleOnProgressed_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose()
-
- // Dispatching on a disposed dispatcher should fail.
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnProgressed(NavigationEvent())
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun handleOnCompleted_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose()
-
- // Dispatching on a disposed dispatcher should fail.
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCompleted()
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun handleOnCancelled_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose()
-
- // Dispatching on a disposed dispatcher should fail.
- assertThrows<IllegalStateException> {
- val inputHandler = DirectNavigationEventInputHandler()
- dispatcher.addInputHandler(inputHandler)
- inputHandler.handleOnCancelled()
- }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun dispose_onDisposedDispatcher_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose() // First disposal is fine.
-
- // Disposing an already-disposed dispatcher should fail.
- assertThrows<IllegalStateException> { dispatcher.dispose() }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun dispose_enabled_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose() // First disposal is fine.
-
- // Enabling an already-disposed dispatcher should fail.
- assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-
- @Test
- fun dispose_disabled_throwsException() {
- val dispatcher = NavigationEventDispatcher()
- dispatcher.dispose() // First disposal is fine.
-
- // disabling an already-disposed dispatcher should fail.
- assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
- .hasMessageThat()
- .contains("has already been disposed")
- }
-}
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt
deleted file mode 100644
index c2fbdca..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.navigationevent.internal
-
-import androidx.annotation.MainThread
-import androidx.navigationevent.NavigationEvent
-import androidx.navigationevent.NavigationEventDispatcher
-import androidx.navigationevent.NavigationEventInputHandler
-
-/**
- * A test implementation of [NavigationEventInputHandler] that records lifecycle events and
- * invocation counts.
- *
- * Use this class in tests to verify that `onAdded`, `onRemoved`, and `onHasEnabledCallbacksChanged`
- * are called correctly. It counts how many times each lifecycle method is invoked and stores a
- * reference to the most recently added dispatcher. It also provides helper methods to simulate
- * dispatching navigation events.
- *
- * @param onAdded An optional lambda to execute when [onAdded] is called.
- * @param onRemoved An optional lambda to execute when [onRemoved] is called.
- * @param onHasEnabledCallbacksChanged An optional lambda to execute when
- * [onHasEnabledCallbacksChanged] is called.
- */
-// TODO(mgalhardo): aosp/3732271 is renaming this to `Input`
-internal class TestNavigationEventInput(
- private val onAdded: (dispatcher: NavigationEventDispatcher) -> Unit = {},
- private val onRemoved: () -> Unit = {},
- private val onHasEnabledCallbacksChanged: (hasEnabledCallbacks: Boolean) -> Unit = {},
-) : NavigationEventInputHandler() {
-
- /** The number of times [onAdded] has been invoked. */
- var addedInvocations: Int = 0
- private set
-
- /** The number of times [onRemoved] has been invoked. */
- var removedInvocations: Int = 0
- private set
-
- /** The number of times [onHasEnabledCallbacksChanged] has been invoked. */
- var onHasEnabledCallbacksChangedInvocations: Int = 0
- private set
-
- /**
- * The most recently added [NavigationEventDispatcher].
- *
- * This is set by [onAdded] and cleared to `null` by [onRemoved].
- */
- var currentDispatcher: NavigationEventDispatcher? = null
- private set
-
- /**
- * Test helper to simulate the start of a navigation event.
- *
- * This directly calls `dispatchOnStarted`, notifying any registered callbacks. Use this to
- * trigger the beginning of a navigation flow in your tests.
- *
- * @param event The [NavigationEvent] to dispatch.
- */
- @MainThread
- fun handleOnStarted(event: NavigationEvent = NavigationEvent()) {
- dispatchOnStarted(event)
- }
-
- /**
- * Test helper to simulate the progress of a navigation event.
- *
- * This directly calls `dispatchOnProgressed`, notifying any registered callbacks.
- *
- * @param event The [NavigationEvent] to dispatch.
- */
- @MainThread
- fun handleOnProgressed(event: NavigationEvent = NavigationEvent()) {
- dispatchOnProgressed(event)
- }
-
- /**
- * Test helper to simulate the completion of a navigation event.
- *
- * This directly calls `dispatchOnCompleted`, notifying any registered callbacks.
- */
- @MainThread
- fun handleOnCompleted() {
- dispatchOnCompleted()
- }
-
- /**
- * Test helper to simulate the cancellation of a navigation event.
- *
- * This directly calls `dispatchOnCancelled`, notifying any registered callbacks.
- */
- @MainThread
- fun handleOnCancelled() {
- dispatchOnCancelled()
- }
-
- // TODO(mgalhardo): aosp/3732466 is renaming this to `onAdded`
- override fun onAttach(dispatcher: NavigationEventDispatcher) {
- addedInvocations++
- currentDispatcher = dispatcher
- onAdded.invoke(dispatcher)
- }
-
- // TODO(mgalhardo): aosp/3732466 is renaming this to `onRemoved`
- override fun onDetach() {
- currentDispatcher = null
- removedInvocations++
- onRemoved.invoke()
- }
-
- override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
- onHasEnabledCallbacksChangedInvocations++
- onHasEnabledCallbacksChanged.invoke(hasEnabledCallbacks)
- }
-}
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index c135920..97bc2218d 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -119,36 +119,6 @@
dependsOn(commonTest)
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- }
-
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
jvmTest {
dependsOn(commonJvmAndroidTest)
}
@@ -176,6 +146,9 @@
throw new GradleException("unknown native target ${target}")
}
}
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(webTest)
}
}
}
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 1671885..eabf42c 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -55,8 +55,7 @@
commonTest {
dependencies {
implementation(project(":kruth:kruth"))
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinTestForWasmTests)
+ implementation(libs.kotlinTest)
}
}
@@ -76,25 +75,14 @@
dependsOn(commonMain)
}
- jsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- wasmJsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
+ nativeTest {
+ dependsOn(commonTest)
}
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
- target.compilations["main"].defaultSourceSet {
- dependsOn(nativeMain)
- }
+ target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+ target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
}
}
}
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 c1a53a0..5612081 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
@@ -38,7 +38,6 @@
import androidx.testutils.TestExecutor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
-import kotlin.test.Ignore
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.CoroutineName
@@ -735,9 +734,9 @@
db.close()
}
- @Ignore("Due to b/373727432.")
@Test
fun invalid_append() = runTest {
+ dao.addAllItems(ITEMS_LIST)
val pagingSource = LimitOffsetPagingSourceImpl(db)
val pager = TestPager(CONFIG, pagingSource)
@@ -763,9 +762,9 @@
assertThat(pagingSource.invalid).isTrue()
}
- @Ignore("Due to b/365167269.")
@Test
fun invalid_prepend() = runTest {
+ dao.addAllItems(ITEMS_LIST)
val pagingSource = LimitOffsetPagingSourceImpl(db)
val pager = TestPager(CONFIG, pagingSource)
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index d958dbd..701b58b 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -73,35 +73,28 @@
internal val itemCount = AtomicInt(INITIAL_ITEM_COUNT)
- private val flow = db.invalidationTracker.createFlow(*tables, emitInitialState = false)
-
- private val invalidationFlowStarted = AtomicBoolean(false)
-
private val refreshComplete = AtomicBoolean(false)
private var invalidationFlowJob: Job? = null
init {
// register db listeners right away
- db.getCoroutineScope().launch { flow.collect() }
+ invalidationFlowJob =
+ db.getCoroutineScope().launch {
+ db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
+ if (pagingSource.invalid) {
+ throw CancellationException("PagingSource is invalid")
+ }
+ if (refreshComplete.get()) {
+ pagingSource.invalidate()
+ }
+ }
+ }
pagingSource.registerInvalidatedCallback { invalidationFlowJob?.cancel() }
}
suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
- if (invalidationFlowStarted.compareAndSet(false, true)) {
- invalidationFlowJob =
- db.getCoroutineScope().launch {
- db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
- if (pagingSource.invalid) {
- throw CancellationException("PagingSource is invalid")
- }
- if (refreshComplete.get()) {
- pagingSource.invalidate()
- }
- }
- }
- }
val tempCount = itemCount.get()
// if itemCount is < 0, then it is initial load
return try {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
index 97bf39b..49863d4 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
@@ -118,6 +118,16 @@
if (currentVersion == databaseVersion) {
return
}
+ if (
+ databaseConfiguration.migrationContainer.findMigrationPath(
+ currentVersion,
+ databaseVersion,
+ ) != null
+ ) {
+ // There is a migration path and it will be prioritized, i.e. we won't be
+ // performing a copy destructive migration.
+ return
+ }
if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) {
// From the current version to the desired version a migration is required, i.e.
// we won't be performing a copy destructive migration.
diff --git a/savedstate/savedstate-compose/build.gradle b/savedstate/savedstate-compose/build.gradle
index b25aca2..aa19acc 100644
--- a/savedstate/savedstate-compose/build.gradle
+++ b/savedstate/savedstate-compose/build.gradle
@@ -103,36 +103,6 @@
dependsOn(nonAndroidTest)
}
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestWasm)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/savedstate/savedstate-testing/build.gradle b/savedstate/savedstate-testing/build.gradle
index ffe49bf..66d47d2 100644
--- a/savedstate/savedstate-testing/build.gradle
+++ b/savedstate/savedstate-testing/build.gradle
@@ -25,6 +25,7 @@
import androidx.build.PlatformIdentifier
import androidx.build.SoftwareType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
plugins {
id("AndroidXPlugin")
@@ -165,45 +166,15 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+ } else if (target.konanTarget.family == Family.LINUX) {
target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
- } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+ } else if (target.konanTarget.family == Family.MINGW) {
target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
} else {
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index bd6baba..0df19ff 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -168,36 +168,6 @@
}
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestForWasmTests)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependsOn(webTest)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
targets.configureEach { target ->
if (target.platformType == KotlinPlatformType.native) {
if (target.konanTarget.family.appleFamily) {
diff --git a/settings.gradle b/settings.gradle
index 4f45dba..cef59e5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -603,6 +603,7 @@
includeProject(":compose:remote:remote-creation-core", [BuildType.MAIN])
includeProject(":compose:remote:remote-creation", [BuildType.MAIN])
includeProject(":compose:remote:remote-player-view", [BuildType.MAIN])
+includeProject(":compose:remote:remote-player-compose", [BuildType.MAIN])
includeProject(":compose:remote:remote-frontend", [BuildType.MAIN])
includeProject(":compose:remote:remote-foundation", [BuildType.MAIN])
includeProject(":compose:remote:test-utils", [BuildType.MAIN])
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
index 7b4ec59..daed86e 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
@@ -102,7 +102,6 @@
// we'll need to use manually-defined stubs.
val compileSdkProvider =
project.provider {
- @Suppress("deprecation") // TODO(aurimas): migrate to new API
project.extensions.findByType(CommonExtension::class.java)?.compileSdk
?: project.extensions
.findByType(KotlinMultiplatformExtension::class.java)
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index 31516f5..93f26af 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -47,7 +47,7 @@
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
- androidTestImplementation("androidx.lifecycle:lifecycle-common:2.9.0")
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testMonitor)
}
diff --git a/testutils/testutils-lifecycle/build.gradle b/testutils/testutils-lifecycle/build.gradle
index b551d74..9f85c43 100644
--- a/testutils/testutils-lifecycle/build.gradle
+++ b/testutils/testutils-lifecycle/build.gradle
@@ -21,8 +21,10 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
+
import androidx.build.SoftwareType
import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
plugins {
id("AndroidXPlugin")
@@ -73,23 +75,11 @@
dependsOn(commonMain)
}
- wasmJsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- jsMain {
- dependsOn(webMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
targets.all { target ->
- if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+ if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+ } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+ target.compilations["main"].defaultSourceSet.dependsOn(webMain)
}
}
}
diff --git a/testutils/testutils-paging/build.gradle b/testutils/testutils-paging/build.gradle
index 82e9973..1f1adc7 100644
--- a/testutils/testutils-paging/build.gradle
+++ b/testutils/testutils-paging/build.gradle
@@ -46,20 +46,6 @@
implementation(libs.kotlinTest)
}
}
-
- jsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- wasmJsMain {
- dependsOn(commonMain)
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
}
}
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index df65ccb..46a09c2 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -59,7 +59,7 @@
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
- androidTestImplementation("androidx.lifecycle:lifecycle-common:2.9.0")
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
}
androidx {
diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
index 70e984e..853e3b5 100644
--- a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
+++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
@@ -53,7 +53,7 @@
@Test
public void testConstructsWebView() {
- WebViewBuilder builder = new WebViewBuilder();
+ WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY);
try (ActivityScenario<WebViewTestActivity> scenario =
ActivityScenario.launch(WebViewTestActivity.class)) {
@@ -75,7 +75,7 @@
@Test
public void testConstructsWebViewTwice() {
- WebViewBuilder builder = new WebViewBuilder();
+ WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY);
try (ActivityScenario<WebViewTestActivity> scenario =
ActivityScenario.launch(WebViewTestActivity.class)) {
@@ -137,8 +137,9 @@
.javascriptInterface(new TestInterface(3), "jsInterface3")
.build();
- WebViewBuilder builder =
- new WebViewBuilder().restrictJavascriptInterface().addAllowlist(allowlist);
+ WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY)
+ .restrictJavascriptInterface()
+ .addAllowlist(allowlist);
WebView webview = build(builder);
@@ -168,7 +169,8 @@
.javascriptInterface(jsInterface, "jsInterface")
.build();
- WebViewBuilder builder = new WebViewBuilder().addAllowlist(allowlist);
+ WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY)
+ .addAllowlist(allowlist);
// This builder did not call restrictJavascriptInterface before allowlisting
Assert.assertThrows(WebViewBuilderException.class, () -> build(builder));
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
index 9c73f26..b5bb634 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.webkit.WebView;
+import androidx.annotation.IntDef;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RequiresOptIn;
import androidx.annotation.RestrictTo;
@@ -59,9 +60,29 @@
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
public @interface Experimental {}
+ /**
+ * Common configuration presets for WebView.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ Baseline.LEGACY,
+ })
+ public @interface Baseline {
+ /**
+ * Matches the configuration of a WebView created via the WebView constructor.
+ */
+ int LEGACY = 0;
+ }
+
@Nullable WebViewBuilderBoundaryInterface mBuilderStateBoundary;
- public WebViewBuilder() {}
+ public WebViewBuilder(@Baseline int baseline) {
+ if (baseline != Baseline.LEGACY) {
+ throw new IllegalArgumentException("Invalid baseline: " + baseline);
+ }
+ // TODO(crbug.com/419726203): We only have the no-op LEGACY baseline right now, so no logic
+ // consumes this argument, yet.
+ }
/**
* Restrict {@link WebView#addJavascriptInterface(Object, String)} and {@link
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index 7da0f71..0ad5c2e 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -55,8 +55,7 @@
commonTest {
dependencies {
- // https://youtrack.jetbrains.com/issue/KT-71032
- implementation(libs.kotlinTestForWasmTests)
+ implementation(libs.kotlinTest)
implementation(libs.kotlinTestAnnotationsCommon)
implementation(libs.kotlinCoroutinesCore)
}
@@ -67,32 +66,6 @@
implementation(libs.testRunner)
}
}
-
- jsMain {
- dependencies {
- implementation(libs.kotlinStdlibJs)
- }
- }
-
- jsTest {
- dependencies {
- implementation(libs.kotlinStdlibJs)
- implementation(libs.kotlinTestJs)
- }
- }
-
- wasmJsMain {
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- }
- }
-
- wasmJsTest {
- dependencies {
- implementation(libs.kotlinStdlibWasm)
- implementation(libs.kotlinTestWasm)
- }
- }
}
}
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 084179f..c562ade 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -57,7 +57,7 @@
implementation(project(":window:window-core"))
def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
- def extensions_version = "androidx.window.extensions:extensions:1.5.0-beta01"
+ def extensions_version = "androidx.window.extensions:extensions:1.5.0"
// A compile only dependency on extensions.core so that other libraries do not expose it
// transitively.
compileOnly(extensions_core_version)
diff --git a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
index d2db738..3d85d3e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
@@ -27,7 +27,6 @@
import androidx.window.SafeWindowExtensionsProvider
import androidx.window.WindowSdkExtensions
import androidx.window.core.ConsumerAdapter
-import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior
import androidx.window.extensions.WindowExtensions
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.extensions.core.util.function.Function
@@ -210,7 +209,6 @@
* - [ActivityStack.Token]
* - [WindowAttributes]
* - [SplitInfo.Token]
- * - [EmbeddingConfiguration.Builder.setDimAreaBehavior]
*/
@VisibleForTesting
internal fun hasValidVendorApiLevel5(): Boolean =
@@ -224,8 +222,7 @@
isClassAnimationBackgroundValid() &&
isClassActivityStackTokenValid() &&
isClassWindowAttributesValid() &&
- isClassSplitInfoTokenValid() &&
- isClassEmbeddingConfigurationBuilderApi5Valid()
+ isClassSplitInfoTokenValid()
/**
* Vendor API level 6 includes the following methods:
@@ -862,20 +859,6 @@
createFromBinder.isPublic && createFromBinder.doesReturn(splitInfoTokenClass)
}
- private fun isClassEmbeddingConfigurationBuilderApi5Valid(): Boolean =
- validateReflection("Class EmbeddingConfiguration.Builder is not valid") {
- val EmbeddingConfigurationBuilderClass = EmbeddingConfiguration.Builder::class.java
- val setAutoSaveEmbeddingStateMethod =
- EmbeddingConfigurationBuilderClass.getMethod(
- "setDimAreaBehavior",
- DimAreaBehavior::class.java,
- )
- setAutoSaveEmbeddingStateMethod.isPublic &&
- setAutoSaveEmbeddingStateMethod.doesReturn(
- EmbeddingConfiguration.Builder::class.java
- )
- }
-
/** Vendor API level 6 validation methods */
private fun isMethodGetEmbeddedActivityWindowInfoValid(): Boolean =
validateReflection(
@@ -1090,21 +1073,6 @@
setChangeAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java)
}
- /** Vendor API level 8 validation methods */
- private fun isClassEmbeddingConfigurationBuilderApi8Valid(): Boolean =
- validateReflection("Class EmbeddingConfiguration.Builder is not valid") {
- val EmbeddingConfigurationBuilderClass = EmbeddingConfiguration.Builder::class.java
- val setAutoSaveEmbeddingStateMethod =
- EmbeddingConfigurationBuilderClass.getMethod(
- "setAutoSaveEmbeddingState",
- Boolean::class.java,
- )
- setAutoSaveEmbeddingStateMethod.isPublic &&
- setAutoSaveEmbeddingStateMethod.doesReturn(
- EmbeddingConfiguration.Builder::class.java
- )
- }
-
/** Overlay features validation methods */
private fun isActivityStackGetTagValid(): Boolean =
validateReflection("ActivityStack#getTag is not valid") {
diff --git a/xr/compose/compose-testing/api/restricted_current.txt b/xr/compose/compose-testing/api/restricted_current.txt
index 48ca38c..a72c5ac 100644
--- a/xr/compose/compose-testing/api/restricted_current.txt
+++ b/xr/compose/compose-testing/api/restricted_current.txt
@@ -216,7 +216,7 @@
method @InaccessibleFromJava public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
method @InaccessibleFromJava public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
method @InaccessibleFromJava @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
- method @InaccessibleFromJava public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+ method @InaccessibleFromJava public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
method @InaccessibleFromJava public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
method @InaccessibleFromJava public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
method @InaccessibleFromJava public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
index 55abeb2e..e2e6a9a 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
@@ -130,23 +130,25 @@
public value class StereoMode private constructor(public val value: Int) {
public companion object {
/** Each eye will see the entire surface (no separation). */
- public val Mono: StereoMode = StereoMode(SurfaceEntity.StereoMode.MONO)
+ public val Mono: StereoMode = StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MONO)
/** The [top, bottom] halves of the surface will map to [left, right] eyes. */
- public val TopBottom: StereoMode = StereoMode(SurfaceEntity.StereoMode.TOP_BOTTOM)
+ public val TopBottom: StereoMode =
+ StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM)
/** The [left, right] halves of the surface will map to [left, right] eyes. */
- public val SideBySide: StereoMode = StereoMode(SurfaceEntity.StereoMode.SIDE_BY_SIDE)
+ public val SideBySide: StereoMode =
+ StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE)
/**
* For displaying mv-hevc video format, [base, secondary] view layers will map to
* [left, right] eyes.
*/
public val MultiviewLeftPrimary: StereoMode =
- StereoMode(SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY)
+ StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY)
/**
* For displaying mv-hevc video format, [base, secondary] view layers will map to
* [right, left] eyes.
*/
public val MultiviewRightPrimary: StereoMode =
- StereoMode(SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY)
+ StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY)
}
}
@@ -157,14 +159,14 @@
public companion object {
/** No security is applied. */
public val None: SurfaceProtection =
- SurfaceProtection(SurfaceEntity.ContentSecurityLevel.NONE)
+ SurfaceProtection(SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE)
/**
* Sets the underlying Surface to set the
* [android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT] flag. This is mainly used to
* protect DRM video content.
*/
public val Protected: SurfaceProtection =
- SurfaceProtection(SurfaceEntity.ContentSecurityLevel.PROTECTED)
+ SurfaceProtection(SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED)
}
}
@@ -226,7 +228,7 @@
SurfaceEntity.create(
session = checkNotNull(session) { "Session is required" },
stereoMode = stereoMode.value,
- contentSecurityLevel = surfaceProtection.value,
+ surfaceProtection = surfaceProtection.value,
),
localDensity = density,
)
@@ -391,12 +393,12 @@
SurfaceEntity.create(
session = checkNotNull(session) { "Session is required" },
stereoMode = stereoMode.value,
- contentSecurityLevel = surfaceProtection.value,
- canvasShape =
+ surfaceProtection = surfaceProtection.value,
+ shape =
if (isHemisphere) {
- SurfaceEntity.CanvasShape.Vr180Hemisphere(meterRadius)
+ SurfaceEntity.Shape.Hemisphere(meterRadius)
} else {
- SurfaceEntity.CanvasShape.Vr360Sphere(meterRadius)
+ SurfaceEntity.Shape.Sphere(meterRadius)
},
),
headPose,
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
index 1a1beb8..1a9d861 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
@@ -585,7 +585,7 @@
SideEffect { corePanelEntity.setShape(shape, density) }
- LaunchedEffect(intent) { corePanelEntity.launchActivity(intent) }
+ LaunchedEffect(intent) { corePanelEntity.startActivity(intent) }
SpatialBox {
SubspaceLayout(modifier = finalModifier, coreEntity = corePanelEntity) { _, constraints ->
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
index 27446dc..9fee0b0 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
@@ -30,6 +30,7 @@
import androidx.xr.compose.unit.IntVolumeSize
import androidx.xr.compose.unit.Meter
import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.IntSize2d
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Vector3
@@ -295,8 +296,8 @@
internal class CoreActivityPanelEntity(private val activityPanelEntity: ActivityPanelEntity) :
CoreBasePanelEntity(activityPanelEntity) {
- fun launchActivity(intent: Intent, bundle: Bundle? = null) {
- activityPanelEntity.launchActivity(intent, bundle)
+ fun startActivity(intent: Intent, bundle: Bundle? = null) {
+ activityPanelEntity.startActivity(intent, bundle)
}
}
@@ -351,10 +352,12 @@
set(value) {
if (super.size != value) {
super.size = value
- surfaceEntity.canvasShape =
- SurfaceEntity.CanvasShape.Quad(
- Meter.fromPixel(size.width.toFloat(), localDensity).value,
- Meter.fromPixel(size.height.toFloat(), localDensity).value,
+ surfaceEntity.shape =
+ SurfaceEntity.Shape.Quad(
+ FloatSize2d(
+ Meter.fromPixel(size.width.toFloat(), localDensity).value,
+ Meter.fromPixel(size.height.toFloat(), localDensity).value,
+ )
)
updateFeathering()
}
@@ -377,8 +380,8 @@
private fun updateFeathering() {
(currentFeatheringEffect as? SpatialSmoothFeatheringEffect)?.let {
- surfaceEntity.edgeFeather =
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+ surfaceEntity.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
it.size.toWidthPercent(size.width.toFloat(), localDensity),
it.size.toHeightPercent(size.height.toFloat(), localDensity),
)
@@ -463,23 +466,23 @@
/** Radius in meters. */
internal var radius: Float
- get() = radiusFromShape(surfaceEntity.canvasShape)
+ get() = radiusFromShape(surfaceEntity.shape)
set(value) {
- val shape = surfaceEntity.canvasShape
+ val shape = surfaceEntity.shape
if (value != radiusFromShape(shape)) {
- if (shape is SurfaceEntity.CanvasShape.Vr180Hemisphere) {
- surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(value)
+ if (shape is SurfaceEntity.Shape.Hemisphere) {
+ surfaceEntity.shape = SurfaceEntity.Shape.Hemisphere(value)
} else {
- surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(value)
+ surfaceEntity.shape = SurfaceEntity.Shape.Sphere(value)
}
updateFeathering()
}
}
- private fun radiusFromShape(shape: SurfaceEntity.CanvasShape): Float {
- if (shape is SurfaceEntity.CanvasShape.Vr180Hemisphere) {
+ private fun radiusFromShape(shape: SurfaceEntity.Shape): Float {
+ if (shape is SurfaceEntity.Shape.Hemisphere) {
return shape.radius
- } else if (shape is SurfaceEntity.CanvasShape.Vr360Sphere) {
+ } else if (shape is SurfaceEntity.Shape.Sphere) {
return shape.radius
}
throw IllegalStateException("Shape must be spherical")
@@ -501,31 +504,28 @@
return
}
- surfaceEntity.edgeFeather =
+ surfaceEntity.edgeFeatheringParams =
if (!isBoundaryAvailable) {
val radius = if (isHemisphere) 0.5f else 0.7f
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(radius, radius)
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(radius, radius)
} else {
val semicircleArcLength = Meter((radius * PI).toFloat()).toPx(localDensity)
(currentFeatheringEffect as? SpatialSmoothFeatheringEffect)?.let {
val radiusX =
it.size.toWidthPercent(
- if (
- surfaceEntity.canvasShape
- is SurfaceEntity.CanvasShape.Vr180Hemisphere
- )
+ if (surfaceEntity.shape is SurfaceEntity.Shape.Hemisphere)
semicircleArcLength / 2
else semicircleArcLength,
localDensity,
)
val radiusY = it.size.toHeightPercent(semicircleArcLength, localDensity)
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(radiusX, radiusY)
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(radiusX, radiusY)
}
- } ?: surfaceEntity.edgeFeather
+ } ?: surfaceEntity.edgeFeatheringParams
}
private val isHemisphere
- get() = surfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Vr180Hemisphere
+ get() = surfaceEntity.shape is SurfaceEntity.Shape.Hemisphere
}
/** [CoreEntity] types that implement this interface may have the ResizableComponent attached. */
diff --git a/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt b/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
index 7d20f88..c934956 100644
--- a/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
+++ b/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
@@ -101,6 +101,7 @@
import androidx.xr.compose.subspace.layout.width
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Quaternion
@@ -688,15 +689,16 @@
surfaceEntity =
SurfaceEntity.create(
session = session,
- stereoMode = SurfaceEntity.StereoMode.MONO,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
pose =
Pose(
Vector3(0f, -0.45f, 0f),
rotation = Quaternion(0.0f, 0.0f, 0.0f, 1.0f),
),
- contentSecurityLevel =
- if (useDrmState.value) SurfaceEntity.ContentSecurityLevel.PROTECTED
- else SurfaceEntity.ContentSecurityLevel.NONE,
+ surfaceProtection =
+ if (useDrmState.value)
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
+ else SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE,
)
// Make the video player movable (to make it easier to look at it from different
// angles and distances)
@@ -720,10 +722,9 @@
if (height > 0 && width > 0) {
var dimensions =
getCanvasAspectRatio(surfaceEntity!!.stereoMode, width, height)
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Quad(
- dimensions.width,
- dimensions.height,
+ surfaceEntity!!.shape =
+ SurfaceEntity.Shape.Quad(
+ FloatSize2d(dimensions.width, dimensions.height)
)
// Resize the MovableComponent to match the canvas dimensions.
@@ -865,39 +866,39 @@
1.0f
}
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Quad(1.0f, canvasHeight)
+ surfaceEntity!!.shape =
+ SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, canvasHeight))
}
) {
Text(text = "Set Quad", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(5.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f) }) {
Text(text = "Set Vr360", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Vr180Hemisphere(5.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f) }) {
Text(text = "Set Vr180", fontSize = 10.sp)
}
} // end row
Row(verticalAlignment = Alignment.CenterVertically) {
- Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+ Button(
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+ }
+ ) {
Text(text = "Mono", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+ }
) {
Text(text = "Top-Bottom", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+ onClick = {
+ surfaceEntity!!.stereoMode =
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+ }
) {
Text(text = "Side-by-Side", fontSize = 10.sp)
}
@@ -918,11 +919,11 @@
fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): FloatSize3d {
when (stereoMode) {
- SurfaceEntity.StereoMode.MONO ->
+ SurfaceEntity.StereoMode.STEREO_MODE_MONO ->
return FloatSize3d(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.TOP_BOTTOM ->
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
return FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
return FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
}
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
index 1d78d2c..d35320f 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
@@ -64,6 +64,7 @@
import androidx.xr.compose.unit.DpVolumeSize
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Vector3
import androidx.xr.scenecore.AnchorEntity
@@ -241,27 +242,25 @@
var surfaceEntity by remember { mutableStateOf<SurfaceEntity?>(null) }
val radius = 0.65f
var shape by remember {
- mutableStateOf<SurfaceEntity.CanvasShape>(
- SurfaceEntity.CanvasShape.Vr180Hemisphere(radius)
- )
+ mutableStateOf<SurfaceEntity.Shape>(SurfaceEntity.Shape.Hemisphere(radius))
}
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Column {
LabeledRadioButton(
"Quad",
- shape is SurfaceEntity.CanvasShape.Quad,
- { shape = SurfaceEntity.CanvasShape.Quad(radius * 2f, radius * 2f) },
+ shape is SurfaceEntity.Shape.Quad,
+ { shape = SurfaceEntity.Shape.Quad(FloatSize2d(radius * 2f, radius * 2f)) },
)
LabeledRadioButton(
- "Vr180Hemisphere",
- shape is SurfaceEntity.CanvasShape.Vr180Hemisphere,
- { shape = SurfaceEntity.CanvasShape.Vr180Hemisphere(radius) },
+ "Hemisphere",
+ shape is SurfaceEntity.Shape.Hemisphere,
+ { shape = SurfaceEntity.Shape.Hemisphere(radius) },
)
LabeledRadioButton(
- "Vr360Sphere",
- shape is SurfaceEntity.CanvasShape.Vr360Sphere,
- { shape = SurfaceEntity.CanvasShape.Vr360Sphere(radius) },
+ "Sphere",
+ shape is SurfaceEntity.Shape.Sphere,
+ { shape = SurfaceEntity.Shape.Sphere(radius) },
)
}
Button({
@@ -270,7 +269,7 @@
SurfaceEntity.create(
session,
pose = Pose(Vector3(-1f, 0f, -0.5f)),
- canvasShape = shape,
+ shape = shape,
)
surfaceEntity?.contentDescription = "${shape.javaClass.simpleName} Surface"
}
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
index bb1471b..865c888 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
@@ -105,6 +105,7 @@
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
import androidx.xr.runtime.internal.Dimensions
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Quaternion
@@ -794,9 +795,9 @@
onClick = {
surfaceEntity =
SurfaceEntity.create(
- session,
- SurfaceEntity.StereoMode.TOP_BOTTOM,
- Pose(Vector3(0f, -0.45f, 0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
+ session = session,
+ pose = Pose(Vector3(0f, -0.45f, 0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
)
// Make the video player movable (to make it easier to look at it from different
// angles and distances)
@@ -820,8 +821,8 @@
// Resize the canvas to match the video aspect ratio - accounting for the stereo
// mode.
var dimensions = getCanvasAspectRatio(surfaceEntity!!.stereoMode, width, height)
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+ surfaceEntity!!.shape =
+ SurfaceEntity.Shape.Quad(FloatSize2d(dimensions.width, dimensions.height))
// Resize the MovableComponent to match the canvas dimensions.
movableComponent!!.size = surfaceEntity!!.dimensions
@@ -949,39 +950,39 @@
1.0f
}
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Quad(1.0f, canvasHeight)
+ surfaceEntity!!.shape =
+ SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, canvasHeight))
}
) {
Text(text = "Set Quad", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(5.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(5.0f) }) {
Text(text = "Set Vr360", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Vr180Hemisphere(5.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(5.0f) }) {
Text(text = "Set Vr180", fontSize = 10.sp)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
- Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+ Button(
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+ }
+ ) {
Text(text = "Mono", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+ }
) {
Text(text = "Top-Bottom", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+ onClick = {
+ surfaceEntity!!.stereoMode =
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+ }
) {
Text(text = "Side-by-Side", fontSize = 10.sp)
}
@@ -1002,11 +1003,11 @@
fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): Dimensions {
when (stereoMode) {
- SurfaceEntity.StereoMode.MONO ->
+ SurfaceEntity.StereoMode.STEREO_MODE_MONO ->
return Dimensions(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.TOP_BOTTOM ->
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
return Dimensions(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
return Dimensions(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
}
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
index e27847a..22f7b07 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
@@ -85,6 +85,7 @@
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
import androidx.xr.runtime.internal.Dimensions
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.IntSize2d
import androidx.xr.runtime.math.Pose
@@ -238,20 +239,20 @@
val effectiveDisplayWidth = videoWidth.toFloat() * pixelAspectRatio
return when (stereoMode) {
- SurfaceEntity.StereoMode.MONO,
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
- SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+ SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
FloatSize3d(1.0f, videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
- SurfaceEntity.StereoMode.TOP_BOTTOM ->
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
- SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
}
}
private fun quad() {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+ surfaceEntity!!.shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
// Move the Quad-shaped canvas to a spot in front of the User.
surfaceEntity!!.setPose(
session.scene.spatialUser.head?.transformPoseTo(
@@ -476,8 +477,8 @@
value = featherRadiusX,
onValueChange = {
featherRadiusX = it
- surfaceEntity!!.edgeFeather =
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+ surfaceEntity!!.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
featherRadiusX,
featherRadiusY,
)
@@ -489,8 +490,8 @@
value = featherRadiusY,
onValueChange = {
featherRadiusY = it
- surfaceEntity!!.edgeFeather =
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+ surfaceEntity!!.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
featherRadiusX,
featherRadiusY,
)
@@ -554,12 +555,11 @@
ApiButton("Set Quad", modifier) { quad() }
ApiButton("Set Vr360", modifier) {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
+ surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f)
}
ApiButton("Set Vr180", modifier) {
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f)
+ surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f)
}
}
@@ -568,15 +568,16 @@
ApiRow {
val modifier = Modifier.weight(1F)
ApiButton("Mono", modifier) {
- surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
}
ApiButton("Top-Bottom", modifier) {
- surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
}
ApiButton("Side-by-Side", modifier) {
- surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE
+ surfaceEntity!!.stereoMode =
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
}
}
}
@@ -637,20 +638,26 @@
private fun createSurfaceEntity(
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
+ canvasShape: SurfaceEntity.Shape,
protected: Boolean = false,
) {
// Create SurfaceEntity and MovableComponent if they don't exist.
if (surfaceEntity == null) {
val surfaceContentLevel =
if (protected) {
- SurfaceEntity.ContentSecurityLevel.PROTECTED
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
} else {
- SurfaceEntity.ContentSecurityLevel.NONE
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
}
surfaceEntity =
- SurfaceEntity.create(session, stereoMode, pose, canvasShape, surfaceContentLevel)
+ SurfaceEntity.create(
+ session = session,
+ pose = pose,
+ shape = canvasShape,
+ stereoMode = stereoMode,
+ surfaceProtection = surfaceContentLevel,
+ )
// Make the video player movable (to make it easier to look at it from different
// angles and distances)
movableComponent = MovableComponent.createSystemMovable(session)
@@ -695,7 +702,7 @@
private fun setupExoPlayer(
videoUri: String,
stereoMode: Int,
- canvasShape: SurfaceEntity.CanvasShape,
+ canvasShape: SurfaceEntity.Shape,
protected: Boolean,
) {
val drmLicenseUrl = "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
@@ -737,9 +744,11 @@
getCanvasAspectRatio(stereoMode, width, height, currentPixelAspectRatio)
// Set the dimensions of the Quad canvas to the video dimensions and attach the
// a MovableComponent.
- if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
- surfaceEntity?.canvasShape =
- SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+ if (canvasShape is SurfaceEntity.Shape.Quad) {
+ surfaceEntity?.shape =
+ SurfaceEntity.Shape.Quad(
+ FloatSize2d(dimensions.width, dimensions.height)
+ )
movableComponent?.size =
(surfaceEntity?.dimensions ?: Dimensions(1.0f, 1.0f, 1.0f))
as FloatSize3d
@@ -803,7 +812,7 @@
companion object {
val defaultPose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f))
- val defaultCanvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+ val defaultShape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
var videoAttributesMap: IntObjectMap<VideoAttributes> =
MutableIntObjectMap<VideoAttributes>(9).apply {
put(
@@ -811,10 +820,10 @@
VideoAttributes(
"Play Big Buck Bunny",
"/Download/vid_bigbuckbunny.mp4",
- SurfaceEntity.StereoMode.TOP_BOTTOM,
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
false,
defaultPose,
- defaultCanvasShape,
+ defaultShape,
),
)
put(
@@ -822,10 +831,10 @@
VideoAttributes(
"Play MVHEVC Left Primary",
"/Download/mvhevc_flat_left_primary_1080.mov",
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
false,
defaultPose,
- defaultCanvasShape,
+ defaultShape,
),
)
put(
@@ -833,10 +842,10 @@
VideoAttributes(
"Play MVHEVC Right Primary",
"/Download/mvhevc_flat_right_primary_1080.mov",
- SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
false,
defaultPose,
- defaultCanvasShape,
+ defaultShape,
),
)
put(
@@ -844,10 +853,10 @@
VideoAttributes(
"Play Naver 180 (Side-by-Side)",
"/Download/Naver180.mp4",
- SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
false,
defaultPose,
- SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+ SurfaceEntity.Shape.Hemisphere(1.0f),
),
)
put(
@@ -855,10 +864,10 @@
VideoAttributes(
"Play Naver 180 (MV-HEVC)",
"/Download/Naver180_MV-HEVC.mp4",
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
false,
defaultPose,
- SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+ SurfaceEntity.Shape.Hemisphere(1.0f),
),
)
put(
@@ -866,10 +875,10 @@
VideoAttributes(
"Play Galaxy 360 (Top-Bottom)",
"/Download/Galaxy11_VR_3D360.mp4",
- SurfaceEntity.StereoMode.TOP_BOTTOM,
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
false,
defaultPose,
- SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+ SurfaceEntity.Shape.Sphere(1.0f),
),
)
put(
@@ -877,10 +886,10 @@
VideoAttributes(
"Play Galaxy 360 (MV-HEVC)",
"/Download/Galaxy11_VR_3D360_MV-HEVC.mp4",
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
false,
defaultPose,
- SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+ SurfaceEntity.Shape.Sphere(1.0f),
),
)
put(
@@ -888,10 +897,10 @@
VideoAttributes(
"Play DRM Protected For Bigger Blazes",
"/Download/sdr_singleview_protected.mp4",
- SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
true,
defaultPose,
- defaultCanvasShape,
+ defaultShape,
),
)
put(
@@ -899,10 +908,10 @@
VideoAttributes(
"Play DRM Protected MVHEVC Left Primary",
"/Download/mvhevc_flat_left_primary_1080_protected.mp4",
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
true,
defaultPose,
- defaultCanvasShape,
+ defaultShape,
),
)
}
@@ -926,6 +935,6 @@
val stereoMode: Int,
val protected: Boolean,
var pose: Pose,
- var canvasShape: SurfaceEntity.CanvasShape,
+ var canvasShape: SurfaceEntity.Shape,
)
}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
index 7650940..8d9a7c41 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
@@ -22,6 +22,8 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.ExperimentalMaterial3ComponentOverrideApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.LocalContentColor
@@ -44,6 +46,7 @@
import androidx.xr.compose.spatial.ContentEdge
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.OrbiterOffsetType
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
/**
* <a href="https://m3.material.io/components/navigation-rail/overview" class="external"
@@ -84,30 +87,34 @@
header: @Composable (ColumnScope.() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
- val orbiterProperties = LocalNavigationRailOrbiterProperties.current
+ val orbiterProperties =
+ LocalNavigationRailOrbiterProperties.current.copy(
+ shape = SpatialRoundedCornerShape(CornerSize(percent = 0))
+ )
VerticalOrbiter(orbiterProperties) {
- Surface(color = containerColor, contentColor = contentColor, modifier = modifier) {
- Column(
- // XR-changed: Original NavigationRail uses fillMaxHeight() and windowInsets,
- // which do not produce the desired result in XR.
- Modifier.widthIn(min = XrNavigationRailTokens.ContainerWidth)
- .padding(vertical = XrNavigationRailTokens.VerticalPadding)
- .selectableGroup(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
- content = content,
- )
- }
- }
- // Header goes inside a separate top-aligned Orbiter without an outline shape, as this is
- // generally a FAB.
- if (header != null) {
- VerticalOrbiter(orbiterProperties.copy(alignment = Alignment.Top)) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
- content = header,
- )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+ ) {
+ header?.let { it() }
+ Surface(
+ shape = CircleShape,
+ color = containerColor,
+ contentColor = contentColor,
+ modifier = modifier,
+ ) {
+ Column(
+ // XR-changed: Original NavigationRail uses fillMaxHeight() and windowInsets,
+ // which do not produce the desired result in XR.
+ Modifier.widthIn(min = XrNavigationRailTokens.ContainerWidth)
+ .padding(vertical = XrNavigationRailTokens.VerticalPadding)
+ .selectableGroup(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement =
+ Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+ content = content,
+ )
+ }
}
}
}
diff --git a/xr/runtime/runtime-testing/api/restricted_current.txt b/xr/runtime/runtime-testing/api/restricted_current.txt
index 4f8d3cb..f682fb9 100644
--- a/xr/runtime/runtime-testing/api/restricted_current.txt
+++ b/xr/runtime/runtime-testing/api/restricted_current.txt
@@ -190,7 +190,7 @@
method public androidx.xr.runtime.testing.FakeResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
method public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
- method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+ method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
@@ -584,6 +584,11 @@
method public void dispose();
method public java.util.List<androidx.xr.runtime.testing.FakeRenderingRuntime.FakeKhronosPbrMaterial> getCreatedKhronosPbrMaterials();
method public java.util.List<androidx.xr.runtime.testing.FakeRenderingRuntime.FakeWaterMaterial> getCreatedWaterMaterials();
+ method public androidx.xr.runtime.internal.TextureResource? getReflectionTextureFromIbl(androidx.xr.runtime.internal.ExrImageResource iblToken);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByAssetName(String assetName);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByByteArray(byte[] assetData, String assetKey);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByAssetName(String assetName);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByByteArray(byte[] assetData, String assetKey);
method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.TextureResource> loadTexture(String assetName, androidx.xr.runtime.internal.TextureSampler sampler);
method public void setAlphaCutoffOnKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material, float alphaCutoff);
method public void setAlphaMapOnWaterMaterial(androidx.xr.runtime.internal.MaterialResource material, androidx.xr.runtime.internal.TextureResource alphaMap);
@@ -1103,7 +1108,6 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeSurfaceEntity extends androidx.xr.runtime.testing.FakeEntity implements androidx.xr.runtime.internal.SurfaceEntity {
ctor public FakeSurfaceEntity();
method public androidx.xr.runtime.internal.TextureResource? getAuxiliaryAlphaMask();
- method public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape getCanvasShape();
method public int getColorRange();
method public int getColorSpace();
method public int getColorTransfer();
@@ -1111,24 +1115,24 @@
method public androidx.xr.runtime.internal.Dimensions getDimensions();
method public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather getEdgeFeather();
method public boolean getMContentColorMetadataSet();
- method public int getMaxCLL();
+ method public int getMaxContentLightLevel();
method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolution();
method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolutionResult();
method public androidx.xr.runtime.internal.TextureResource? getPrimaryAlphaMask();
+ method public androidx.xr.runtime.internal.SurfaceEntity.Shape getShape();
method public int getStereoMode();
method public android.view.Surface getSurface();
method public void resetContentColorMetadata();
method public void setAuxiliaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
- method public void setCanvasShape(androidx.xr.runtime.internal.SurfaceEntity.CanvasShape);
- method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxCLL);
+ method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxContentLightLevel);
method public void setEdgeFeather(androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather);
method public void setMContentColorMetadataSet(boolean);
method public void setPerceivedResolutionResult(androidx.xr.runtime.internal.PerceivedResolutionResult);
method public void setPrimaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
+ method public void setShape(androidx.xr.runtime.internal.SurfaceEntity.Shape);
method public void setStereoMode(int);
method public void setSurface(android.view.Surface surface);
property public androidx.xr.runtime.internal.TextureResource? auxiliaryAlphaMask;
- property public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape;
property public int colorRange;
property public int colorSpace;
property public int colorTransfer;
@@ -1136,9 +1140,10 @@
property public androidx.xr.runtime.internal.Dimensions dimensions;
property public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather edgeFeather;
property public boolean mContentColorMetadataSet;
- property public int maxCLL;
+ property public int maxContentLightLevel;
property public androidx.xr.runtime.internal.PerceivedResolutionResult perceivedResolutionResult;
property public androidx.xr.runtime.internal.TextureResource? primaryAlphaMask;
+ property public androidx.xr.runtime.internal.SurfaceEntity.Shape shape;
property public int stereoMode;
property public android.view.Surface surface;
}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
index d229aa3..e743016 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
@@ -549,15 +549,15 @@
override fun createSurfaceEntity(
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
- contentSecurityLevel: Int,
+ shape: SurfaceEntity.Shape,
+ surfaceProtection: Int,
superSampling: Int,
parentEntity: Entity,
): SurfaceEntity {
val surfaceEntity = FakeSurfaceEntity()
surfaceEntity.stereoMode = stereoMode
surfaceEntity.setPose(pose)
- surfaceEntity.canvasShape = canvasShape
+ surfaceEntity.shape = shape
surfaceEntity.parent = parentEntity
return surfaceEntity
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
index 8375286..2da5d02 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
@@ -18,6 +18,8 @@
import android.app.Activity
import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.ExrImageResource
+import androidx.xr.runtime.internal.GltfModelResource
import androidx.xr.runtime.internal.KhronosPbrMaterialSpec
import androidx.xr.runtime.internal.MaterialResource
import androidx.xr.runtime.internal.RenderingRuntime
@@ -38,6 +40,26 @@
private val activity: Activity,
) : RenderingRuntime {
@Suppress("AsyncSuffixFuture")
+ override fun loadGltfByAssetName(assetName: String): ListenableFuture<GltfModelResource> =
+ immediateFuture(FakeGltfModelResource(0))
+
+ @Suppress("AsyncSuffixFuture")
+ override fun loadGltfByByteArray(
+ assetData: ByteArray,
+ assetKey: String,
+ ): ListenableFuture<GltfModelResource> = immediateFuture(FakeGltfModelResource(0))
+
+ @Suppress("AsyncSuffixFuture")
+ override fun loadExrImageByAssetName(assetName: String): ListenableFuture<ExrImageResource> =
+ immediateFuture(FakeExrImageResource(0))
+
+ @Suppress("AsyncSuffixFuture")
+ override fun loadExrImageByByteArray(
+ assetData: ByteArray,
+ assetKey: String,
+ ): ListenableFuture<ExrImageResource> = immediateFuture(FakeExrImageResource(1))
+
+ @Suppress("AsyncSuffixFuture")
override fun loadTexture(
assetName: String,
sampler: TextureSampler,
@@ -64,6 +86,10 @@
reflectionTexture = null
}
+ override fun getReflectionTextureFromIbl(iblToken: ExrImageResource): TextureResource? {
+ return reflectionTexture
+ }
+
/**
* For test purposes only.
*
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
index 07c7b95..4644f5d 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
@@ -23,8 +23,9 @@
import androidx.xr.runtime.internal.Dimensions
import androidx.xr.runtime.internal.PerceivedResolutionResult
import androidx.xr.runtime.internal.SurfaceEntity
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape
+import androidx.xr.runtime.internal.SurfaceEntity.Shape
import androidx.xr.runtime.internal.TextureResource
+import androidx.xr.runtime.math.FloatSize2d
/**
* Test-only implementation of [SurfaceEntity].
@@ -45,7 +46,7 @@
override var stereoMode: Int = SurfaceEntity.StereoMode.SIDE_BY_SIDE
/** Specifies the shape of the spatial canvas which the surface is texture mapped to. */
- override var canvasShape: CanvasShape = CanvasShape.Quad(0f, 0f)
+ override var shape: Shape = Shape.Quad(FloatSize2d(0f, 0f))
/**
* Retrieves the dimensions of the "spatial canvas" which the surface is mapped to. These values
@@ -54,7 +55,7 @@
* @return The canvas [Dimensions].
*/
override val dimensions: Dimensions
- get() = canvasShape.dimensions
+ get() = shape.dimensions
private var _surface: Surface =
ImageReader.newInstance(1, 1, ImageFormat.YUV_420_888, 1).surface
@@ -184,14 +185,14 @@
override val colorRange: Int
get() = _colorRange
- private var _maxCLL: Int = 0
+ private var _maxContentLightLevel: Int = 0
/**
* The active maximum content light level (MaxCLL) in nits. A value of 0 indicates that MaxCLL
* is not set or is unknown. This value is used if [contentColorMetadataSet] is `true`.
*/
- override val maxCLL: Int
- get() = _maxCLL
+ override val maxContentLightLevel: Int
+ get() = _maxContentLightLevel
/**
* Sets the explicit color information for the surface content. This will also set
@@ -207,12 +208,12 @@
colorSpace: Int,
colorTransfer: Int,
colorRange: Int,
- maxCLL: Int,
+ maxContentLightLevel: Int,
) {
_colorSpace = colorSpace
_colorTransfer = colorTransfer
_colorRange = colorRange
- _maxCLL = maxCLL
+ _maxContentLightLevel = maxContentLightLevel
}
/**
@@ -224,7 +225,7 @@
_colorSpace = SurfaceEntity.ColorSpace.BT709
_colorTransfer = SurfaceEntity.ColorTransfer.LINEAR
_colorRange = SurfaceEntity.ColorRange.FULL
- _maxCLL = 0
+ _maxContentLightLevel = 0
}
/**
@@ -232,5 +233,5 @@
*
* @throws IllegalStateException if the Entity has been disposed.
*/
- override var edgeFeather: SurfaceEntity.EdgeFeather = SurfaceEntity.EdgeFeather.SolidEdge()
+ override var edgeFeather: SurfaceEntity.EdgeFeather = SurfaceEntity.EdgeFeather.NoFeathering()
}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
index 1ec3d63..f8fd565 100644
--- a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
@@ -281,7 +281,7 @@
fun createSurfaceEntity_returnsInitialValue() {
val stereoMode = 0
val pose = Pose.Identity
- val canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
+ val canvasShape = SurfaceEntity.Shape.Sphere(1.0f)
val contentSecurityLevel = 0
val superSampling = 0
val parentEntity = FakeEntity()
@@ -298,7 +298,7 @@
assertThat(surfaceEntity).isInstanceOf(FakeSurfaceEntity::class.java)
assertThat(surfaceEntity.stereoMode).isEqualTo(stereoMode)
assertThat(surfaceEntity.getPose()).isEqualTo(pose)
- assertThat(surfaceEntity.canvasShape).isEqualTo(canvasShape)
+ assertThat(surfaceEntity.shape).isEqualTo(canvasShape)
assertThat(surfaceEntity.parent).isEqualTo(parentEntity)
}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
index 7208b67..9e815fd 100644
--- a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
@@ -22,6 +22,7 @@
import androidx.xr.runtime.internal.PixelDimensions
import androidx.xr.runtime.internal.SurfaceEntity
import androidx.xr.runtime.internal.TextureResource
+import androidx.xr.runtime.math.FloatSize2d
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -31,7 +32,7 @@
@RunWith(RobolectricTestRunner::class)
class FakeSurfaceEntityTest {
- val testCanvasShape = SurfaceEntity.CanvasShape.Quad(1f, 1f)
+ val testCanvasShape = SurfaceEntity.Shape.Quad(FloatSize2d(1f, 1f))
lateinit var underTest: FakeSurfaceEntity
@@ -57,7 +58,7 @@
@Test
fun getDimensions_setCanvasShape_returnsCanvasShapeDimensions() {
- underTest.canvasShape = testCanvasShape
+ underTest.shape = testCanvasShape
assertThat(underTest.dimensions).isEqualTo(testCanvasShape.dimensions)
}
@@ -123,7 +124,7 @@
check(underTest.colorSpace == SurfaceEntity.ColorSpace.BT709)
check(underTest.colorTransfer == SurfaceEntity.ColorTransfer.LINEAR)
check(underTest.colorRange == SurfaceEntity.ColorRange.FULL)
- check(underTest.maxCLL == 0)
+ check(underTest.maxContentLightLevel == 0)
underTest.setContentColorMetadata(
SurfaceEntity.ColorSpace.BT2020,
@@ -135,7 +136,7 @@
assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT2020)
assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.SRGB)
assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.LIMITED)
- assertThat(underTest.maxCLL).isEqualTo(100)
+ assertThat(underTest.maxContentLightLevel).isEqualTo(100)
}
@Test
@@ -150,13 +151,13 @@
assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT2020)
assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.SRGB)
assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.LIMITED)
- assertThat(underTest.maxCLL).isEqualTo(100)
+ assertThat(underTest.maxContentLightLevel).isEqualTo(100)
underTest.resetContentColorMetadata()
assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT709)
assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.LINEAR)
assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.FULL)
- assertThat(underTest.maxCLL).isEqualTo(0)
+ assertThat(underTest.maxContentLightLevel).isEqualTo(0)
}
}
diff --git a/xr/runtime/runtime/api/restricted_current.txt b/xr/runtime/runtime/api/restricted_current.txt
index af071ff..c9a3e97 100644
--- a/xr/runtime/runtime/api/restricted_current.txt
+++ b/xr/runtime/runtime/api/restricted_current.txt
@@ -1025,7 +1025,7 @@
method public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
- method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+ method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
@@ -1428,6 +1428,11 @@
method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
method public void destroyWaterMaterial(androidx.xr.runtime.internal.MaterialResource material);
method public void dispose();
+ method public androidx.xr.runtime.internal.TextureResource? getReflectionTextureFromIbl(androidx.xr.runtime.internal.ExrImageResource iblToken);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByAssetName(String assetName);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByByteArray(byte[] assetData, String assetKey);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByAssetName(String assetName);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByByteArray(byte[] assetData, String assetKey);
method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.TextureResource> loadTexture(String assetName, androidx.xr.runtime.internal.TextureSampler sampler);
method public void setAlphaCutoffOnKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material, float alphaCutoff);
method public void setAlphaMapOnWaterMaterial(androidx.xr.runtime.internal.MaterialResource material, androidx.xr.runtime.internal.TextureResource alphaMap);
@@ -1747,67 +1752,36 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SurfaceEntity extends androidx.xr.runtime.internal.Entity {
- method public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape getCanvasShape();
method public int getColorRange();
method public int getColorSpace();
method public int getColorTransfer();
method public boolean getContentColorMetadataSet();
method public androidx.xr.runtime.internal.Dimensions getDimensions();
method public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather getEdgeFeather();
- method public int getMaxCLL();
+ method public int getMaxContentLightLevel();
method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolution();
+ method public androidx.xr.runtime.internal.SurfaceEntity.Shape getShape();
method public int getStereoMode();
method public android.view.Surface getSurface();
method public void resetContentColorMetadata();
method public void setAuxiliaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
- method public void setCanvasShape(androidx.xr.runtime.internal.SurfaceEntity.CanvasShape);
- method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxCLL);
+ method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxContentLightLevel);
method public void setEdgeFeather(androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather);
method public void setPrimaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
+ method public void setShape(androidx.xr.runtime.internal.SurfaceEntity.Shape);
method public void setStereoMode(int);
- property public abstract androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape;
property public abstract int colorRange;
property public abstract int colorSpace;
property public abstract int colorTransfer;
property public abstract boolean contentColorMetadataSet;
property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
property public abstract androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather edgeFeather;
- property public abstract int maxCLL;
+ property public abstract int maxContentLightLevel;
+ property public abstract androidx.xr.runtime.internal.SurfaceEntity.Shape shape;
property public abstract int stereoMode;
property public abstract android.view.Surface surface;
}
- public static interface SurfaceEntity.CanvasShape {
- method public androidx.xr.runtime.internal.Dimensions getDimensions();
- property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
- }
-
- public static final class SurfaceEntity.CanvasShape.Quad implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Quad(float width, float height);
- method public androidx.xr.runtime.internal.Dimensions getDimensions();
- method public float getHeight();
- method public float getWidth();
- property public androidx.xr.runtime.internal.Dimensions dimensions;
- property public float height;
- property public float width;
- }
-
- public static final class SurfaceEntity.CanvasShape.Vr180Hemisphere implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Vr180Hemisphere(float radius);
- method public androidx.xr.runtime.internal.Dimensions getDimensions();
- method public float getRadius();
- property public androidx.xr.runtime.internal.Dimensions dimensions;
- property public float radius;
- }
-
- public static final class SurfaceEntity.CanvasShape.Vr360Sphere implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Vr360Sphere(float radius);
- method public androidx.xr.runtime.internal.Dimensions getDimensions();
- method public float getRadius();
- property public androidx.xr.runtime.internal.Dimensions dimensions;
- property public float radius;
- }
-
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.ColorRange {
field public static final androidx.xr.runtime.internal.SurfaceEntity.ColorRange.Companion Companion;
field public static final int FULL = 1; // 0x1
@@ -1874,32 +1848,48 @@
field public static final int ST2084 = 6; // 0x6
}
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.ContentSecurityLevel {
- field public static final androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel.Companion Companion;
- field public static final int NONE = 0; // 0x0
- field public static final int PROTECTED = 1; // 0x1
- }
-
- public static final class SurfaceEntity.ContentSecurityLevel.Companion {
- property public static int NONE;
- property public static int PROTECTED;
- field public static final int NONE = 0; // 0x0
- field public static final int PROTECTED = 1; // 0x1
- }
-
public static interface SurfaceEntity.EdgeFeather {
}
- public static final class SurfaceEntity.EdgeFeather.SmoothFeather implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
- ctor public SurfaceEntity.EdgeFeather.SmoothFeather(float leftRight, float topBottom);
+ public static final class SurfaceEntity.EdgeFeather.NoFeathering implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
+ ctor public SurfaceEntity.EdgeFeather.NoFeathering();
+ }
+
+ public static final class SurfaceEntity.EdgeFeather.RectangleFeather implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
+ ctor public SurfaceEntity.EdgeFeather.RectangleFeather(float leftRight, float topBottom);
method public float getLeftRight();
method public float getTopBottom();
property public float leftRight;
property public float topBottom;
}
- public static final class SurfaceEntity.EdgeFeather.SolidEdge implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
- ctor public SurfaceEntity.EdgeFeather.SolidEdge();
+ public static interface SurfaceEntity.Shape {
+ method public androidx.xr.runtime.internal.Dimensions getDimensions();
+ property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
+ }
+
+ public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+ method public androidx.xr.runtime.internal.Dimensions getDimensions();
+ method public float getRadius();
+ property public androidx.xr.runtime.internal.Dimensions dimensions;
+ property public float radius;
+ }
+
+ public static final class SurfaceEntity.Shape.Quad implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+ method public androidx.xr.runtime.internal.Dimensions getDimensions();
+ method public androidx.xr.runtime.math.FloatSize2d getExtents();
+ property public androidx.xr.runtime.internal.Dimensions dimensions;
+ property public androidx.xr.runtime.math.FloatSize2d extents;
+ }
+
+ public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Sphere(float radius);
+ method public androidx.xr.runtime.internal.Dimensions getDimensions();
+ method public float getRadius();
+ property public androidx.xr.runtime.internal.Dimensions dimensions;
+ property public float radius;
}
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.StereoMode {
@@ -1937,6 +1927,19 @@
field public static final int NONE = 0; // 0x0
}
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.SurfaceProtection {
+ field public static final androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection.Companion Companion;
+ field public static final int NONE = 0; // 0x0
+ field public static final int PROTECTED = 1; // 0x1
+ }
+
+ public static final class SurfaceEntity.SurfaceProtection.Companion {
+ property public static int NONE;
+ property public static int PROTECTED;
+ field public static final int NONE = 0; // 0x0
+ field public static final int PROTECTED = 1; // 0x1
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SystemSpaceEntity extends androidx.xr.runtime.internal.Entity {
method public void setOnSpaceUpdatedListener(Runnable? listener, java.util.concurrent.Executor? executor);
}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
index a2e46a6..71dd832 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
@@ -444,10 +444,9 @@
*
* @param stereoMode Stereo mode for the surface.
* @param pose Pose of this entity relative to its parent, default value is Identity.
- * @param canvasShape The [SurfaceEntity.CanvasShape] which describes the spatialized shape of
- * the canvas.
- * @param contentSecurityLevel The [SurfaceEntity.ContentSecurityLevel] which describes whether
- * DRM is enabled.
+ * @param shape The [SurfaceEntity.Shape] which describes the 3D geometry of the entity.
+ * @param surfaceProtection The [SurfaceEntity.SurfaceProtection] which describes whether DRM is
+ * enabled.
* @param superSampling The [SurfaceEntity.SuperSampling] which describes whether super sampling
* is enabled. Whether to use super sampling for the surface.
* @param parentEntity The parent entity of this entity.
@@ -456,8 +455,8 @@
public fun createSurfaceEntity(
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
- @SurfaceEntity.ContentSecurityLevel contentSecurityLevel: Int,
+ shape: SurfaceEntity.Shape,
+ @SurfaceEntity.SurfaceProtection surfaceProtection: Int,
superSampling: Int,
parentEntity: Entity,
): SurfaceEntity
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
index 4eff280..e51fe76 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
@@ -36,6 +36,59 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public interface RenderingRuntime {
/**
+ * Loads glTF Asset for the given asset name from the assets folder. The future returned by this
+ * method will fire listeners on the UI thread if Runnable::run is supplied.
+ *
+ * @param assetName The name of the asset to load from the assets folder.
+ * @return A future that resolves to the glTF model when it is loaded. The future will be null
+ * if the asset was not found.
+ */
+ @Suppress("AsyncSuffixFuture")
+ public fun loadGltfByAssetName(assetName: String): ListenableFuture<GltfModelResource>
+
+ /**
+ * Loads glTF Asset from a provided byte array. The future returned by this method will fire
+ * listeners on the UI thread if Runnable::run is supplied.
+ *
+ * @param assetData A gltfAsset in the form of a byte array.
+ * @param assetKey The name of the asset to load from the cache.
+ * @return A future that resolves to the glTF model when it is loaded. The future will be null
+ * if the asset was not found.
+ */
+ @Suppress("AsyncSuffixFuture")
+ // TODO(b/397746548): Add InputStream support for loading glTFs.
+ // Suppressed to allow CompletableFuture.
+ public fun loadGltfByByteArray(
+ assetData: ByteArray,
+ assetKey: String,
+ ): ListenableFuture<GltfModelResource>
+
+ /**
+ * Loads an ExrImage for the given asset name from the assets folder.
+ *
+ * @param assetName The name of the asset to load from the assets folder.
+ * @return A future that resolves to the ExrImage when it is loaded. The future will be null if
+ * the asset was not found.
+ */
+ @SuppressWarnings("AsyncSuffixFuture")
+ public fun loadExrImageByAssetName(assetName: String): ListenableFuture<ExrImageResource>
+
+ /**
+ * Loads an ExrImage from a provided byte array.
+ *
+ * @param assetData An ExrImage in the form of a byte array.
+ * @param assetKey The name of the asset to load from the cache.
+ * @return A future that resolves to the ExrImage when it is loaded. The future will be null if
+ * the asset was not found.
+ */
+ @Suppress("AsyncSuffixFuture")
+ // Suppressed to allow CompletableFuture.
+ public fun loadExrImageByByteArray(
+ assetData: ByteArray,
+ assetKey: String,
+ ): ListenableFuture<ExrImageResource>
+
+ /**
* Loads a texture resource for the given asset name or URL. The future returned by this method
* will fire listeners on the UI thread if Runnable::run is supplied.
*
@@ -60,6 +113,15 @@
public fun destroyTexture(texture: TextureResource)
/**
+ * Returns the reflection texture from the given IBL.
+ *
+ * @param iblToken An ExrImageResource representing a loaded Image-Based Lighting (IBL) asset.
+ * @return A TextureResource representing the reflection texture, or null if the texture was not
+ * found.
+ */
+ public fun getReflectionTextureFromIbl(iblToken: ExrImageResource): TextureResource?
+
+ /**
* Creates a water material by querying it from the system's built-in materials. The future
* returned by this method will fire listeners on the UI thread if Runnable::run is supplied.
*
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
index d28d461..d0cc6ad 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
@@ -18,6 +18,7 @@
import android.view.Surface
import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.FloatSize2d
/**
* Interface for a spatialized Entity which manages an Android Surface. Applications can render to
@@ -38,11 +39,11 @@
public var stereoMode: Int
/**
- * Specifies the shape of the spatial canvas which the surface is texture mapped to.
+ * Specifies the geometry of the spatial canvas which the surface is texture mapped to.
*
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
- public var canvasShape: CanvasShape
+ public var shape: Shape
/**
* Retrieves the dimensions of the "spatial canvas" which the surface is mapped to. These values
@@ -103,8 +104,8 @@
* Indicates whether explicit color information has been set for the surface content. If
* `false`, the runtime should signal the backend to use its best effort color correction and
* tonemapping. If `true`, the runtime should inform the backend to use the values specified in
- * [colorSpace], [colorTransfer], [colorRange], and [maxCLL] for color correction and
- * tonemapping of the surface content.
+ * [colorSpace], [colorTransfer], [colorRange], and [maxContentLightLevel] for color correction
+ * and tonemapping of the surface content.
*
* This property is typically managed by the `setContentColorMetadata` and
* `resetContentColorMetadata` methods.
@@ -131,10 +132,11 @@
public val colorRange: Int
/**
- * The active maximum content light level (MaxCLL) in nits. A value of 0 indicates that MaxCLL
- * is not set or is unknown. This value is used if [contentColorMetadataSet] is `true`.
+ * The active maximum content light level (MaxContentLightLevel) in nits. A value of 0 indicates
+ * that MaxContentLightLevel is not set or is unknown. This value is used if
+ * [contentColorMetadataSet] is `true`.
*/
- public val maxCLL: Int
+ public val maxContentLightLevel: Int
/**
* Sets the explicit color information for the surface content. This will also set
@@ -144,20 +146,20 @@
* @param colorTransfer The runtime color transfer value (e.g.,
* [SurfaceEntity.ColorTransfer.SRGB]).
* @param colorRange The runtime color range value (e.g., [SurfaceEntity.ColorRange.FULL]).
- * @param maxCLL The maximum content light level in nits.
+ * @param maxContentLightLevel The maximum content light level in nits.
* @throws IllegalStateException if the Entity has been disposed.
*/
public fun setContentColorMetadata(
colorSpace: Int,
colorTransfer: Int,
colorRange: Int,
- maxCLL: Int,
+ maxContentLightLevel: Int,
)
/**
* Resets the color information to the runtime's default handling. This will set
* [contentColorMetadataSet] to `false` and typically involves reverting [colorSpace],
- * [colorTransfer], [colorRange], and [maxCLL] to their default runtime values.
+ * [colorTransfer], [colorRange], and [maxContentLightLevel] to their default runtime values.
*
* @throws IllegalStateException if the Entity has been disposed.
*/
@@ -189,7 +191,7 @@
*
* See https://developer.android.com/reference/android/media/MediaDrm for more details.
*/
- public annotation class ContentSecurityLevel {
+ public annotation class SurfaceProtection {
public companion object {
// The Surface content is not secured. DRM content can not be decoded into this Surface.
// Screen captures of the SurfaceEntity will show the Surface content.
@@ -264,22 +266,22 @@
}
/** Represents the shape of the spatial canvas which the surface is texture mapped to. */
- public interface CanvasShape {
+ public interface Shape {
public val dimensions: Dimensions
/**
* A 2D rectangle-shaped canvas. Width and height are represented in the local spatial
* coordinate system of the entity. (0,0,0) is the center of the canvas.
*/
- public class Quad(public val width: Float, public val height: Float) : CanvasShape {
- override val dimensions: Dimensions = Dimensions(width, height, 0f)
+ public class Quad(public val extents: FloatSize2d) : Shape {
+ override val dimensions: Dimensions = Dimensions(extents.width, extents.height, 0f)
}
/**
* A sphere-shaped canvas. Radius is represented in the local spatial coordinate system of
* the entity. (0,0,0) is the center of the sphere.
*/
- public class Vr360Sphere(public val radius: Float) : CanvasShape {
+ public class Sphere(public val radius: Float) : Shape {
override val dimensions: Dimensions = Dimensions(radius * 2, radius * 2, radius * 2)
}
@@ -287,7 +289,7 @@
* A hemisphere-shaped canvas. Radius is represented in the local spatial coordinate system
* of the entity. (0,0,0) is the center of the base of the hemisphere.
*/
- public class Vr180Hemisphere(public val radius: Float) : CanvasShape {
+ public class Hemisphere(public val radius: Float) : Shape {
override val dimensions: Dimensions = Dimensions(radius * 2, radius * 2, radius)
}
}
@@ -295,15 +297,15 @@
/** Specifies edge transparency effects for the canvas. */
public interface EdgeFeather {
/** A smooth feathering effect which moves from edges of UV space to the center. */
- public class SmoothFeather(public val leftRight: Float, public val topBottom: Float) :
+ public class RectangleFeather(public val leftRight: Float, public val topBottom: Float) :
EdgeFeather
/** A Default implementation of EdgeFeather that does nothing. */
- public class SolidEdge() : EdgeFeather
+ public class NoFeathering() : EdgeFeather
}
/**
- * The edge feathering effect for the spatialized geometry.
+ * The edge feathering effect for the spatialized shape.
*
* @throws IllegalStateException if the Entity has been disposed.
*/
diff --git a/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt b/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
index 85bb1e7..a86e30a 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
@@ -96,7 +96,7 @@
val intent = Intent(this, ActivityPanel::class.java)
intent.putExtra("NAV_ICON", false)
// Launch an activity in the panel
- activityPanelEntity.launchActivity(intent, savedInstanceState)
+ activityPanelEntity.startActivity(intent, savedInstanceState)
// Add movable component
val movableComponent = MovableComponent.createSystemMovable(session!!)
activityPanelEntity.addComponent(movableComponent)
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
index 1206de6..1bea6ff 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
@@ -20,6 +20,7 @@
import android.widget.RadioGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.Pose
import androidx.xr.scenecore.MovableComponent
@@ -47,21 +48,21 @@
activity.findViewById<Button>(R.id.button_destroy_surface_entity)
val canvasRadioOptions =
listOf(
- SurfaceEntity.CanvasShape.Quad(1f, 1f),
- SurfaceEntity.CanvasShape.Vr180Hemisphere(1f),
- SurfaceEntity.CanvasShape.Vr360Sphere(1f),
+ SurfaceEntity.Shape.Quad(FloatSize2d(1f, 1f)),
+ SurfaceEntity.Shape.Hemisphere(1f),
+ SurfaceEntity.Shape.Sphere(1f),
)
- private val _selectedCanvasShapeOptionFlow = MutableStateFlow(canvasRadioOptions[0])
- private var selectedCanvasShapeOption: SurfaceEntity.CanvasShape
- get() = _selectedCanvasShapeOptionFlow.value
+ private val _selectedShapeOptionFlow = MutableStateFlow(canvasRadioOptions[0])
+ private var selectedShapeOption: SurfaceEntity.Shape
+ get() = _selectedShapeOptionFlow.value
set(value) {
- _selectedCanvasShapeOptionFlow.value = value
+ _selectedShapeOptionFlow.value = value
}
init {
updateButtonStates()
surfaceEntityRadioGroup.setOnCheckedChangeListener { group, checkedId ->
- selectedCanvasShapeOption =
+ selectedShapeOption =
when (checkedId) {
R.id.radiobutton_quad -> canvasRadioOptions[0]
R.id.radiobutton_vr180 -> canvasRadioOptions[1]
@@ -70,7 +71,7 @@
}
// If entity exists, update its shape immediately
if (surfaceEntity != null) {
- surfaceEntity?.canvasShape = selectedCanvasShapeOption
+ surfaceEntity?.shape = selectedShapeOption
}
}
@@ -90,10 +91,10 @@
if (surfaceEntity == null) {
surfaceEntity =
SurfaceEntity.create(
- session,
- SurfaceEntity.StereoMode.MONO,
- Pose.Identity,
- selectedCanvasShapeOption,
+ session = session,
+ pose = Pose.Identity,
+ shape = selectedShapeOption,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
)
// Make the video player movable (to make it easier to look at it from
// different angles and distances)
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
index b8ef940..5f19f0a 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
@@ -170,7 +170,7 @@
ActivityPanelEntity.create(session!!, IntSize2d(640, 480), "activity_panel")
val intent = Intent(this, ActivityPanel::class.java)
intent.putExtra("NAV_ICON", false)
- activityPanelEntity!!.launchActivity(intent)
+ activityPanelEntity!!.startActivity(intent)
activityPanelEntity!!.setPose(Pose(Vector3(0.75f, 0.0f, 0.0f)))
activityPanelCreated = true
}
diff --git a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
index 4c7f83c..983319a 100644
--- a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
+++ b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
@@ -68,6 +68,7 @@
import androidx.xr.runtime.Config.HeadTrackingMode
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.IntSize2d
import androidx.xr.runtime.math.Pose
@@ -229,13 +230,13 @@
fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): FloatSize3d {
when (stereoMode) {
- SurfaceEntity.StereoMode.MONO,
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
- SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+ SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
return FloatSize3d(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.TOP_BOTTOM ->
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
return FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
- SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
return FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
}
@@ -246,7 +247,7 @@
videoUri: String,
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
+ shape: SurfaceEntity.Shape,
loop: Boolean = true,
protected: Boolean = false,
) {
@@ -254,20 +255,26 @@
if (surfaceEntity == null) {
val surfaceContentLevel =
if (protected) {
- SurfaceEntity.ContentSecurityLevel.PROTECTED
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
} else {
- SurfaceEntity.ContentSecurityLevel.NONE
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
}
surfaceEntity =
- SurfaceEntity.create(session, stereoMode, pose, canvasShape, surfaceContentLevel)
+ SurfaceEntity.create(
+ session = session,
+ pose = pose,
+ shape = shape,
+ stereoMode = stereoMode,
+ surfaceProtection = surfaceContentLevel,
+ )
// Make the video player movable (to make it easier to look at it from different
// angles and distances) (only on quad canvas)
movableComponent = MovableComponent.createSystemMovable(session)
// The quad has a radius of 1.0 meters
movableComponent!!.size = FloatSize3d(1.0f, 1.0f, 1.0f)
- if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
+ if (shape is SurfaceEntity.Shape.Quad) {
val unused = surfaceEntity!!.addComponent(movableComponent!!)
}
}
@@ -306,10 +313,14 @@
// Resize the canvas to match the video aspect ratio - accounting for
// the stereo mode.
val dimensions = getCanvasAspectRatio(stereoMode, width, height)
- // Set the dimensions of the Quad canvas to the video dimensions
- if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
- surfaceEntity?.canvasShape =
- SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+ // Set the dimensions of the Quad canvas to the video dimensions and
+ // attach the
+ // a MovableComponent.
+ if (shape is SurfaceEntity.Shape.Quad) {
+ surfaceEntity?.shape =
+ SurfaceEntity.Shape.Quad(
+ FloatSize2d(dimensions.width, dimensions.height)
+ )
movableComponent?.size =
surfaceEntity?.dimensions ?: FloatSize3d(1.0f, 1.0f, 1.0f)
}
@@ -392,7 +403,7 @@
videoUri: String,
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
+ shape: SurfaceEntity.Shape,
buttonText: String,
enabled: Boolean = true,
loop: Boolean = true,
@@ -414,7 +425,7 @@
enabled = enabled,
onClick = {
// Create SurfaceEntity and MovableComponent if they don't exist.
- playVideo(session, videoUri, stereoMode, pose, canvasShape, loop, protected)
+ playVideo(session, videoUri, stereoMode, pose, shape, loop, protected)
},
) {
Text(text = buttonText, fontSize = 20.sp)
@@ -452,9 +463,9 @@
playVideo(
session = session,
videoUri = videoUri,
- stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
pose = Pose(Vector3(0.0f, 0.0f, -0.25f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
loop = true,
protected = false,
)
@@ -499,9 +510,9 @@
playVideo(
session = session,
videoUri = videoUri,
- stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
pose = Pose(Vector3(0.0f, 0.0f, -0.25f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
loop = true,
protected = true,
)
diff --git a/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt b/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
index 0a399de..c605581 100644
--- a/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
+++ b/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
@@ -83,6 +83,7 @@
import androidx.xr.runtime.Config.HeadTrackingMode
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.IntSize2d
import androidx.xr.runtime.math.Pose
@@ -330,13 +331,13 @@
val effectiveDisplayWidth = videoWidth.toFloat() * pixelAspectRatio
return when (stereoMode) {
- SurfaceEntity.StereoMode.MONO,
- SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
- SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+ SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+ SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
FloatSize3d(1.0f, videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
- SurfaceEntity.StereoMode.TOP_BOTTOM ->
+ SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
- SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
}
@@ -413,8 +414,8 @@
value = featherRadiusX,
onValueChange = {
featherRadiusX = it
- surfaceEntity!!.edgeFeather =
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+ surfaceEntity!!.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
featherRadiusX,
featherRadiusY,
)
@@ -425,8 +426,8 @@
value = featherRadiusY,
onValueChange = {
featherRadiusY = it
- surfaceEntity!!.edgeFeather =
- SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+ surfaceEntity!!.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
featherRadiusX,
featherRadiusY,
)
@@ -438,7 +439,7 @@
Row(verticalAlignment = Alignment.CenterVertically) {
Button(
onClick = {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+ surfaceEntity!!.shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
// Move the Quad-shaped canvas to a spot in front of the User.
surfaceEntity!!.setPose(
session.scene.spatialUser.head?.transformPoseTo(
@@ -453,33 +454,33 @@
) {
Text(text = "Set Quad", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f) }) {
Text(text = "Set Vr360", fontSize = 10.sp)
}
- Button(
- onClick = {
- surfaceEntity!!.canvasShape =
- SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f)
- }
- ) {
+ Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f) }) {
Text(text = "Set Vr180", fontSize = 10.sp)
}
} // end row
Row(verticalAlignment = Alignment.CenterVertically) {
- Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+ Button(
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+ }
+ ) {
Text(text = "Mono", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+ onClick = {
+ surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+ }
) {
Text(text = "Top-Bottom", fontSize = 10.sp)
}
Button(
- onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+ onClick = {
+ surfaceEntity!!.stereoMode =
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+ }
) {
Text(text = "Side-by-Side", fontSize = 10.sp)
}
@@ -521,9 +522,11 @@
orientedHeight,
currentPixelAspectRatio,
)
- if (currentSurfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Quad) {
- currentSurfaceEntity.canvasShape =
- SurfaceEntity.CanvasShape.Quad(newShapeDimensions.width, newShapeDimensions.height)
+ if (currentSurfaceEntity.shape is SurfaceEntity.Shape.Quad) {
+ currentSurfaceEntity.shape =
+ SurfaceEntity.Shape.Quad(
+ FloatSize2d(newShapeDimensions.width, newShapeDimensions.height)
+ )
movableComponent?.size = currentSurfaceEntity.dimensions
}
@@ -566,7 +569,7 @@
// This is a hack for nonQuad canvas shapes. We don't expect those videos to contain
// internal rotation, and we want to be able to position the control panel relative to
// the video panel.
- if (!(currentSurfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Quad)) {
+ if (!(currentSurfaceEntity.shape is SurfaceEntity.Shape.Quad)) {
panel.parent = currentSurfaceEntity
}
@@ -577,7 +580,7 @@
}
}
- // Note that pose here will be ignored if the canvasShape is not a Quad
+ // Note that pose here will be ignored if the shape is not a Quad
// TODO: Update this to take a Pose for the controlPanel
@Suppress("UnsafeOptInUsageError")
@Composable
@@ -587,7 +590,7 @@
videoUri: String,
stereoMode: Int,
pose: Pose,
- canvasShape: SurfaceEntity.CanvasShape,
+ shape: SurfaceEntity.Shape,
buttonText: String,
buttonColor: Color = VideoButtonColors.DefaultButton,
enabled: Boolean = true,
@@ -611,7 +614,7 @@
enabled = enabled,
onClick = {
var actualPose = pose
- if (!(canvasShape is SurfaceEntity.CanvasShape.Quad)) {
+ if (!(shape is SurfaceEntity.Shape.Quad)) {
actualPose =
session.scene.spatialUser.head?.transformPoseTo(
Pose.Identity,
@@ -624,9 +627,9 @@
val surfaceContentLevel =
if (protected) {
- SurfaceEntity.ContentSecurityLevel.PROTECTED
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
} else {
- SurfaceEntity.ContentSecurityLevel.NONE
+ SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
}
val superSamplingMode =
@@ -634,20 +637,19 @@
this@VideoPlayerTestActivity.superSamplingMode ==
SuperSamplingMode.DEFAULT
) {
- SurfaceEntity.SuperSampling.DEFAULT
+ SurfaceEntity.SuperSampling.SUPER_SAMPLING_PENTAGON
} else {
- SurfaceEntity.SuperSampling.NONE
+ SurfaceEntity.SuperSampling.SUPER_SAMPLING_NONE
}
surfaceEntity =
SurfaceEntity.create(
- session,
- stereoMode,
- actualPose,
- canvasShape,
- surfaceContentLevel,
- null,
- superSamplingMode,
+ session = session,
+ pose = actualPose,
+ shape = shape,
+ stereoMode = stereoMode,
+ superSampling = superSamplingMode,
+ surfaceProtection = surfaceContentLevel,
)
// Make the video player movable (to make it easier to look at it from different
// angles and distances) (only on quad canvas)
@@ -655,7 +657,7 @@
// The quad has a radius of 1.0 meters
movableComponent!!.size = FloatSize3d(1.0f, 1.0f, 1.0f)
- if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
+ if (shape is SurfaceEntity.Shape.Quad) {
val unused = surfaceEntity!!.addComponent(movableComponent!!)
}
}
@@ -743,7 +745,7 @@
colorSpace = colorSpace,
colorTransfer = colorTransfer,
colorRange = colorRange,
- maxCLL = maxContentLightLevel,
+ maxContentLightLevel = maxContentLightLevel,
)
surfaceEntity?.contentColorMetadata =
contentColorMetadata
@@ -829,9 +831,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/vid_bigbuckbunny.mp4",
- stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[Stereo] Play Big Buck Bunny",
buttonColor = VideoButtonColors.StandardPlayback,
enabled = enabled,
@@ -854,9 +856,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/mvhevc_flat_left_primary_1080.mov",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[Multiview] Play MVHEVC Left Primary",
buttonColor = VideoButtonColors.Multiview,
enabled = enabled,
@@ -880,9 +882,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/mvhevc_flat_right_primary_1080.mov",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[Multiview] Play MVHEVC Right Primary",
buttonColor = VideoButtonColors.Multiview,
enabled = enabled,
@@ -899,9 +901,9 @@
// For Testers: Note that this translates to "/sdcard/Download/Naver180.mp4".
videoUri =
Environment.getExternalStorageDirectory().getPath() + "/Download/Naver180.mp4",
- stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
pose = Pose.Identity, // will be head pose
- canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+ shape = SurfaceEntity.Shape.Hemisphere(1.0f),
buttonText = "[VR] Play Naver 180 (Side-by-Side)",
buttonColor = VideoButtonColors.VR,
enabled = enabled,
@@ -918,9 +920,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/Galaxy11_VR_3D360.mp4",
- stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
pose = Pose.Identity, // will be head pose
- canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+ shape = SurfaceEntity.Shape.Sphere(1.0f),
buttonText = "[VR] Play Galaxy 360 (Top-Bottom)",
buttonColor = VideoButtonColors.VR,
enabled = enabled,
@@ -937,9 +939,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/Naver180_MV-HEVC.mp4",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
pose = Pose.Identity, // will be head pose
- canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+ shape = SurfaceEntity.Shape.Hemisphere(1.0f),
buttonText = "[VR] Play Naver 180 (MV-HEVC)",
buttonColor = VideoButtonColors.VR,
enabled = enabled,
@@ -957,9 +959,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/Galaxy11_VR_3D360_MV-HEVC.mp4",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
pose = Pose.Identity, // will be head pose
- canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+ shape = SurfaceEntity.Shape.Sphere(1.0f),
buttonText = "[VR] Play Galaxy 360 (MV-HEVC)",
buttonColor = VideoButtonColors.VR,
enabled = enabled,
@@ -982,9 +984,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/sdr_singleview_protected.mp4",
- stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[DRM] Play Side-by-Side",
buttonColor = VideoButtonColors.DRM,
enabled = enabled,
@@ -1008,9 +1010,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/mvhevc_flat_left_primary_1080_protected.mp4",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[DRM] Play MVHEVC Left Primary",
buttonColor = VideoButtonColors.DRM,
enabled = enabled,
@@ -1034,9 +1036,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/hdr_pq_1000nits_1080p.mp4",
- stereoMode = SurfaceEntity.StereoMode.MONO,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[HDR] Play HDR PQ Video",
buttonColor = VideoButtonColors.HDR,
enabled = enabled,
@@ -1060,9 +1062,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/single_view_rotated_270_half_width.mp4",
- stereoMode = SurfaceEntity.StereoMode.MONO,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[Transform] Play Single View (Rot 270, PAR 0.5)",
buttonColor = VideoButtonColors.Transformations,
enabled = enabled,
@@ -1086,9 +1088,9 @@
videoUri =
Environment.getExternalStorageDirectory().getPath() +
"/Download/mvhevc_left_primary_rotated_180.mp4",
- stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
- canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
buttonText = "[Transform] Play MVHEVC Left Primary (Rot 180)",
buttonColor = VideoButtonColors.Transformations,
enabled = enabled,
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java
new file mode 100644
index 0000000..67ee229
--- /dev/null
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.xr.scenecore.spatial.rendering;
+
+import androidx.xr.runtime.internal.ExrImageResource;
+
+/**
+ * Implementation of a SceneCore ExrImageResource for the Split Engine.
+ *
+ * <p>EXR Images are high dynamic range images that can be used as environmental skyboxes, and can
+ * be used for Image Based Lighting.
+ */
+final class ExrImageResourceImpl implements ExrImageResource {
+ private final long mToken;
+
+ ExrImageResourceImpl(long token) {
+ mToken = token;
+ }
+
+ public long getExtensionImageToken() {
+ return mToken;
+ }
+}
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java
new file mode 100644
index 0000000..f4b73de
--- /dev/null
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java
@@ -0,0 +1,38 @@
+/*
+ * 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.xr.scenecore.spatial.rendering;
+
+import androidx.xr.runtime.internal.GltfModelResource;
+
+/**
+ * Implementation of a SceneCore GltfModelResource.
+ *
+ * <p>This is used to create to load a glTF that can later be used when creating a GltfEntity.
+ */
+// TODO: b/417750821 - Add an interface which returns an integer animation IDX given a string
+// animation name for a loaded GLTF.
+final class GltfModelResourceImpl implements GltfModelResource {
+ private final long mToken;
+
+ GltfModelResourceImpl(long token) {
+ mToken = token;
+ }
+
+ public long getExtensionModelToken() {
+ return mToken;
+ }
+}
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
index b95935d..6fefd15 100644
--- a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
@@ -21,6 +21,8 @@
import androidx.annotation.VisibleForTesting;
import androidx.concurrent.futures.ResolvableFuture;
+import androidx.xr.runtime.internal.ExrImageResource;
+import androidx.xr.runtime.internal.GltfModelResource;
import androidx.xr.runtime.internal.KhronosPbrMaterialSpec;
import androidx.xr.runtime.internal.MaterialResource;
import androidx.xr.runtime.internal.RenderingRuntime;
@@ -47,6 +49,8 @@
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
+import java.util.function.Supplier;
+
/**
* Implementation of [RenderingRuntime] for devices that support the [Feature.SPATIAL] system
* feature.
@@ -136,6 +140,113 @@
return new MaterialResourceImpl(token);
}
+ private static GltfModelResourceImpl getModelResourceFromToken(long token) {
+ return new GltfModelResourceImpl(token);
+ }
+
+ private static ExrImageResourceImpl getExrImageResourceFromToken(long token) {
+ return new ExrImageResourceImpl(token);
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private @Nullable ListenableFuture<GltfModelResource> loadGltfAsset(
+ Supplier<ListenableFuture<Long>> modelLoader) {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+
+ ResolvableFuture<GltfModelResource> gltfModelResourceFuture = ResolvableFuture.create();
+
+ ListenableFuture<Long> gltfTokenFuture;
+ try {
+ gltfTokenFuture = modelLoader.get();
+ } catch (RuntimeException e) {
+ return null;
+ }
+
+ gltfTokenFuture.addListener(
+ () -> {
+ try {
+ long gltfToken = gltfTokenFuture.get();
+ gltfModelResourceFuture.set(getModelResourceFromToken(gltfToken));
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ gltfModelResourceFuture.setException(e);
+ }
+ },
+ mActivity::runOnUiThread);
+
+ return gltfModelResourceFuture;
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private @Nullable ListenableFuture<ExrImageResource> loadExrImage(
+ Supplier<ListenableFuture<Long>> assetLoader) {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+
+ ResolvableFuture<ExrImageResource> exrImageResourceFuture = ResolvableFuture.create();
+
+ ListenableFuture<Long> exrImageTokenFuture;
+ try {
+ exrImageTokenFuture = assetLoader.get();
+ } catch (RuntimeException e) {
+ return null;
+ }
+
+ exrImageTokenFuture.addListener(
+ () -> {
+ try {
+ long exrImageToken = exrImageTokenFuture.get();
+ exrImageResourceFuture.set(getExrImageResourceFromToken(exrImageToken));
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ exrImageResourceFuture.setException(e);
+ }
+ },
+ mActivity::runOnUiThread);
+
+ return exrImageResourceFuture;
+ }
+
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Override
+ public @NonNull ListenableFuture<GltfModelResource> loadGltfByAssetName(@NonNull String name) {
+ return loadGltfAsset(() -> mImpressApi.loadGltfAsset(name));
+ }
+
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Override
+ public @NonNull ListenableFuture<GltfModelResource> loadGltfByByteArray(
+ byte @NonNull [] assetData, @NonNull String assetKey) {
+ return loadGltfAsset(() -> mImpressApi.loadGltfAsset(assetData, assetKey));
+ }
+
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Override
+ public @NonNull ListenableFuture<ExrImageResource> loadExrImageByAssetName(
+ @NonNull String assetName) {
+ return loadExrImage(() -> mImpressApi.loadImageBasedLightingAsset(assetName));
+ }
+
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Override
+ public @NonNull ListenableFuture<ExrImageResource> loadExrImageByByteArray(
+ byte @NonNull [] assetData, @NonNull String assetKey) {
+ return loadExrImage(() -> mImpressApi.loadImageBasedLightingAsset(assetData, assetKey));
+ }
+
// ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
// within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
// warning, however, we get a build error - go/bugpattern/RestrictTo.
@@ -193,6 +304,18 @@
mImpressApi.destroyNativeObject(textureResource.getTextureToken());
}
+ @Override
+ public @Nullable TextureResource getReflectionTextureFromIbl(
+ @NonNull ExrImageResource iblToken) {
+ ExrImageResourceImpl exrImageResource = (ExrImageResourceImpl) iblToken;
+ Texture texture =
+ mImpressApi.getReflectionTextureFromIbl(exrImageResource.getExtensionImageToken());
+ if (texture == null) {
+ return null;
+ }
+ return texture;
+ }
+
// ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
// within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
// warning, however, we get a build error - go/bugpattern/RestrictTo.
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
index 7a7047d..3825965 100644
--- a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
+++ b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
@@ -20,6 +20,8 @@
import android.app.Activity;
+import androidx.xr.runtime.internal.ExrImageResource;
+import androidx.xr.runtime.internal.GltfModelResource;
import androidx.xr.runtime.internal.MaterialResource;
import androidx.xr.runtime.internal.SceneRuntime;
import androidx.xr.runtime.internal.SceneRuntimeFactory;
@@ -103,6 +105,66 @@
}
@Test
+ public void loadGltfByAssetName_returnsModel() throws Exception {
+ ListenableFuture<GltfModelResource> modelFuture =
+ mRuntime.loadGltfByAssetName("FakeAsset.glb");
+
+ assertThat(modelFuture).isNotNull();
+
+ GltfModelResource model = modelFuture.get();
+ assertThat(model).isNotNull();
+ GltfModelResourceImpl modelImpl = (GltfModelResourceImpl) model;
+ assertThat(modelImpl).isNotNull();
+ long token = modelImpl.getExtensionModelToken();
+ assertThat(token).isEqualTo(1);
+ }
+
+ @Test
+ public void loadGltfByByteArray_returnsModel() throws Exception {
+ ListenableFuture<GltfModelResource> modelFuture =
+ mRuntime.loadGltfByByteArray(new byte[] {1, 2, 3}, "FakeAsset.glb");
+
+ assertThat(modelFuture).isNotNull();
+
+ GltfModelResource model = modelFuture.get();
+ assertThat(model).isNotNull();
+ GltfModelResourceImpl modelImpl = (GltfModelResourceImpl) model;
+ assertThat(modelImpl).isNotNull();
+ long token = modelImpl.getExtensionModelToken();
+ assertThat(token).isEqualTo(1);
+ }
+
+ @Test
+ public void loadExrImageByAssetName_returnsModel() throws Exception {
+ ListenableFuture<ExrImageResource> imageFuture =
+ mRuntime.loadExrImageByAssetName("FakeAsset.zip");
+
+ assertThat(imageFuture).isNotNull();
+
+ ExrImageResource image = imageFuture.get();
+ assertThat(image).isNotNull();
+ ExrImageResourceImpl imageImpl = (ExrImageResourceImpl) image;
+ assertThat(imageImpl).isNotNull();
+ long token = imageImpl.getExtensionImageToken();
+ assertThat(token).isEqualTo(1);
+ }
+
+ @Test
+ public void loadExrImageByByteArray_returnsModel() throws Exception {
+ ListenableFuture<ExrImageResource> imageFuture =
+ mRuntime.loadExrImageByByteArray(new byte[] {1, 2, 3}, "FakeAsset.zip");
+
+ assertThat(imageFuture).isNotNull();
+
+ ExrImageResource image = imageFuture.get();
+ assertThat(image).isNotNull();
+ ExrImageResourceImpl imageImpl = (ExrImageResourceImpl) image;
+ assertThat(imageImpl).isNotNull();
+ long token = imageImpl.getExtensionImageToken();
+ assertThat(token).isEqualTo(1);
+ }
+
+ @Test
public void createWaterMaterial_returnsWaterMaterial() throws Exception {
assertThat(createWaterMaterial()).isNotNull();
}
diff --git a/xr/scenecore/scenecore/api/current.txt b/xr/scenecore/scenecore/api/current.txt
index 0a7bc22..d803f9e 100644
--- a/xr/scenecore/scenecore/api/current.txt
+++ b/xr/scenecore/scenecore/api/current.txt
@@ -4,9 +4,9 @@
public final class ActivityPanelEntity extends androidx.xr.scenecore.PanelEntity {
method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name);
method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name, optional androidx.xr.runtime.math.Pose pose);
- method public void launchActivity(android.content.Intent intent);
- method public void launchActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
method public void moveActivity(android.app.Activity activity);
+ method public void startActivity(android.content.Intent intent);
+ method public void startActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
field public static final androidx.xr.scenecore.ActivityPanelEntity.Companion Companion;
}
@@ -706,5 +706,104 @@
field public static final float NO_PREFERRED_ASPECT_RATIO = -1.0f;
}
+ public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+ method public androidx.xr.runtime.math.FloatSize3d getDimensions();
+ method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeatheringParams();
+ method public androidx.xr.scenecore.PerceivedResolutionResult getPerceivedResolution();
+ method public androidx.xr.scenecore.SurfaceEntity.Shape getShape();
+ method public int getStereoMode();
+ method @MainThread public android.view.Surface getSurface();
+ method @MainThread public void setEdgeFeatheringParams(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
+ method @MainThread public void setShape(androidx.xr.scenecore.SurfaceEntity.Shape);
+ method @MainThread public void setStereoMode(int);
+ property public androidx.xr.runtime.math.FloatSize3d dimensions;
+ property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeatheringParams;
+ property public androidx.xr.scenecore.SurfaceEntity.Shape shape;
+ property public int stereoMode;
+ field public static final androidx.xr.scenecore.SurfaceEntity.Companion Companion;
+ }
+
+ public static final class SurfaceEntity.Companion {
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+ }
+
+ public abstract static class SurfaceEntity.EdgeFeatheringParams {
+ }
+
+ public static final class SurfaceEntity.EdgeFeatheringParams.NoFeathering extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+ ctor public SurfaceEntity.EdgeFeatheringParams.NoFeathering();
+ }
+
+ public static final class SurfaceEntity.EdgeFeatheringParams.RectangleFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+ ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather();
+ ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
+ method public float getLeftRight();
+ method public float getTopBottom();
+ property public float leftRight;
+ property public float topBottom;
+ }
+
+ public static interface SurfaceEntity.Shape {
+ }
+
+ public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+ method public float getRadius();
+ property public float radius;
+ }
+
+ public static final class SurfaceEntity.Shape.Quad implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+ method public androidx.xr.runtime.math.FloatSize2d getExtents();
+ property public androidx.xr.runtime.math.FloatSize2d extents;
+ }
+
+ public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Sphere(float radius);
+ method public float getRadius();
+ property public float radius;
+ }
+
+ public static final class SurfaceEntity.StereoMode {
+ property public static int STEREO_MODE_MONO;
+ property public static int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY;
+ property public static int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY;
+ property public static int STEREO_MODE_SIDE_BY_SIDE;
+ property public static int STEREO_MODE_TOP_BOTTOM;
+ field public static final androidx.xr.scenecore.SurfaceEntity.StereoMode INSTANCE;
+ field public static final int STEREO_MODE_MONO = 0; // 0x0
+ field public static final int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
+ field public static final int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
+ field public static final int STEREO_MODE_SIDE_BY_SIDE = 2; // 0x2
+ field public static final int STEREO_MODE_TOP_BOTTOM = 1; // 0x1
+ }
+
+ public static final class SurfaceEntity.SuperSampling {
+ property public static int SUPER_SAMPLING_NONE;
+ property public static int SUPER_SAMPLING_PENTAGON;
+ field public static final androidx.xr.scenecore.SurfaceEntity.SuperSampling INSTANCE;
+ field public static final int SUPER_SAMPLING_NONE = 0; // 0x0
+ field public static final int SUPER_SAMPLING_PENTAGON = 1; // 0x1
+ }
+
+ public static final class SurfaceEntity.SurfaceProtection {
+ property public static int SURFACE_PROTECTION_NONE;
+ property public static int SURFACE_PROTECTION_PROTECTED;
+ field public static final androidx.xr.scenecore.SurfaceEntity.SurfaceProtection INSTANCE;
+ field public static final int SURFACE_PROTECTION_NONE = 0; // 0x0
+ field public static final int SURFACE_PROTECTION_PROTECTED = 1; // 0x1
+ }
+
}
diff --git a/xr/scenecore/scenecore/api/restricted_current.txt b/xr/scenecore/scenecore/api/restricted_current.txt
index 66eb40c..5792866 100644
--- a/xr/scenecore/scenecore/api/restricted_current.txt
+++ b/xr/scenecore/scenecore/api/restricted_current.txt
@@ -4,9 +4,9 @@
public final class ActivityPanelEntity extends androidx.xr.scenecore.PanelEntity {
method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name);
method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name, optional androidx.xr.runtime.math.Pose pose);
- method public void launchActivity(android.content.Intent intent);
- method public void launchActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
method public void moveActivity(android.app.Activity activity);
+ method public void startActivity(android.content.Intent intent);
+ method public void startActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
field public static final androidx.xr.scenecore.ActivityPanelEntity.Companion Companion;
}
@@ -931,182 +931,175 @@
method public androidx.xr.scenecore.SubspaceNodeEntity create(androidx.xr.runtime.Session session, com.google.androidxr.splitengine.SubspaceNode subspaceNode, androidx.xr.runtime.math.FloatSize3d size);
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
+ public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata);
- method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata, optional int superSampling);
- method public androidx.xr.scenecore.Texture? getAuxiliaryAlphaMaskTexture();
- method public androidx.xr.scenecore.SurfaceEntity.CanvasShape getCanvasShape();
- method public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? getContentColorMetadata();
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+ method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? getAuxiliaryAlphaMaskTexture();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? getContentColorMetadata();
method public androidx.xr.runtime.math.FloatSize3d getDimensions();
- method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeather();
+ method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeatheringParams();
method public androidx.xr.scenecore.PerceivedResolutionResult getPerceivedResolution();
- method public androidx.xr.scenecore.Texture? getPrimaryAlphaMaskTexture();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? getPrimaryAlphaMaskTexture();
+ method public androidx.xr.scenecore.SurfaceEntity.Shape getShape();
method public int getStereoMode();
method @MainThread public android.view.Surface getSurface();
- method @MainThread public void setAuxiliaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
- method @MainThread public void setCanvasShape(androidx.xr.scenecore.SurfaceEntity.CanvasShape);
- method @MainThread public void setContentColorMetadata(androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata?);
- method @MainThread public void setEdgeFeather(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
- method @MainThread public void setPrimaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+ method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setAuxiliaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+ method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setContentColorMetadata(androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata?);
+ method @MainThread public void setEdgeFeatheringParams(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
+ method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setPrimaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+ method @MainThread public void setShape(androidx.xr.scenecore.SurfaceEntity.Shape);
method @MainThread public void setStereoMode(int);
- property public androidx.xr.scenecore.Texture? auxiliaryAlphaMaskTexture;
- property public androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape;
- property public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? auxiliaryAlphaMaskTexture;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata;
property public androidx.xr.runtime.math.FloatSize3d dimensions;
- property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeather;
- property public androidx.xr.scenecore.Texture? primaryAlphaMaskTexture;
+ property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeatheringParams;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? primaryAlphaMaskTexture;
+ property public androidx.xr.scenecore.SurfaceEntity.Shape shape;
property public int stereoMode;
field public static final androidx.xr.scenecore.SurfaceEntity.Companion Companion;
}
- public abstract static class SurfaceEntity.CanvasShape {
- method public androidx.xr.runtime.math.FloatSize3d getDimensions();
- property public androidx.xr.runtime.math.FloatSize3d dimensions;
- }
-
- public static final class SurfaceEntity.CanvasShape.Quad extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Quad(float width, float height);
- method public float getHeight();
- method public float getWidth();
- property public androidx.xr.runtime.math.FloatSize3d dimensions;
- property public float height;
- property public float width;
- }
-
- public static final class SurfaceEntity.CanvasShape.Vr180Hemisphere extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Vr180Hemisphere(float radius);
- method public float getRadius();
- property public androidx.xr.runtime.math.FloatSize3d dimensions;
- property public float radius;
- }
-
- public static final class SurfaceEntity.CanvasShape.Vr360Sphere extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
- ctor public SurfaceEntity.CanvasShape.Vr360Sphere(float radius);
- method public float getRadius();
- property public androidx.xr.runtime.math.FloatSize3d dimensions;
- property public float radius;
- }
-
public static final class SurfaceEntity.Companion {
method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata);
- method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata, optional int superSampling);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+ method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
}
- public static final class SurfaceEntity.ContentColorMetadata {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class SurfaceEntity.ContentColorMetadata {
ctor public SurfaceEntity.ContentColorMetadata();
- ctor public SurfaceEntity.ContentColorMetadata(optional int colorSpace, optional int colorTransfer, optional int colorRange, optional int maxCLL);
+ ctor public SurfaceEntity.ContentColorMetadata(optional int colorSpace, optional int colorTransfer, optional int colorRange, optional int maxContentLightLevel);
method public int getColorRange();
method public int getColorSpace();
method public int getColorTransfer();
- method public int getMaxCLL();
+ method public int getMaxContentLightLevel();
property public int colorRange;
property public int colorSpace;
property public int colorTransfer;
- property public int maxCLL;
+ property public int maxContentLightLevel;
field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.Companion Companion;
- field public static final int maxCLLUnknown = 0; // 0x0
+ field public static final int MAX_CONTENT_LIGHT_LEVEL_UNKNOWN = 0; // 0x0
}
public static final class SurfaceEntity.ContentColorMetadata.ColorRange {
- property public static int FULL;
- property public static int LIMITED;
- field public static final int FULL = 1; // 0x1
+ property public static int COLOR_RANGE_FULL;
+ property public static int COLOR_RANGE_LIMITED;
+ field public static final int COLOR_RANGE_FULL = 1; // 0x1
+ field public static final int COLOR_RANGE_LIMITED = 2; // 0x2
field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorRange INSTANCE;
- field public static final int LIMITED = 2; // 0x2
}
public static final class SurfaceEntity.ContentColorMetadata.ColorSpace {
- property public static int ADOBE_RGB;
- property public static int BT2020;
- property public static int BT601_525;
- property public static int BT601_PAL;
- property public static int BT709;
- property public static int DCI_P3;
- property public static int DISPLAY_P3;
- field public static final int ADOBE_RGB = 243; // 0xf3
- field public static final int BT2020 = 6; // 0x6
- field public static final int BT601_525 = 240; // 0xf0
- field public static final int BT601_PAL = 2; // 0x2
- field public static final int BT709 = 1; // 0x1
- field public static final int DCI_P3 = 242; // 0xf2
- field public static final int DISPLAY_P3 = 241; // 0xf1
+ property public static int COLOR_SPACE_ADOBE_RGB;
+ property public static int COLOR_SPACE_BT2020;
+ property public static int COLOR_SPACE_BT601_525;
+ property public static int COLOR_SPACE_BT601_PAL;
+ property public static int COLOR_SPACE_BT709;
+ property public static int COLOR_SPACE_DCI_P3;
+ property public static int COLOR_SPACE_DISPLAY_P3;
+ field public static final int COLOR_SPACE_ADOBE_RGB = 243; // 0xf3
+ field public static final int COLOR_SPACE_BT2020 = 6; // 0x6
+ field public static final int COLOR_SPACE_BT601_525 = 240; // 0xf0
+ field public static final int COLOR_SPACE_BT601_PAL = 2; // 0x2
+ field public static final int COLOR_SPACE_BT709 = 1; // 0x1
+ field public static final int COLOR_SPACE_DCI_P3 = 242; // 0xf2
+ field public static final int COLOR_SPACE_DISPLAY_P3 = 241; // 0xf1
field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorSpace INSTANCE;
}
public static final class SurfaceEntity.ContentColorMetadata.ColorTransfer {
- property public static int GAMMA_2_2;
- property public static int HLG;
- property public static int LINEAR;
- property public static int SDR;
- property public static int SRGB;
- property public static int ST2084;
- field public static final int GAMMA_2_2 = 10; // 0xa
- field public static final int HLG = 7; // 0x7
+ property public static int COLOR_TRANSFER_GAMMA_2_2;
+ property public static int COLOR_TRANSFER_HLG;
+ property public static int COLOR_TRANSFER_LINEAR;
+ property public static int COLOR_TRANSFER_SDR;
+ property public static int COLOR_TRANSFER_SRGB;
+ property public static int COLOR_TRANSFER_ST2084;
+ field public static final int COLOR_TRANSFER_GAMMA_2_2 = 10; // 0xa
+ field public static final int COLOR_TRANSFER_HLG = 7; // 0x7
+ field public static final int COLOR_TRANSFER_LINEAR = 1; // 0x1
+ field public static final int COLOR_TRANSFER_SDR = 3; // 0x3
+ field public static final int COLOR_TRANSFER_SRGB = 2; // 0x2
+ field public static final int COLOR_TRANSFER_ST2084 = 6; // 0x6
field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorTransfer INSTANCE;
- field public static final int LINEAR = 1; // 0x1
- field public static final int SDR = 3; // 0x3
- field public static final int SRGB = 2; // 0x2
- field public static final int ST2084 = 6; // 0x6
}
public static final class SurfaceEntity.ContentColorMetadata.Companion {
- property public static int maxCLLUnknown;
- }
-
- public static final class SurfaceEntity.ContentSecurityLevel {
- property public static int NONE;
- property public static int PROTECTED;
- field public static final androidx.xr.scenecore.SurfaceEntity.ContentSecurityLevel INSTANCE;
- field public static final int NONE = 0; // 0x0
- field public static final int PROTECTED = 1; // 0x1
+ method public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata getDEFAULT_UNSET_CONTENT_COLOR_METADATA();
+ property public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata DEFAULT_UNSET_CONTENT_COLOR_METADATA;
+ property public static int MAX_CONTENT_LIGHT_LEVEL_UNKNOWN;
}
public abstract static class SurfaceEntity.EdgeFeatheringParams {
}
- public static final class SurfaceEntity.EdgeFeatheringParams.SmoothFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
- ctor public SurfaceEntity.EdgeFeatheringParams.SmoothFeather();
- ctor public SurfaceEntity.EdgeFeatheringParams.SmoothFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
+ public static final class SurfaceEntity.EdgeFeatheringParams.NoFeathering extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+ ctor public SurfaceEntity.EdgeFeatheringParams.NoFeathering();
+ }
+
+ public static final class SurfaceEntity.EdgeFeatheringParams.RectangleFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+ ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather();
+ ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
method public float getLeftRight();
method public float getTopBottom();
property public float leftRight;
property public float topBottom;
}
- public static final class SurfaceEntity.EdgeFeatheringParams.SolidEdge extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
- ctor public SurfaceEntity.EdgeFeatheringParams.SolidEdge();
+ public static interface SurfaceEntity.Shape {
+ }
+
+ public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+ method public float getRadius();
+ property public float radius;
+ }
+
+ public static final class SurfaceEntity.Shape.Quad implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+ method public androidx.xr.runtime.math.FloatSize2d getExtents();
+ property public androidx.xr.runtime.math.FloatSize2d extents;
+ }
+
+ public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+ ctor public SurfaceEntity.Shape.Sphere(float radius);
+ method public float getRadius();
+ property public float radius;
}
public static final class SurfaceEntity.StereoMode {
- property public static int MONO;
- property public static int MULTIVIEW_LEFT_PRIMARY;
- property public static int MULTIVIEW_RIGHT_PRIMARY;
- property public static int SIDE_BY_SIDE;
- property public static int TOP_BOTTOM;
+ property public static int STEREO_MODE_MONO;
+ property public static int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY;
+ property public static int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY;
+ property public static int STEREO_MODE_SIDE_BY_SIDE;
+ property public static int STEREO_MODE_TOP_BOTTOM;
field public static final androidx.xr.scenecore.SurfaceEntity.StereoMode INSTANCE;
- field public static final int MONO = 0; // 0x0
- field public static final int MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
- field public static final int MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
- field public static final int SIDE_BY_SIDE = 2; // 0x2
- field public static final int TOP_BOTTOM = 1; // 0x1
+ field public static final int STEREO_MODE_MONO = 0; // 0x0
+ field public static final int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
+ field public static final int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
+ field public static final int STEREO_MODE_SIDE_BY_SIDE = 2; // 0x2
+ field public static final int STEREO_MODE_TOP_BOTTOM = 1; // 0x1
}
public static final class SurfaceEntity.SuperSampling {
- property public static int DEFAULT;
- property public static int NONE;
- field public static final int DEFAULT = 1; // 0x1
+ property public static int SUPER_SAMPLING_NONE;
+ property public static int SUPER_SAMPLING_PENTAGON;
field public static final androidx.xr.scenecore.SurfaceEntity.SuperSampling INSTANCE;
- field public static final int NONE = 0; // 0x0
+ field public static final int SUPER_SAMPLING_NONE = 0; // 0x0
+ field public static final int SUPER_SAMPLING_PENTAGON = 1; // 0x1
+ }
+
+ public static final class SurfaceEntity.SurfaceProtection {
+ property public static int SURFACE_PROTECTION_NONE;
+ property public static int SURFACE_PROTECTION_PROTECTED;
+ field public static final androidx.xr.scenecore.SurfaceEntity.SurfaceProtection INSTANCE;
+ field public static final int SURFACE_PROTECTION_NONE = 0; // 0x0
+ field public static final int SURFACE_PROTECTION_PROTECTED = 1; // 0x1
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Texture {
@@ -1270,7 +1263,7 @@
method public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions, androidx.xr.runtime.internal.Dimensions);
method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
method public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object!>, androidx.xr.runtime.internal.Dimensions);
- method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(@androidx.xr.runtime.internal.SurfaceEntity.StereoMode int, androidx.xr.runtime.math.Pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int, @androidx.xr.runtime.internal.SurfaceEntity.SuperSampling int, androidx.xr.runtime.internal.Entity);
+ method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(@androidx.xr.runtime.internal.SurfaceEntity.StereoMode int, androidx.xr.runtime.math.Pose, androidx.xr.runtime.internal.SurfaceEntity.Shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int, @androidx.xr.runtime.internal.SurfaceEntity.SuperSampling int, androidx.xr.runtime.internal.Entity);
method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource!> createWaterMaterial(boolean);
method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource);
method public void destroyTexture(androidx.xr.runtime.internal.TextureResource);
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
index e3f94a1..ccffdd0 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
@@ -29,8 +29,9 @@
/**
* ActivityPanelEntity creates a spatial panel for embedding an [Activity] in Android XR. Users can
* either use an [Intent] to launch an Activity in the given panel or provide an instance of
- * Activity to move into this panel. Calling [Entity.dispose] on this Entity will destroy the
- * underlying Activity.
+ * Activity to move into this panel. In order to launch and embed an activity,
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability is required. Calling
+ * [Entity.dispose] on this Entity will destroy the underlying Activity.
*/
public class ActivityPanelEntity
private constructor(
@@ -40,21 +41,23 @@
) : PanelEntity(lifecycleManager, rtActivityPanelEntity, entityManager) {
/**
- * Launches an [Activity] in the given panel. Subsequent calls to this method will replace the
+ * Starts an [Activity] in the given panel. Subsequent calls to this method will replace the
* already existing Activity in the panel with the new one. The panel will not be visible until
- * an Activity is successfully launched. This method will not provide any information about when
- * the Activity successfully launches.
+ * an Activity is successfully launched. This will fail if the [Scene] does not have the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability. This method will not
+ * provide any information about when the Activity successfully launches.
*
* @param intent Intent to launch the activity.
* @param bundle Bundle to pass to the activity, can be null.
*/
@JvmOverloads
- public fun launchActivity(intent: Intent, bundle: Bundle? = null) {
+ public fun startActivity(intent: Intent, bundle: Bundle? = null) {
rtActivityPanelEntity.launchActivity(intent, bundle)
}
/**
- * Moves the given [Activity] into this panel.
+ * Moves the given [Activity] into this panel. This will fail if the application does not have
+ * the [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability.
*
* @param activity Activity to move into this panel.
*/
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
index 33b13fe..45cd9a6 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
@@ -16,6 +16,7 @@
package androidx.xr.scenecore
+import android.annotation.SuppressLint
import android.view.Surface
import androidx.annotation.FloatRange
import androidx.annotation.IntDef
@@ -26,122 +27,131 @@
import androidx.xr.runtime.internal.JxrPlatformAdapter
import androidx.xr.runtime.internal.LifecycleManager
import androidx.xr.runtime.internal.SurfaceEntity as RtSurfaceEntity
+import androidx.xr.runtime.math.FloatSize2d
import androidx.xr.runtime.math.FloatSize3d
import androidx.xr.runtime.math.Pose
/**
- * SurfaceEntity is a concrete implementation of Entity that hosts a StereoSurface Canvas. The
- * entity creates and owns an Android Surface into which the application can render stereo image
- * content. This Surface is then texture mapped to the canvas, and if a stereoscopic StereoMode is
- * specified, then the User will see left and right eye content mapped to the appropriate display.
+ * SurfaceEntity is an [Entity] that hosts a [Surface], which will be texture mapped onto the
+ * [Shape]. If a stereoscopic [StereoMode] is specified, then the User will see left and right eye
+ * content mapped to the appropriate display.
*
- * Note that it is not currently possible to synchronize CanvasShape and StereoMode changes with
+ * Note that it is not currently possible to synchronize [Shape] and [StereoMode] changes with
* application rendering or video decoding. Applications are advised to carefully hide this entity
- * around transitions to manage glitchiness.
+ * around state transitions (for example in response to video events) to manage glitchiness.
*
- * @property canvasShape The [CanvasShape] which describes the mesh to which the Surface is mapped.
+ * @property shape The [Shape] which describes the mesh to which the Surface is mapped.
* @property stereoMode The [StereoMode] which describes how parts of the surface are displayed to
* the user's eyes.
* @property dimensions The dimensions of the canvas in the local spatial coordinate system of the
* entity.
- * @property primaryAlphaMaskTexture The texture to be composited into the alpha channel of the
- * surface. If null, the alpha mask will be disabled.
- * @property auxiliaryAlphaMaskTexture The texture to be composited into the alpha channel of the
- * secondary view of the surface. This is only used for interleaved stereo content. If null, the
- * alpha mask will be disabled.
- * @property edgeFeather The [EdgeFeather] which describes the edge fading effects for the surface.
- * @property contentColorMetadata The [ContentColorMetadata] of the content (nullable).
+ * @property edgeFeatheringParams The [EdgeFeatheringParams] which describes the edge fading effects
+ * for the surface.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class SurfaceEntity
private constructor(
private val lifecycleManager: LifecycleManager,
rtEntity: RtSurfaceEntity,
entityManager: EntityManager,
- canvasShape: CanvasShape,
+ shape: Shape,
private var disposed: Boolean = false, // TODO b/427314036: remove this
) : BaseEntity<RtSurfaceEntity>(rtEntity, entityManager) {
/** Represents the shape of the Canvas that backs a SurfaceEntity. */
- public abstract class CanvasShape private constructor() {
- public open val dimensions: FloatSize3d = FloatSize3d(0.0f, 0.0f, 0.0f)
+ public interface Shape {
- // A Quad-shaped canvas. Width and height are represented in the local spatial coordinate
- // system of the entity. (0,0,0) is the center of the canvas.
- public class Quad(public val width: Float, public val height: Float) : CanvasShape() {
- override val dimensions: FloatSize3d
- get() = FloatSize3d(width, height, 0.0f)
- }
+ /**
+ * A Quadrilateral-shaped canvas. Width and height are expressed in the X and Y axis in the
+ * local spatial coordinate system of the entity. (0,0) is the center of the Quad mesh; the
+ * lower-left corner of the Quad is at (-width/2, -height/2).
+ *
+ * @property extents The size of the Quad in the local spatial coordinate system of the
+ * entity.
+ */
+ public class Quad(public val extents: FloatSize2d) : Shape {}
- // An inwards-facing sphere-shaped canvas, centered at (0,0,0) in the local coordinate
- // space.
- // This is intended to be used by setting the entity's pose to the user's head pose.
- // Radius is represented in the local spatial coordinate system of the entity.
- // The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
- // space,
- // and UV's are applied from positive X to negative X in an equirectangular projection.
- public class Vr360Sphere(public val radius: Float) : CanvasShape() {
- override val dimensions: FloatSize3d
- get() = FloatSize3d(radius * 2, radius * 2, radius * 2)
- }
+ /**
+ * cal An inwards-facing sphere-shaped mesh, centered at (0,0,0) in the local coordinate
+ * space. This is intended to be used by setting the entity's pose to the user's head pose.
+ * Radius is represented in the local spatial coordinate system of the entity. The center of
+ * the Surface will be mapped to (0, 0, -radius) in the local coordinate space, and texture
+ * coordinate UVs are applied from positive X to negative X in an equirectangular
+ * projection. This means that if this Entity is set to the user's head pose, then they will
+ * be looking at the center of the video if it were viewed in a 2D player.
+ *
+ * @property radius The radius of the sphere in the local spatial coordinate system of the
+ * entity.
+ */
+ public class Sphere(public val radius: Float) : Shape {}
- // An inwards-facing hemisphere-shaped canvas, where (0,0,0) is the center of the base of
- // the
- // hemisphere. Radius is represented in the local spatial coordinate system of the entity.
- // This is intended to be used by setting the entity's pose to the user's head pose.
- // The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
- // space,
- // and UV's are applied from positive X to negative X in an equirectangular projection.
- public class Vr180Hemisphere(public val radius: Float) : CanvasShape() {
- override val dimensions: FloatSize3d
- get() = FloatSize3d(radius * 2, radius * 2, radius)
- }
+ /**
+ * An inwards-facing hemisphere-shaped canvas, where (0,0,0) is the center of the base of
+ * the hemisphere. Radius is represented in the local spatial coordinate system of the
+ * entity. This is intended to be used by setting the entity's pose to the user's head pose.
+ * The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
+ * space, and texture coordinate UV's are applied from positive X to negative X in an
+ * equirectangular projection.
+ *
+ * @property radius The radius of the hemisphere in the local spatial coordinate system of
+ * the entity.
+ */
+ public class Hemisphere(public val radius: Float) : Shape {}
}
/** Represents edge fading effects for a SurfaceEntity. */
public abstract class EdgeFeatheringParams private constructor() {
/**
- * @property leftRight a [Float] which controls the canvas-relative radius of the edge
+ * @property leftRight a [Float] which controls the shape-relative radius of the edge
* fadeout on the left and right edges of the SurfaceEntity canvas.
- * @property topBottom a [Float] which controls the canvas-relative radius of the edge
+ * @property topBottom a [Float] which controls the shape-relative radius of the edge
* fadeout on the top and bottom edges of the SurfaceEntity canvas.
*
* A radius of 0.05 represents 5% of the width of the visible canvas surface. Please note
* that this is scaled by the aspect ratio of Quad-shaped canvases.
*
- * Applications are encouraged to use ZeroFeather or set this to 0.0 on Spherical canvases.
- * The behavior is only defined for values between [0.0f - 0.5f]. Default values are 0.0f.
+ * Applications are encouraged to use [NoFeathering] on Spherical canvases. The behavior is
+ * only defined for values between [0.0f - 0.5f]. Default values are 0.0f.
*/
- public class SmoothFeather(
+ public class RectangleFeather(
@FloatRange(from = 0.0, to = 0.5) public val leftRight: Float = 0.0f,
@FloatRange(from = 0.0, to = 0.5) public val topBottom: Float = 0.0f,
) : EdgeFeatheringParams() {}
/** Applies no edge fading to any canvas. */
- public class SolidEdge : EdgeFeatheringParams() {}
+ public class NoFeathering : EdgeFeatheringParams() {}
}
- @IntDef(ContentSecurityLevel.NONE, ContentSecurityLevel.PROTECTED)
+ @IntDef(
+ SurfaceProtection.SURFACE_PROTECTION_NONE,
+ SurfaceProtection.SURFACE_PROTECTION_PROTECTED,
+ )
@Retention(AnnotationRetention.SOURCE)
- internal annotation class ContentSecurityLevelValue
+ internal annotation class SurfaceProtectionValue
/**
- * Specifies whether the Surface which backs this entity should support DRM content. This is
- * useful when decoding video content which requires DRM.
+ * Specifies whether the [Surface] which backs this [Entity] should be backed by
+ * [android.hardware.HardwareBuffer]s with the USAGE_PROTECTED_CONTENT flag set. These buffers
+ * support hardware paths for decoding protected content.
*
- * See https://developer.android.com/reference/android/media/MediaDrm for more details.
+ * @see https://developer.android.com/reference/android/media/MediaDrm for more details.
*/
- public object ContentSecurityLevel {
- // The Surface content is not secured. DRM content can not be decoded into this Surface.
- // Screen captures of the SurfaceEntity will show the Surface content.
- public const val NONE: Int = 0
+ public object SurfaceProtection {
+ /**
+ * The Surface content is not protected. Non-protected content can be decoded into this
+ * surface. Protected content can not be decoded into this Surface. Screen captures of the
+ * SurfaceEntity will show the Surface content.
+ */
+ public const val SURFACE_PROTECTION_NONE: Int = 0
- // The surface content is secured. DRM content can be decoded into this Surface.
- // Screen captures of the SurfaceEntity will redact the Surface content.
- public const val PROTECTED: Int = 1
+ /**
+ * The Surface content is protected. Non-protected content can be decoded into this surface.
+ * Protected content can be decoded into this Surface. Screen captures of the SurfaceEntity
+ * will redact the Surface content.
+ */
+ public const val SURFACE_PROTECTION_PROTECTED: Int = 1
}
- @IntDef(SuperSampling.NONE, SuperSampling.DEFAULT)
+ @IntDef(SuperSampling.SUPER_SAMPLING_NONE, SuperSampling.SUPER_SAMPLING_PENTAGON)
@Retention(AnnotationRetention.SOURCE)
internal annotation class SuperSamplingValue
@@ -150,56 +160,63 @@
* improve text clarity at a performance cost.
*/
public object SuperSampling {
- // Super sampling is disabled.
- public const val NONE: Int = 0
- // Super sampling is enabled. This is the default.
- public const val DEFAULT: Int = 1
+ /** Super sampling is disabled. */
+ public const val SUPER_SAMPLING_NONE: Int = 0
+
+ /**
+ * Super sampling is enabled with a default sampling pattern. This is the value that is set
+ * if SuperSampling is not specified when the Entity is created.
+ */
+ public const val SUPER_SAMPLING_PENTAGON: Int = 1
}
/**
* Specifies how the surface content will be routed for stereo viewing. Applications must render
- * into the surface in accordance with what is specified here in order for the compositor to
+ * into the surface in accordance with what they provided here in order for the compositor to
* correctly produce a stereoscopic view to the user.
*
- * Values here match values from androidx.media3.common.C.StereoMode in
- * //third_party/java/android_libs/media:common
+ * Values here match values from [androidx.media3.common.C.StereoMode].
+ *
+ * @see https://developer.android.com/reference/androidx/media3/common/C.StereoMode
*/
@IntDef(
- StereoMode.MONO,
- StereoMode.TOP_BOTTOM,
- StereoMode.SIDE_BY_SIDE,
- StereoMode.MULTIVIEW_LEFT_PRIMARY,
- StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+ StereoMode.STEREO_MODE_MONO,
+ StereoMode.STEREO_MODE_TOP_BOTTOM,
+ StereoMode.STEREO_MODE_SIDE_BY_SIDE,
+ StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+ StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
)
@Retention(AnnotationRetention.SOURCE)
internal annotation class StereoModeValue
public object StereoMode {
- // Each eye will see the entire surface (no separation)
- public const val MONO: Int = 0
- // The [top, bottom] halves of the surface will map to [left, right] eyes
- public const val TOP_BOTTOM: Int = 1
- // The [left, right] halves of the surface will map to [left, right] eyes
- public const val SIDE_BY_SIDE: Int = 2
- // Multiview video, [primary, auxiliary] views will map to [left, right] eyes
- public const val MULTIVIEW_LEFT_PRIMARY: Int = 4
- // Multiview video, [primary, auxiliary] views will map to [right, left] eyes
- public const val MULTIVIEW_RIGHT_PRIMARY: Int = 5
+ /** Each eye will see the entire surface (no separation) */
+ public const val STEREO_MODE_MONO: Int = 0
+ /** The [top, bottom] halves of the surface will map to [left, right] eyes */
+ public const val STEREO_MODE_TOP_BOTTOM: Int = 1
+ /** The [left, right] halves of the surface will map to [left, right] eyes */
+ public const val STEREO_MODE_SIDE_BY_SIDE: Int = 2
+ /** Multiview video, [primary, auxiliary] views will map to [left, right] eyes */
+ public const val STEREO_MODE_MULTIVIEW_LEFT_PRIMARY: Int = 4
+ /** Multiview video, [primary, auxiliary] views will map to [right, left] eyes */
+ public const val STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY: Int = 5
}
/**
- * Color information for the content drawn on a surface.
+ * Color information for the content drawn on a surface. This is used to hint to the system how
+ * the content should be rendered depending on display settings.
*
- * @param colorSpace The color space of the content.
- * @param colorTransfer The transfer function of the content.
- * @param colorRange The color range of the content.
- * @param maxCLL The maximum content light level of the content (in nits).
+ * @property colorSpace The color space of the content.
+ * @property colorTransfer The transfer function to apply to the content.
+ * @property colorRange The color range of the content.
+ * @property maxContentLightLevel The maximum brightness of the content (in nits).
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class ContentColorMetadata(
- @ColorSpaceValue public val colorSpace: Int = ColorSpace.BT709,
- @ColorTransferValue public val colorTransfer: Int = ColorTransfer.SRGB,
- @ColorRangeValue public val colorRange: Int = ColorRange.FULL,
- public val maxCLL: Int = Companion.maxCLLUnknown,
+ @ColorSpaceValue public val colorSpace: Int = ColorSpace.COLOR_SPACE_BT709,
+ @ColorTransferValue public val colorTransfer: Int = ColorTransfer.COLOR_TRANSFER_SRGB,
+ @ColorRangeValue public val colorRange: Int = ColorRange.COLOR_RANGE_FULL,
+ public val maxContentLightLevel: Int = Companion.MAX_CONTENT_LIGHT_LEVEL_UNKNOWN,
) {
/**
@@ -208,100 +225,140 @@
* These values are a superset of androidx.media3.common.C.ColorSpace.
*/
@IntDef(
- ColorSpace.BT709,
- ColorSpace.BT601_PAL,
- ColorSpace.BT2020,
- ColorSpace.BT601_525,
- ColorSpace.DISPLAY_P3,
- ColorSpace.DCI_P3,
- ColorSpace.ADOBE_RGB,
+ ColorSpace.COLOR_SPACE_BT709,
+ ColorSpace.COLOR_SPACE_BT601_PAL,
+ ColorSpace.COLOR_SPACE_BT2020,
+ ColorSpace.COLOR_SPACE_BT601_525,
+ ColorSpace.COLOR_SPACE_DISPLAY_P3,
+ ColorSpace.COLOR_SPACE_DCI_P3,
+ ColorSpace.COLOR_SPACE_ADOBE_RGB,
)
@Retention(AnnotationRetention.SOURCE)
internal annotation class ColorSpaceValue
public object ColorSpace {
- public const val BT709: Int = 1
- public const val BT601_PAL: Int = 2
- public const val BT2020: Int = 6
- public const val BT601_525: Int = 0xf0
- public const val DISPLAY_P3: Int = 0xf1
- public const val DCI_P3: Int = 0xf2
- public const val ADOBE_RGB: Int = 0xf3
+ /** Please see androidx.media3.common.C.COLOR_SPACE_BT709 */
+ public const val COLOR_SPACE_BT709: Int = 1
+ /** Please see androidx.media3.common.C.COLOR_SPACE_BT601 */
+ public const val COLOR_SPACE_BT601_PAL: Int = 2
+ /** Please see androidx.media3.common.C.COLOR_SPACE_BT2020 */
+ public const val COLOR_SPACE_BT2020: Int = 6
+ /** Please see ADataSpace::ADATASPACE_BT601_525 */
+ public const val COLOR_SPACE_BT601_525: Int = 0xf0
+ /** Please see ADataSpace::ADATASPACE_DISPLAY_P3 */
+ public const val COLOR_SPACE_DISPLAY_P3: Int = 0xf1
+ /** Please see ADataSpace::ADATASPACE_DCI_P3 */
+ public const val COLOR_SPACE_DCI_P3: Int = 0xf2
+ /** Please see ADataSpace::ADATASPACE_ADOBE_RGB */
+ public const val COLOR_SPACE_ADOBE_RGB: Int = 0xf3
}
+ @IntDef(
+ ColorTransfer.COLOR_TRANSFER_LINEAR,
+ ColorTransfer.COLOR_TRANSFER_SRGB,
+ ColorTransfer.COLOR_TRANSFER_SDR,
+ ColorTransfer.COLOR_TRANSFER_GAMMA_2_2,
+ ColorTransfer.COLOR_TRANSFER_ST2084,
+ ColorTransfer.COLOR_TRANSFER_HLG,
+ )
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class ColorTransferValue
+
/**
* Specifies the color transfer function of the media asset drawn on the surface.
*
* Enum members cover the transfer functions available in android::ADataSpace Enum values
* match values from androidx.media3.common.C.ColorTransfer.
*/
- @IntDef(
- ColorTransfer.LINEAR,
- ColorTransfer.SRGB,
- ColorTransfer.SDR, // SMPTE170M
- ColorTransfer.GAMMA_2_2,
- ColorTransfer.ST2084,
- ColorTransfer.HLG,
- )
- @Retention(AnnotationRetention.SOURCE)
- internal annotation class ColorTransferValue
-
public object ColorTransfer {
- public const val LINEAR: Int = 1
- public const val SRGB: Int = 2
- public const val SDR: Int = 3
- public const val GAMMA_2_2: Int = 10
- public const val ST2084: Int = 6
- public const val HLG: Int = 7
+ /** Linear transfer characteristic curve. */
+ public const val COLOR_TRANSFER_LINEAR: Int = 1
+ /** The standard RGB transfer function, used for some SDR use-cases like image input. */
+ public const val COLOR_TRANSFER_SRGB: Int = 2
+ /**
+ * SMPTE 170M transfer characteristic curve used by BT.601/BT.709/BT.2020. This is the
+ * curve used by most non-HDR video content.
+ */
+ public const val COLOR_TRANSFER_SDR: Int = 3
+ /** The Gamma 2.2 transfer function, used for some SDR use-cases like tone-mapping. */
+ public const val COLOR_TRANSFER_GAMMA_2_2: Int = 10
+ /** SMPTE ST 2084 transfer function. This is used by some HDR video content. */
+ public const val COLOR_TRANSFER_ST2084: Int = 6
+ /**
+ * ARIB STD-B67 hybrid-log-gamma transfer function. This is used by some HDR video
+ * content.
+ */
+ public const val COLOR_TRANSFER_HLG: Int = 7
}
+ @IntDef(ColorRange.COLOR_RANGE_FULL, ColorRange.COLOR_RANGE_LIMITED)
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class ColorRangeValue
+
/**
* Specifies the color range of the media asset drawn on the surface.
*
* Enum values match values from androidx.media3.common.C.ColorRange.
*/
- @IntDef(ColorRange.FULL, ColorRange.LIMITED)
- @Retention(AnnotationRetention.SOURCE)
- internal annotation class ColorRangeValue
-
public object ColorRange {
- public const val FULL: Int = 1
- public const val LIMITED: Int = 2
+ /** Please see android.media.MediaFormat.COLOR_RANGE_FULL */
+ public const val COLOR_RANGE_FULL: Int = 1
+ /** Please see android.media.MedaiFormat.COLOR_RANGE_LIMITED */
+ public const val COLOR_RANGE_LIMITED: Int = 2
}
public companion object {
- // Represents an unknown maximum content light level.
- public const val maxCLLUnknown: Int = 0
+ /**
+ * Represents an unknown maximum content light level.
+ *
+ * Note that the smallest value for this is 1 nit, so this value should only be used if
+ * the actual value is unknown or if the content is constant luminance.
+ */
+ public const val MAX_CONTENT_LIGHT_LEVEL_UNKNOWN: Int = 0
+
+ /**
+ * A Default (unset) value for ContentColorMetadata. Setting this will cause the system
+ * to render the content according to values set on the underlying [HardwareBuffer]s;
+ * these are usually set correctly by the MediaCodec.
+ */
+ public val DEFAULT_UNSET_CONTENT_COLOR_METADATA: ContentColorMetadata =
+ ContentColorMetadata(
+ colorSpace = ColorSpace.COLOR_SPACE_BT709,
+ colorTransfer = ColorTransfer.COLOR_TRANSFER_SRGB,
+ colorRange = ColorRange.COLOR_RANGE_FULL,
+ maxContentLightLevel = MAX_CONTENT_LIGHT_LEVEL_UNKNOWN,
+ )
internal fun getRtColorSpace(colorSpace: Int): Int {
return when (colorSpace) {
- ColorSpace.BT709 -> RtSurfaceEntity.ColorSpace.BT709
- ColorSpace.BT601_PAL -> RtSurfaceEntity.ColorSpace.BT601_PAL
- ColorSpace.BT2020 -> RtSurfaceEntity.ColorSpace.BT2020
- ColorSpace.BT601_525 -> RtSurfaceEntity.ColorSpace.BT601_525
- ColorSpace.DISPLAY_P3 -> RtSurfaceEntity.ColorSpace.DISPLAY_P3
- ColorSpace.DCI_P3 -> RtSurfaceEntity.ColorSpace.DCI_P3
- ColorSpace.ADOBE_RGB -> RtSurfaceEntity.ColorSpace.ADOBE_RGB
+ ColorSpace.COLOR_SPACE_BT709 -> RtSurfaceEntity.ColorSpace.BT709
+ ColorSpace.COLOR_SPACE_BT601_PAL -> RtSurfaceEntity.ColorSpace.BT601_PAL
+ ColorSpace.COLOR_SPACE_BT2020 -> RtSurfaceEntity.ColorSpace.BT2020
+ ColorSpace.COLOR_SPACE_BT601_525 -> RtSurfaceEntity.ColorSpace.BT601_525
+ ColorSpace.COLOR_SPACE_DISPLAY_P3 -> RtSurfaceEntity.ColorSpace.DISPLAY_P3
+ ColorSpace.COLOR_SPACE_DCI_P3 -> RtSurfaceEntity.ColorSpace.DCI_P3
+ ColorSpace.COLOR_SPACE_ADOBE_RGB -> RtSurfaceEntity.ColorSpace.ADOBE_RGB
else -> RtSurfaceEntity.ColorSpace.BT709
}
}
internal fun getRtColorTransfer(colorTransfer: Int): Int {
return when (colorTransfer) {
- ColorTransfer.LINEAR -> RtSurfaceEntity.ColorTransfer.LINEAR
- ColorTransfer.SRGB -> RtSurfaceEntity.ColorTransfer.SRGB
- ColorTransfer.SDR -> RtSurfaceEntity.ColorTransfer.SDR
- ColorTransfer.GAMMA_2_2 -> RtSurfaceEntity.ColorTransfer.GAMMA_2_2
- ColorTransfer.ST2084 -> RtSurfaceEntity.ColorTransfer.ST2084
- ColorTransfer.HLG -> RtSurfaceEntity.ColorTransfer.HLG
+ ColorTransfer.COLOR_TRANSFER_LINEAR -> RtSurfaceEntity.ColorTransfer.LINEAR
+ ColorTransfer.COLOR_TRANSFER_SRGB -> RtSurfaceEntity.ColorTransfer.SRGB
+ ColorTransfer.COLOR_TRANSFER_SDR -> RtSurfaceEntity.ColorTransfer.SDR
+ ColorTransfer.COLOR_TRANSFER_GAMMA_2_2 ->
+ RtSurfaceEntity.ColorTransfer.GAMMA_2_2
+ ColorTransfer.COLOR_TRANSFER_ST2084 -> RtSurfaceEntity.ColorTransfer.ST2084
+ ColorTransfer.COLOR_TRANSFER_HLG -> RtSurfaceEntity.ColorTransfer.HLG
else -> RtSurfaceEntity.ColorTransfer.SRGB
}
}
internal fun getRtColorRange(colorRange: Int): Int {
return when (colorRange) {
- ColorRange.FULL -> RtSurfaceEntity.ColorRange.FULL
- ColorRange.LIMITED -> RtSurfaceEntity.ColorRange.LIMITED
+ ColorRange.COLOR_RANGE_FULL -> RtSurfaceEntity.ColorRange.FULL
+ ColorRange.COLOR_RANGE_LIMITED -> RtSurfaceEntity.ColorRange.LIMITED
else -> RtSurfaceEntity.ColorRange.FULL
}
}
@@ -324,28 +381,29 @@
public companion object {
private fun getRtStereoMode(stereoMode: Int): Int {
return when (stereoMode) {
- StereoMode.MONO -> RtSurfaceEntity.StereoMode.MONO
- StereoMode.TOP_BOTTOM -> RtSurfaceEntity.StereoMode.TOP_BOTTOM
- StereoMode.MULTIVIEW_LEFT_PRIMARY ->
+ StereoMode.STEREO_MODE_MONO -> RtSurfaceEntity.StereoMode.MONO
+ StereoMode.STEREO_MODE_TOP_BOTTOM -> RtSurfaceEntity.StereoMode.TOP_BOTTOM
+ StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY ->
RtSurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY
- StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+ StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
RtSurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY
else -> RtSurfaceEntity.StereoMode.SIDE_BY_SIDE
}
}
- private fun getRtContentSecurityLevel(contentSecurityLevel: Int): Int {
- return when (contentSecurityLevel) {
- ContentSecurityLevel.NONE -> RtSurfaceEntity.ContentSecurityLevel.NONE
- ContentSecurityLevel.PROTECTED -> RtSurfaceEntity.ContentSecurityLevel.PROTECTED
- else -> RtSurfaceEntity.ContentSecurityLevel.NONE
+ private fun getRtSurfaceProtection(surfaceProtection: Int): Int {
+ return when (surfaceProtection) {
+ SurfaceProtection.SURFACE_PROTECTION_NONE -> RtSurfaceEntity.SurfaceProtection.NONE
+ SurfaceProtection.SURFACE_PROTECTION_PROTECTED ->
+ RtSurfaceEntity.SurfaceProtection.PROTECTED
+ else -> RtSurfaceEntity.SurfaceProtection.NONE
}
}
private fun getRtSuperSampling(superSampling: Int): Int {
return when (superSampling) {
- SuperSampling.NONE -> RtSurfaceEntity.SuperSampling.NONE
- SuperSampling.DEFAULT -> RtSurfaceEntity.SuperSampling.DEFAULT
+ SuperSampling.SUPER_SAMPLING_NONE -> RtSurfaceEntity.SuperSampling.NONE
+ SuperSampling.SUPER_SAMPLING_PENTAGON -> RtSurfaceEntity.SuperSampling.DEFAULT
else -> RtSurfaceEntity.SuperSampling.DEFAULT
}
}
@@ -358,9 +416,10 @@
* @param entityManager A SceneCore EntityManager
* @param stereoMode An [Int] which defines how surface subregions map to eyes
* @param pose Pose for this StereoSurface entity, relative to its parent.
- * @param canvasShape The [CanvasShape] which describes the spatialized shape of the canvas.
- * @param contentSecurityLevel The [ContentSecurityLevel] which describes whether DRM is
- * enabled for the surface.
+ * @param shape The [Shape] which describes the spatialized shape of the canvas.
+ * @param surfaceProtection The Int member of [SurfaceProtection] which describes whether
+ * DRM is enabled for the surface - which will create protected hardware buffers for
+ * presentation.
* @param contentColorMetadata The [ContentColorMetadata] of the content (nullable).
* @param superSampling The [SuperSampling] which describes whether super sampling is
* enabled for the surface.
@@ -370,22 +429,20 @@
lifecycleManager: LifecycleManager,
adapter: JxrPlatformAdapter,
entityManager: EntityManager,
- stereoMode: Int = StereoMode.SIDE_BY_SIDE,
+ @StereoModeValue stereoMode: Int = StereoMode.STEREO_MODE_MONO,
pose: Pose = Pose.Identity,
- canvasShape: CanvasShape = CanvasShape.Quad(1.0f, 1.0f),
- contentSecurityLevel: Int = ContentSecurityLevel.NONE,
+ shape: Shape = Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+ @SurfaceProtectionValue
+ surfaceProtection: Int = SurfaceProtection.SURFACE_PROTECTION_NONE,
contentColorMetadata: ContentColorMetadata? = null,
- superSampling: Int = SuperSampling.DEFAULT,
+ @SuperSamplingValue superSampling: Int = SuperSampling.SUPER_SAMPLING_PENTAGON,
): SurfaceEntity {
- val rtCanvasShape =
- when (canvasShape) {
- is CanvasShape.Quad ->
- RtSurfaceEntity.CanvasShape.Quad(canvasShape.width, canvasShape.height)
- is CanvasShape.Vr360Sphere ->
- RtSurfaceEntity.CanvasShape.Vr360Sphere(canvasShape.radius)
- is CanvasShape.Vr180Hemisphere ->
- RtSurfaceEntity.CanvasShape.Vr180Hemisphere(canvasShape.radius)
- else -> throw IllegalArgumentException("Unsupported canvas shape: $canvasShape")
+ val rtShape =
+ when (shape) {
+ is Shape.Quad -> RtSurfaceEntity.Shape.Quad(shape.extents)
+ is Shape.Sphere -> RtSurfaceEntity.Shape.Sphere(shape.radius)
+ is Shape.Hemisphere -> RtSurfaceEntity.Shape.Hemisphere(shape.radius)
+ else -> throw IllegalArgumentException("Unsupported shape: $shape")
}
val surfaceEntity =
SurfaceEntity(
@@ -393,13 +450,13 @@
adapter.createSurfaceEntity(
getRtStereoMode(stereoMode),
pose,
- rtCanvasShape,
- getRtContentSecurityLevel(contentSecurityLevel),
+ rtShape,
+ getRtSurfaceProtection(surfaceProtection),
getRtSuperSampling(superSampling),
adapter.activitySpaceRootImpl,
),
entityManager,
- canvasShape,
+ shape,
)
surfaceEntity.contentColorMetadata = contentColorMetadata
return surfaceEntity
@@ -408,16 +465,12 @@
/**
* Public factory function for a SurfaceEntity.
*
- * This method must be called from the main thread.
- * https://developer.android.com/guide/components/processes-and-threads
- *
* @param session Session to create the SurfaceEntity in.
* @param stereoMode Stereo mode for the surface.
* @param pose Pose of this entity relative to its parent, default value is Identity.
- * @param canvasShape The [CanvasShape] which describes the spatialized shape of the canvas.
- * @param contentSecurityLevel The [ContentSecurityLevel] which describes whether DRM is
- * enabled for the surface.
- * @param contentColorMetadata The [ContentColorMetadata] of the content (nullable).
+ * @param shape The [Shape] which describes the spatialized shape of the canvas.
+ * @param surfaceProtection The [SurfaceProtection] which describes whether the hosted
+ * surface should support Widevine DRM.
* @param superSampling The [SuperSampling] which describes whether super sampling is
* enabled for the surface.
* @return a SurfaceEntity instance
@@ -427,12 +480,12 @@
@JvmStatic
public fun create(
session: Session,
- stereoMode: Int = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
pose: Pose = Pose.Identity,
- canvasShape: CanvasShape = CanvasShape.Quad(1.0f, 1.0f),
- contentSecurityLevel: Int = ContentSecurityLevel.NONE,
- contentColorMetadata: ContentColorMetadata? = null,
- superSampling: Int = SuperSampling.DEFAULT,
+ shape: Shape = Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+ @StereoModeValue stereoMode: Int = StereoMode.STEREO_MODE_MONO,
+ @SuperSamplingValue superSampling: Int = SuperSampling.SUPER_SAMPLING_PENTAGON,
+ @SurfaceProtectionValue
+ surfaceProtection: Int = SurfaceProtection.SURFACE_PROTECTION_NONE,
): SurfaceEntity =
SurfaceEntity.create(
session.runtime.lifecycleManager,
@@ -440,9 +493,9 @@
session.scene.entityManager,
stereoMode,
pose,
- canvasShape,
- contentSecurityLevel,
- contentColorMetadata,
+ shape,
+ surfaceProtection,
+ null,
superSampling,
)
}
@@ -452,10 +505,9 @@
* into the surface in accordance with what is specified here in order for the compositor to
* correctly produce a stereoscopic view to the user.
*
- * Values must be one of the values from [StereoMode].
- *
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
+ @StereoModeValue
public var stereoMode: Int
get() {
checkDisposed()
@@ -468,11 +520,9 @@
}
/**
- * Returns the dimensions of the Entity.
+ * Returns the size of the canvas in the local spatial coordinate system of the entity.
*
- * This is the size of the canvas in the local spatial coordinate system of the entity. This
- * field cannot be directly set - to update the dimensions of the canvas, update the value of
- * [canvasShape].
+ * This value is entirely determined by the value of [shape].
*/
public val dimensions: FloatSize3d
get() {
@@ -481,27 +531,24 @@
}
/**
- * The shape of the canvas that backs the Entity. Updating this value will alter the dimensions
- * of the Entity.
+ * The shape of the canvas that backs the Entity. Updating this value will alter the
+ * [dimensions] of the Entity.
*
* @throws IllegalArgumentException if an invalid canvas shape is provided.
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
- public var canvasShape: CanvasShape = canvasShape
+ public var shape: Shape = shape
@MainThread
set(value) {
checkDisposed()
- val rtCanvasShape =
+ val rtShape =
when (value) {
- is CanvasShape.Quad ->
- RtSurfaceEntity.CanvasShape.Quad(value.width, value.height)
- is CanvasShape.Vr360Sphere ->
- RtSurfaceEntity.CanvasShape.Vr360Sphere(value.radius)
- is CanvasShape.Vr180Hemisphere ->
- RtSurfaceEntity.CanvasShape.Vr180Hemisphere(value.radius)
+ is Shape.Quad -> RtSurfaceEntity.Shape.Quad(value.extents)
+ is Shape.Sphere -> RtSurfaceEntity.Shape.Sphere(value.radius)
+ is Shape.Hemisphere -> RtSurfaceEntity.Shape.Hemisphere(value.radius)
else -> throw IllegalArgumentException("Unsupported canvas shape: $value")
}
- rtEntity.canvasShape = rtCanvasShape
+ rtEntity.shape = rtShape
field = value
}
@@ -511,8 +558,16 @@
*
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public var primaryAlphaMaskTexture: Texture? = null
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ get() {
+ checkDisposed()
+ return field
+ }
@MainThread
+ @SuppressLint("HiddenTypeParameter")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
set(value) {
checkDisposed()
rtEntity.setPrimaryAlphaMaskTexture(value?.texture)
@@ -525,8 +580,16 @@
*
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public var auxiliaryAlphaMaskTexture: Texture? = null
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ get() {
+ checkDisposed()
+ return field
+ }
@MainThread
+ @SuppressLint("HiddenTypeParameter")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
set(value) {
checkDisposed()
rtEntity.setAuxiliaryAlphaMaskTexture(value?.texture)
@@ -534,12 +597,12 @@
}
/**
- * The [EdgeFeather] feathering pattern to be used along the edges of the CanvasShape. This
+ * The [EdgeFeatheringParams] feathering pattern to be used along the edges of the Shape. This
* value must only be set from the main thread.
*
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
- public var edgeFeather: EdgeFeatheringParams = EdgeFeatheringParams.SolidEdge()
+ public var edgeFeatheringParams: EdgeFeatheringParams = EdgeFeatheringParams.NoFeathering()
get() {
checkDisposed()
return field
@@ -549,9 +612,13 @@
checkDisposed()
val rtEdgeFeather =
when (value) {
- is EdgeFeatheringParams.SolidEdge -> RtSurfaceEntity.EdgeFeather.SolidEdge()
- is EdgeFeatheringParams.SmoothFeather ->
- RtSurfaceEntity.EdgeFeather.SmoothFeather(value.leftRight, value.topBottom)
+ is EdgeFeatheringParams.NoFeathering ->
+ RtSurfaceEntity.EdgeFeather.NoFeathering()
+ is EdgeFeatheringParams.RectangleFeather ->
+ RtSurfaceEntity.EdgeFeather.RectangleFeather(
+ value.leftRight,
+ value.topBottom,
+ )
else -> throw IllegalArgumentException("Unsupported edge feather: $value")
}
rtEntity.edgeFeather = rtEdgeFeather
@@ -561,29 +628,31 @@
/**
* Manages the explicit [ContentColorMetadata] for the surface's content.
*
- * Describes how the application wants the system renderer to color convert Surface content to
+ * Describes how the application wants the system renderer to color convert [Surface] content to
* the Display. When this is null, the system will make a best guess at the appropriate
- * conversion.
+ * conversion. Most applications will not need to set this - video playback libraries such as
+ * ExoPlayer will automatically apply the correct conversion for media playback. Applications
+ * rendering to the surface using APIs such as Vulkan are encouraged to use Vulkan extensions to
+ * specify the color space and transfer function of their content and leave this value as null.
*
* The setter must be called from the main thread.
*
* @throws IllegalStateException when setting this value if the Entity has been disposed.
*/
- public var contentColorMetadata: ContentColorMetadata?
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public var contentColorMetadata: ContentColorMetadata? = null
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
get() {
checkDisposed()
return if (!rtEntity.contentColorMetadataSet) {
null
} else {
- ContentColorMetadata(
- colorSpace = rtEntity.colorSpace,
- colorTransfer = rtEntity.colorTransfer,
- colorRange = rtEntity.colorRange,
- maxCLL = rtEntity.maxCLL,
- )
+ return field
}
}
@MainThread
+ @SuppressLint("HiddenTypeParameter")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
set(value) {
checkDisposed()
if (value == null) {
@@ -593,18 +662,16 @@
ContentColorMetadata.getRtColorSpace(value.colorSpace),
ContentColorMetadata.getRtColorTransfer(value.colorTransfer),
ContentColorMetadata.getRtColorRange(value.colorRange),
- value.maxCLL,
+ value.maxContentLightLevel,
)
}
+ field = value
}
/**
* Returns a surface into which the application can render stereo image content. Note that
* android.graphics.Canvas Apis are not currently supported on this Canvas.
*
- * This method must be called from the main thread.
- * https://developer.android.com/guide/components/processes-and-threads
- *
* @throws IllegalStateException if the Entity has been disposed.
*/
@MainThread
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
index 2715ca7..9f8cc7e 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
@@ -1540,8 +1540,8 @@
public @NonNull SurfaceEntity createSurfaceEntity(
@SurfaceEntity.StereoMode int stereoMode,
@NonNull Pose pose,
- SurfaceEntity.@NonNull CanvasShape canvasShape,
- @SurfaceEntity.ContentSecurityLevel int contentSecurityLevel,
+ SurfaceEntity.@NonNull Shape shape,
+ @SurfaceEntity.SurfaceProtection int surfaceProtection,
@SurfaceEntity.SuperSampling int superSampling,
@NonNull Entity parentEntity) {
if (!mUseSplitEngine) {
@@ -1550,8 +1550,8 @@
} else {
return createSurfaceEntitySplitEngine(
stereoMode,
- canvasShape,
- contentSecurityLevel,
+ shape,
+ surfaceProtection,
superSampling,
pose,
parentEntity);
@@ -1909,8 +1909,8 @@
private SurfaceEntity createSurfaceEntitySplitEngine(
@SurfaceEntity.StereoMode int stereoMode,
- SurfaceEntity.CanvasShape canvasShape,
- @SurfaceEntity.ContentSecurityLevel int contentSecurityLevel,
+ SurfaceEntity.Shape shape,
+ @SurfaceEntity.SurfaceProtection int surfaceProtection,
@SurfaceEntity.SuperSampling int superSampling,
Pose pose,
@NonNull Entity parentEntity) {
@@ -1929,8 +1929,8 @@
mEntityManager,
mExecutor,
stereoMode,
- canvasShape,
- contentSecurityLevel,
+ shape,
+ surfaceProtection,
superSampling);
entity.setPose(pose, Space.PARENT);
return entity;
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
index a33acd3..3ece972 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
@@ -26,7 +26,7 @@
import androidx.xr.runtime.internal.PerceivedResolutionResult;
import androidx.xr.runtime.internal.Space;
import androidx.xr.runtime.internal.SurfaceEntity;
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape;
+import androidx.xr.runtime.internal.SurfaceEntity.Shape;
import androidx.xr.runtime.internal.TextureResource;
import androidx.xr.runtime.math.Vector3;
import androidx.xr.scenecore.impl.impress.ImpressApi;
@@ -57,12 +57,12 @@
private final int mSubspaceImpressNode;
@StereoMode private int mStereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE;
- @ContentSecurityLevel
- private int mContentSecurityLevel = SurfaceEntity.ContentSecurityLevel.NONE;
+ @SurfaceProtection
+ private int mSurfaceProtection = SurfaceEntity.SurfaceProtection.NONE;
@SuperSampling private int mSuperSampling = SurfaceEntity.SuperSampling.DEFAULT;
- private CanvasShape mCanvasShape;
+ private Shape mShape;
private EdgeFeather mEdgeFeather;
private boolean mContentColorMetadataSet = false;
@ColorSpace private int mColorSpace = SurfaceEntity.ColorSpace.BT709;
@@ -73,19 +73,19 @@
// still allowing the application to hold a reference to the SurfaceEntity.
private SubspaceNode mSubspace = null;
- // Converts SurfaceEntity's ContentSecurityLevel to an Impress ContentSecurityLevel.
+ // Converts SurfaceEntity's SurfaceProtection to an Impress ContentSecurityLevel.
private static int toImpressContentSecurityLevel(
- @ContentSecurityLevel int contentSecurityLevel) {
- switch (contentSecurityLevel) {
- case ContentSecurityLevel.NONE:
+ @SurfaceProtection int surfaceProtection) {
+ switch (surfaceProtection) {
+ case SurfaceProtection.NONE:
return ImpressApi.ContentSecurityLevel.NONE;
- case ContentSecurityLevel.PROTECTED:
+ case SurfaceProtection.PROTECTED:
return ImpressApi.ContentSecurityLevel.PROTECTED;
default:
Log.e(
"SurfaceEntityImpl",
- "Unsupported content security level: "
- + contentSecurityLevel
+ "Unsupported surface protection level: "
+ + surfaceProtection
+ ". Defaulting to NONE.");
return ImpressApi.ContentSecurityLevel.NONE;
}
@@ -117,16 +117,16 @@
EntityManager entityManager,
ScheduledExecutorService executor,
@StereoMode int stereoMode,
- CanvasShape canvasShape,
- @ContentSecurityLevel int contentSecurityLevel,
+ Shape shape,
+ @SurfaceProtection int surfaceProtection,
@SuperSampling int superSampling) {
super(context, extensions.createNode(), extensions, entityManager, executor);
mImpressApi = impressApi;
mSplitEngineSubspaceManager = splitEngineSubspaceManager;
mStereoMode = stereoMode;
- mContentSecurityLevel = contentSecurityLevel;
+ mSurfaceProtection = surfaceProtection;
mSuperSampling = superSampling;
- mCanvasShape = canvasShape;
+ mShape = shape;
setParent(parentEntity);
// System will only render Impress nodes that are parented by this subspace node.
@@ -145,12 +145,12 @@
mEntityImpressNode =
mImpressApi.createStereoSurface(
stereoMode,
- toImpressContentSecurityLevel(mContentSecurityLevel),
+ toImpressContentSecurityLevel(mSurfaceProtection),
toImpressSuperSampling(mSuperSampling));
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
}
- setCanvasShape(mCanvasShape);
+ setShape(mShape);
// The CPM node hierarchy is: Entity CPM node --- parent of ---> Subspace CPM node.
// The Impress node hierarchy is: Subspace Impress node --- parent of ---> Entity Impress
@@ -163,32 +163,32 @@
}
@Override
- public CanvasShape getCanvasShape() {
- return mCanvasShape;
+ public Shape getShape() {
+ return mShape;
}
@Override
- public void setCanvasShape(CanvasShape canvasShape) {
- mCanvasShape = canvasShape;
+ public void setShape(Shape shape) {
+ mShape = shape;
- if (mCanvasShape instanceof CanvasShape.Quad) {
- CanvasShape.Quad q = (CanvasShape.Quad) mCanvasShape;
+ if (mShape instanceof Shape.Quad) {
+ Shape.Quad q = (Shape.Quad) mShape;
try {
mImpressApi.setStereoSurfaceEntityCanvasShapeQuad(
- mEntityImpressNode, q.getWidth(), q.getHeight());
+ mEntityImpressNode, q.getExtents().getWidth(), q.getExtents().getHeight());
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
}
- } else if (mCanvasShape instanceof CanvasShape.Vr360Sphere) {
- CanvasShape.Vr360Sphere s = (CanvasShape.Vr360Sphere) mCanvasShape;
+ } else if (mShape instanceof Shape.Sphere) {
+ Shape.Sphere s = (Shape.Sphere) mShape;
try {
mImpressApi.setStereoSurfaceEntityCanvasShapeSphere(
mEntityImpressNode, s.getRadius());
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
}
- } else if (mCanvasShape instanceof CanvasShape.Vr180Hemisphere) {
- CanvasShape.Vr180Hemisphere h = (CanvasShape.Vr180Hemisphere) mCanvasShape;
+ } else if (mShape instanceof Shape.Hemisphere) {
+ Shape.Hemisphere h = (Shape.Hemisphere) mShape;
try {
mImpressApi.setStereoSurfaceEntityCanvasShapeHemisphere(
mEntityImpressNode, h.getRadius());
@@ -196,22 +196,22 @@
throw new IllegalStateException(e);
}
} else {
- throw new IllegalArgumentException("Unsupported canvas shape: " + mCanvasShape);
+ throw new IllegalArgumentException("Unsupported canvas shape: " + mShape);
}
}
@Override
public void setEdgeFeather(EdgeFeather edgeFeather) {
mEdgeFeather = edgeFeather;
- if (mEdgeFeather instanceof EdgeFeather.SmoothFeather) {
- EdgeFeather.SmoothFeather s = (EdgeFeather.SmoothFeather) mEdgeFeather;
+ if (mEdgeFeather instanceof EdgeFeather.RectangleFeather) {
+ EdgeFeather.RectangleFeather s = (EdgeFeather.RectangleFeather) mEdgeFeather;
try {
mImpressApi.setFeatherRadiusForStereoSurface(
mEntityImpressNode, s.getLeftRight(), s.getTopBottom());
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
}
- } else if (mEdgeFeather instanceof EdgeFeather.SolidEdge) {
+ } else if (mEdgeFeather instanceof EdgeFeather.NoFeathering) {
try {
mImpressApi.setFeatherRadiusForStereoSurface(mEntityImpressNode, 0.0f, 0.0f);
} catch (IllegalArgumentException e) {
@@ -251,7 +251,7 @@
@Override
public Dimensions getDimensions() {
- return mCanvasShape.getDimensions();
+ return mShape.getDimensions();
}
@Override
@@ -322,7 +322,7 @@
}
@Override
- public int getMaxCLL() {
+ public int getMaxContentLightLevel() {
return mMaxContentLightLevel;
}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
index 85ac2f0..bc6d376 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
@@ -344,9 +344,9 @@
lifecycleManager = lifecycleManager,
mockPlatformAdapter,
entityManager,
- SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
Pose.Identity,
- SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+ SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
)
}
@@ -760,7 +760,7 @@
@Test
fun activityPanelEntityLaunchActivity_callsImplLaunchActivity() {
val launchIntent = Intent(activity.applicationContext, ComponentActivity::class.java)
- activityPanelEntity.launchActivity(launchIntent, null)
+ activityPanelEntity.startActivity(launchIntent, null)
verify(mockActivityPanelEntity).launchActivity(launchIntent, null)
}
@@ -1144,14 +1144,14 @@
@Test
fun SurfaceEntity_redirectsCallsToRtEntity() {
- surfaceEntity.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
- verify(mockSurfaceEntity).stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
+ surfaceEntity.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+ verify(mockSurfaceEntity).stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
@Suppress("UNUSED_VARIABLE") var unusedMode = surfaceEntity.stereoMode
verify(mockSurfaceEntity).stereoMode
- surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
- verify(mockSurfaceEntity).canvasShape = any()
+ surfaceEntity.shape = SurfaceEntity.Shape.Sphere(1.0f)
+ verify(mockSurfaceEntity).shape = any()
// no equivalent test for getter - that just returns the Kotlin object for now.
}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
index 7e60bed..ff50189 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
@@ -162,7 +162,7 @@
}
@Test
- public void activityPanelEntityLaunchActivity_callsActivityPanel() {
+ public void activityPanelEntityStartActivity_callsActivityPanel() {
ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
Intent launchIntent = mActivityController.getIntent();
activityPanelEntity.launchActivity(launchIntent, null);
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
index 236888b..9cff1b8 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
@@ -87,6 +87,7 @@
import androidx.xr.runtime.internal.TextureResource;
import androidx.xr.runtime.internal.TextureSampler;
import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.FloatSize2d;
import androidx.xr.runtime.math.Pose;
import androidx.xr.runtime.math.Quaternion;
import androidx.xr.runtime.math.Vector3;
@@ -2301,8 +2302,8 @@
mRuntime.createSurfaceEntity(
SurfaceEntity.StereoMode.SIDE_BY_SIDE,
new Pose(),
- new SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
- SurfaceEntity.ContentSecurityLevel.NONE,
+ new SurfaceEntity.Shape.Quad(new FloatSize2d(1.0f, 1.0f)),
+ SurfaceEntity.SurfaceProtection.NONE,
SurfaceEntity.SuperSampling.DEFAULT,
mRuntime.getActivitySpaceRootImpl()));
}
@@ -2318,8 +2319,8 @@
mRuntime.createSurfaceEntity(
SurfaceEntity.StereoMode.SIDE_BY_SIDE,
new Pose(),
- new SurfaceEntity.CanvasShape.Quad(kTestWidth, kTestHeight),
- SurfaceEntity.ContentSecurityLevel.NONE,
+ new SurfaceEntity.Shape.Quad(new FloatSize2d(kTestWidth, kTestHeight)),
+ SurfaceEntity.SurfaceProtection.NONE,
SurfaceEntity.SuperSampling.DEFAULT,
mRuntime.getActivitySpaceRootImpl());
@@ -2334,8 +2335,8 @@
mRuntime.createSurfaceEntity(
SurfaceEntity.StereoMode.TOP_BOTTOM,
new Pose(),
- new SurfaceEntity.CanvasShape.Vr360Sphere(kTestSphereRadius),
- SurfaceEntity.ContentSecurityLevel.NONE,
+ new SurfaceEntity.Shape.Sphere(kTestSphereRadius),
+ SurfaceEntity.SurfaceProtection.NONE,
SurfaceEntity.SuperSampling.DEFAULT,
mRuntime.getActivitySpaceRootImpl());
@@ -2350,8 +2351,8 @@
mRuntime.createSurfaceEntity(
SurfaceEntity.StereoMode.MONO,
new Pose(),
- new SurfaceEntity.CanvasShape.Vr180Hemisphere(kTestHemisphereRadius),
- SurfaceEntity.ContentSecurityLevel.NONE,
+ new SurfaceEntity.Shape.Hemisphere(kTestHemisphereRadius),
+ SurfaceEntity.SurfaceProtection.NONE,
SurfaceEntity.SuperSampling.DEFAULT,
mRuntime.getActivitySpaceRootImpl());
@@ -2399,10 +2400,10 @@
assertThat(sphereData.getSurface()).isEqualTo(surfaceEntitySphere.getSurface());
assertThat(hemisphereData.getSurface()).isEqualTo(surfaceEntityHemisphere.getSurface());
- // Check that calls to set the CanvasShape and StereoMode after construction call through
+ // Check that calls to set the Shape and StereoMode after construction call through
// Change the Quad to a Sphere
- surfaceEntityQuad.setCanvasShape(
- new SurfaceEntity.CanvasShape.Vr360Sphere(kTestSphereRadius));
+ surfaceEntityQuad.setShape(
+ new SurfaceEntity.Shape.Sphere(kTestSphereRadius));
// change the StereoMode to Top/Bottom from Side/Side
surfaceEntityQuad.setStereoMode(SurfaceEntity.StereoMode.TOP_BOTTOM);
quadData =
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
index a235f1f..5dd136c 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
@@ -31,7 +31,8 @@
import androidx.xr.runtime.internal.PixelDimensions;
import androidx.xr.runtime.internal.Space;
import androidx.xr.runtime.internal.SurfaceEntity;
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape;
+import androidx.xr.runtime.internal.SurfaceEntity.Shape;
+import androidx.xr.runtime.math.FloatSize2d;
import androidx.xr.runtime.math.Pose;
import androidx.xr.runtime.math.Quaternion;
import androidx.xr.runtime.math.Vector3;
@@ -77,7 +78,7 @@
@Before
public void setUp() {
mImpressApi = new FakeImpressApiImpl();
- createDefaultSurfaceEntity(new SurfaceEntity.CanvasShape.Quad(1f, 1f));
+ createDefaultSurfaceEntity(new SurfaceEntity.Shape.Quad(new FloatSize2d(1f, 1f)));
}
@After
@@ -88,7 +89,7 @@
}
}
- private SurfaceEntityImpl createDefaultSurfaceEntity(CanvasShape canvasShape) {
+ private SurfaceEntityImpl createDefaultSurfaceEntity(Shape shape) {
XrExtensions xrExtensions = XrExtensionsProvider.getXrExtensions();
SplitEngineSubspaceManager splitEngineSubspaceManager =
@@ -121,7 +122,7 @@
int stereoMode = SurfaceEntity.StereoMode.MONO;
Pose pose = Pose.Identity;
- int contentSecurityLevel = 0;
+ int surfaceProtection = 0;
int useSuperSampling = 0;
mSurfaceEntity =
@@ -134,8 +135,8 @@
mEntityManager,
executor,
stereoMode,
- canvasShape,
- contentSecurityLevel,
+ shape,
+ surfaceProtection,
useSuperSampling);
mSurfaceEntity.setPose(pose, Space.PARENT);
@@ -162,28 +163,28 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
- public void setCanvasShape_setsCanvasShape() {
- SurfaceEntity.CanvasShape expectedCanvasShape =
- new SurfaceEntity.CanvasShape.Quad(12f, 12f);
- mSurfaceEntity.setCanvasShape(expectedCanvasShape);
- SurfaceEntity.CanvasShape canvasShape = mSurfaceEntity.getCanvasShape();
+ public void setShape_setsShape() {
+ SurfaceEntity.Shape expectedShape =
+ new SurfaceEntity.Shape.Quad(new FloatSize2d(12f, 12f));
+ mSurfaceEntity.setShape(expectedShape);
+ SurfaceEntity.Shape shape = mSurfaceEntity.getShape();
- assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
- assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+ assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+ assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
- expectedCanvasShape = new SurfaceEntity.CanvasShape.Vr360Sphere(11f);
- mSurfaceEntity.setCanvasShape(expectedCanvasShape);
- canvasShape = mSurfaceEntity.getCanvasShape();
+ expectedShape = new SurfaceEntity.Shape.Sphere(11f);
+ mSurfaceEntity.setShape(expectedShape);
+ shape = mSurfaceEntity.getShape();
- assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
- assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+ assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+ assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
- expectedCanvasShape = new SurfaceEntity.CanvasShape.Vr180Hemisphere(10f);
- mSurfaceEntity.setCanvasShape(expectedCanvasShape);
- canvasShape = mSurfaceEntity.getCanvasShape();
+ expectedShape = new SurfaceEntity.Shape.Hemisphere(10f);
+ mSurfaceEntity.setShape(expectedShape);
+ shape = mSurfaceEntity.getShape();
- assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
- assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+ assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+ assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
}
@Ignore // b/428211243 this test currently leaks android.view.Surface
@@ -205,7 +206,7 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
public void dispose_supports_reentry() {
- CanvasShape.Quad quadShape = new CanvasShape.Quad(1.0f, 1.0f); // 1m x 1m local
+ Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(1.0f, 1.0f)); // 1m x 1m local
mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
// Note that we don't test that dispose prevents manipulating other properties because that
@@ -220,19 +221,20 @@
float kFeatherRadiusX = 0.14f;
float kFeatherRadiusY = 0.28f;
SurfaceEntity.EdgeFeather expectedFeather =
- new SurfaceEntity.EdgeFeather.SmoothFeather(kFeatherRadiusX, kFeatherRadiusY);
+ new SurfaceEntity.EdgeFeather.RectangleFeather(kFeatherRadiusX, kFeatherRadiusY);
mSurfaceEntity.setEdgeFeather(expectedFeather);
SurfaceEntity.EdgeFeather returnedFeather = mSurfaceEntity.getEdgeFeather();
assertThat(returnedFeather).isEqualTo(expectedFeather);
// Apply Fake Impress checks
FakeImpressApiImpl.StereoSurfaceEntityData surfaceEntityData =
- mImpressApi.getStereoSurfaceEntities().get(mSurfaceEntity.getEntityImpressNode());
+ mImpressApi
+ .getStereoSurfaceEntities().get(mSurfaceEntity.getEntityImpressNode());
assertThat(surfaceEntityData.getFeatherRadiusX()).isEqualTo(kFeatherRadiusX);
assertThat(surfaceEntityData.getFeatherRadiusY()).isEqualTo(kFeatherRadiusY);
- // Set back to SolidEdge to simulate turning feathering off
- expectedFeather = new SurfaceEntity.EdgeFeather.SolidEdge();
+ // Set back to NoFeathering to simulate turning feathering off
+ expectedFeather = new SurfaceEntity.EdgeFeather.NoFeathering();
mSurfaceEntity.setEdgeFeather(expectedFeather);
returnedFeather = mSurfaceEntity.getEdgeFeather();
assertThat(surfaceEntityData.getFeatherRadiusX()).isEqualTo(0.0f);
@@ -250,7 +252,7 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
public void getPerceivedResolution_quadInFront_returnsSuccess() {
- CanvasShape.Quad quadShape = new CanvasShape.Quad(2.0f, 1.0f); // 2m wide, 1m high
+ Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(2.0f, 1.0f)); // 2m wide, 1m high
// Recreate mSurfaceEntity with the specific shape for this test
mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
setupDefaultMockCameraView();
@@ -270,7 +272,7 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
public void getPerceivedResolution_sphereInFront_returnsSuccess() {
- CanvasShape.Vr360Sphere sphereShape = new CanvasShape.Vr360Sphere(1.0f); // radius 1m
+ Shape.Sphere sphereShape = new Shape.Sphere(1.0f); // radius 1m
mSurfaceEntity = createDefaultSurfaceEntity(sphereShape);
setupDefaultMockCameraView();
@@ -290,7 +292,7 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
public void getPerceivedResolution_quadTooClose_returnsEntityTooClose() {
- CanvasShape.Quad quadShape = new CanvasShape.Quad(2.0f, 1.0f);
+ Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(2.0f, 1.0f));
mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
setupDefaultMockCameraView();
@@ -306,7 +308,7 @@
@Ignore // b/428211243 this test currently leaks android.view.Surface
@Test
public void getPerceivedResolution_quadWithScale_calculatesCorrectly() {
- CanvasShape.Quad quadShape = new CanvasShape.Quad(1.0f, 1.0f); // 1m x 1m local
+ Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(1.0f, 1.0f)); // 1m x 1m local
mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
setupDefaultMockCameraView();
@@ -318,7 +320,7 @@
PerceivedResolutionResult.Success successResult =
(PerceivedResolutionResult.Success) result;
- // The width and height are flipped because perceievedResolution calculations will
+ // The width and height are flipped because perceivedResolution calculations will
// always place the largest dimension as the width, and the second as height.
Truth.assertThat(successResult.getPerceivedResolution().width).isEqualTo(750);
Truth.assertThat(successResult.getPerceivedResolution().height).isEqualTo(500);