From 42406de97e5856848a8dcbe42db599a9eab16c44 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Mon, 15 Dec 2025 17:41:10 +0000 Subject: [PATCH 01/11] Prevent source information stack traces downgrading to group keys In cases when source information is not present at runtime (production app builds), the `SourceInformation` flavour of stack traces no longer emits `GroupKeys` based stack traces. Test: ErrorTraceTests Relnote: "Prevent source information stack traces downgrading to group keys in cases when source information is not present at runtime." Change-Id: If371286516f3e0cffecca02926084ea63d2793e5 --- .../androidx/compose/runtime/GapComposer.kt | 8 ++--- .../compose/runtime/changelist/Operation.kt | 5 ++- .../changelist/OperationArgContainer.kt | 3 ++ .../runtime/tooling/ComposeStackTrace.kt | 28 +++++++++------ .../tooling/CompositionErrorContext.kt | 3 ++ ...iagnosticComposeException.jvmAndAndroid.kt | 2 +- .../runtime/tooling/ErrorTraceTests.kt | 34 ++++++++++++++++++- 7 files changed, 66 insertions(+), 17 deletions(-) diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt index 453fec9d70a11..cfce9d5dd365d 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt @@ -54,7 +54,6 @@ import androidx.compose.runtime.tooling.buildTrace import androidx.compose.runtime.tooling.findLocation import androidx.compose.runtime.tooling.findSubcompositionContextGroup import androidx.compose.runtime.tooling.traceForGroup -import kotlin.collections.plus import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmInline @@ -254,7 +253,7 @@ internal class GapComposer( private var childrenComposing: Int = 0 private var compositionToken: Int = 0 - private var sourceMarkersEnabled = + internal var sourceMarkersEnabled = parentContext.collectingSourceInformation || parentContext.collectingCallByInformation private val derivedStateObserver = @@ -2503,7 +2502,7 @@ internal class GapComposer( stackTraceForGroup(groupIndex, dataIndex) + parentStackTrace() } ?: emptyList() - return ComposeStackTrace(stackTrace) + return ComposeStackTrace(stackTrace, sourceMarkersEnabled) } @OptIn(ComposeToolingApi::class) @@ -2514,7 +2513,8 @@ internal class GapComposer( addAll(writer.buildTrace()) addAll(reader.buildTrace()) addAll(parentStackTrace()) - } + }, + sourceMarkersEnabled, ) } else { null diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt index befffabd54a5b..3936674cf1031 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt @@ -1134,7 +1134,7 @@ private fun Throwable.attachComposeStackTrace( listOf(head.copy(groupOffset = offset)) + tail } } - ComposeStackTrace(trace + parentTrace) + ComposeStackTrace(trace + parentTrace, errorContext.sourceInformationEnabled) } } @@ -1149,5 +1149,8 @@ private fun OperationErrorContext.withCurrentStackTrace(slots: SlotWriter): Oper return slots.buildTrace(currentOffset, currentGroup, slots.parent(currentGroup)) + parentTrace } + + override val sourceInformationEnabled: Boolean + get() = parent.sourceInformationEnabled } } diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/OperationArgContainer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/OperationArgContainer.kt index 4ab8082a9c455..f350745b9f625 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/OperationArgContainer.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/OperationArgContainer.kt @@ -29,6 +29,9 @@ internal interface OperationArgContainer { /** Error context to stitch operation execution in case an error is thrown. */ internal interface OperationErrorContext { + /** Whether the stack trace is expected to contain source information. */ + val sourceInformationEnabled: Boolean + /** * Create a stack trace from the root of the enclosing context (composition or slot table) to a * child of the current group that is located at the slot specified by [currentOffset]. Current diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTrace.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTrace.kt index 1b0a9aec37759..6e6379be52d41 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTrace.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTrace.kt @@ -30,6 +30,8 @@ import androidx.compose.runtime.rootKey import androidx.compose.runtime.snapshots.fastAny import androidx.compose.runtime.snapshots.fastForEach import androidx.compose.runtime.snapshots.fastNone +import androidx.compose.runtime.tooling.ComposeStackTraceMode.Companion.GroupKeys +import androidx.compose.runtime.tooling.ComposeStackTraceMode.Companion.None import kotlin.jvm.JvmInline /** @@ -66,10 +68,10 @@ public value class ComposeStackTraceMode private constructor(private val value: public val GroupKeys: ComposeStackTraceMode = ComposeStackTraceMode(1) /** - * Collects source information for stack trace purposes. When this flag is enabled, - * composition will record source information at runtime. When crash occurs, Compose will - * append a suppressed exception that contains a stack trace pointing to the place in - * composition closest to the crash. + * Collects a stack trace based on source information embedded by Compose compiler. When + * this flag is enabled, composition will record source information at runtime. When crash + * occurs, Compose will append a suppressed exception that contains a stack trace pointing + * to the place in composition closest to the crash. * * Note that: * - Recording source information introduces additional performance overhead, so this option @@ -107,11 +109,10 @@ public value class ComposeStackTraceMode private constructor(private val value: */ internal expect class DiagnosticComposeException(trace: ComposeStackTrace) : RuntimeException -internal class ComposeStackTrace(val frames: List) { - @OptIn(ComposeToolingApi::class) - val hasSourceInformation - get() = frames.fastAny { it.sourceInfo != null } -} +internal class ComposeStackTrace( + val frames: List, + val hasSourceInformation: Boolean, +) @OptIn(ComposeToolingApi::class) internal data class ComposeStackTraceFrame( @@ -120,13 +121,20 @@ internal data class ComposeStackTraceFrame( val groupOffset: Int?, ) +@OptIn(ComposeToolingApi::class) internal fun Throwable.tryAttachComposeStackTrace(trace: () -> ComposeStackTrace?): Boolean { var result = false if (suppressedExceptions.fastNone { it is DiagnosticComposeException }) { val traceException = try { val stackTrace = trace() - result = stackTrace != null && stackTrace.frames.isNotEmpty() + result = + stackTrace != null && + if (stackTrace.hasSourceInformation) { + stackTrace.frames.fastAny { it.sourceInfo != null } + } else { + stackTrace.frames.isNotEmpty() + } if (result) DiagnosticComposeException(stackTrace!!) else null } catch (e: Throwable) { // Attach the exception thrown while collecting trace. diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionErrorContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionErrorContext.kt index 6c368427befc7..4875f237323df 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionErrorContext.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionErrorContext.kt @@ -70,6 +70,9 @@ internal class CompositionErrorContextImpl(private val composer: GapComposer) : override fun buildStackTrace(currentOffset: Int?): List = composer.parentStackTrace() + override val sourceInformationEnabled: Boolean + get() = composer.sourceMarkersEnabled + companion object Key : CoroutineContext.Key { override fun toString(): String = "CompositionErrorContext" } diff --git a/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/tooling/DiagnosticComposeException.jvmAndAndroid.kt b/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/tooling/DiagnosticComposeException.jvmAndAndroid.kt index 53107a1f47843..3a5b28ec1961c 100644 --- a/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/tooling/DiagnosticComposeException.jvmAndAndroid.kt +++ b/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/tooling/DiagnosticComposeException.jvmAndAndroid.kt @@ -42,7 +42,7 @@ actual constructor(private val trace: ComposeStackTrace) : RuntimeException() { if (trace.hasSourceInformation) { buildString { appendLine("Composition stack when thrown:") - appendStackTrace(trace) + appendSourceInformationStackTrace(trace) } } else { "Composition stack when thrown:" diff --git a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/tooling/ErrorTraceTests.kt b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/tooling/ErrorTraceTests.kt index 6417ab01d339b..1275e13183fc1 100644 --- a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/tooling/ErrorTraceTests.kt +++ b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/tooling/ErrorTraceTests.kt @@ -18,6 +18,7 @@ package androidx.compose.runtime.tooling import androidx.compose.runtime.Composable import androidx.compose.runtime.Composer +import androidx.compose.runtime.CompositionImpl import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ExperimentalComposeRuntimeApi import androidx.compose.runtime.ReusableContent @@ -694,6 +695,31 @@ class ErrorTraceTests { state = false advance() } + + @Test + fun setContentNoSourceInformation() { + Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation) + assertTrace(expected = null) { + compositionTest { + var state by mutableStateOf(false) + compose { + InlineLinear { + if (state) { + throwTestException() + } + } + } + + // Remove source information stored for this composition + // `Composer.disableSourceInformation` does not work here as it is used for + // configuring stack trace mode as well. + (composition as CompositionImpl).slotTable.sourceInformationMap = null + + state = true + advance() + } + } + } } private fun throwTestException(): Nothing = throw TestComposeException() @@ -715,7 +741,7 @@ private fun exceptionTest( Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto) } -private fun assertTrace(expected: List, block: () -> Unit) { +private fun assertTrace(expected: List?, block: () -> Unit) { var exception: TestComposeException? = null try { block() @@ -726,6 +752,12 @@ private fun assertTrace(expected: List, block: () -> Unit) { val composeTrace = exception.suppressedExceptions.firstOrNull { it is DiagnosticComposeException } + if (expected == null && composeTrace == null) { + return + } + if (expected == null) { + throw exception + } if (composeTrace == null) { throw exception } From 1f00727deab50acf607ca63fb3d04df7ff96326b Mon Sep 17 00:00:00 2001 From: Andrew Bailey Date: Fri, 19 Dec 2025 17:05:53 -0500 Subject: [PATCH 02/11] Fix incorrect usage of rememberUpdatedState rememberViewModelStoreNavEntryDecorator is incorrectly using rememberUpdatedState. The current setup is ignoring the result of rememberUpdatedState and will never reconstruct the ViewModelStoreNavEntryDecorator in response to the lambda argument changing. This means that users changing the value of `removeViewModelStoreOnPop` will never see the new lambda argument used as the first one is captured indefinitely. This CL fixes this by invoking the latest value from the rememberUpdatedState instance. Test: testChangeRemoveViewModelStoreOnPop Change-Id: Ie9576520632150dcd34c1bbf45b6b175d09a2f98 --- .../ViewModelStoreNavEntryDecoratorTest.kt | 57 +++++++++++++++++++ .../ViewModelStoreNavEntryDecorator.kt | 9 ++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidDeviceTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecoratorTest.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidDeviceTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecoratorTest.kt index 073e3fcf98860..7243196e4acd8 100644 --- a/lifecycle/lifecycle-viewmodel-navigation3/src/androidDeviceTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecoratorTest.kt +++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidDeviceTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecoratorTest.kt @@ -16,8 +16,11 @@ package androidx.lifecycle.viewmodel.navigation3 +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.kruth.assertThat import androidx.kruth.assertWithMessage @@ -197,10 +200,64 @@ class ViewModelStoreNavEntryDecoratorTest { composeTestRule.waitForIdle() assertThat(viewModel.savedStateHandle).isNotNull() } + + @Test + fun testChangeRemoveViewModelStoreOnPop() { + val viewModels = mutableMapOf() + var removeViewModelStoreOnPop by mutableStateOf(true) + + fun createNaveEntry(key: Int) = NavEntry(key) { viewModels[key] = viewModel() } + + val entry1 = createNaveEntry(1) + val entry2 = createNaveEntry(2) + val entry3 = createNaveEntry(3) + val backStack = mutableStateListOf(entry1, entry2, entry3) + + composeTestRule.setContent { + val decorated = + rememberDecoratedNavEntries( + entries = backStack, + entryDecorators = + listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator( + removeViewModelStoreOnPop = + if (removeViewModelStoreOnPop) { + { true } + } else { + { false } + } + ), + ), + ) + + decorated.forEach { entry -> entry.Content() } + } + + assertThat(viewModels.mapValues { (_, viewModel) -> viewModel.isCleared }) + .isEqualTo(mapOf(1 to false, 2 to false, 3 to false)) + + backStack.removeAt(backStack.lastIndex) + composeTestRule.waitForIdle() + assertThat(viewModels.mapValues { (_, viewModel) -> viewModel.isCleared }) + .isEqualTo(mapOf(1 to false, 2 to false, 3 to true)) + + removeViewModelStoreOnPop = false + backStack.removeAt(backStack.lastIndex) + composeTestRule.waitForIdle() + assertThat(viewModels.mapValues { (_, viewModel) -> viewModel.isCleared }) + .isEqualTo(mapOf(1 to false, 2 to false, 3 to true)) + } } class MyViewModel : ViewModel() { var myArg = "default" + var isCleared = false + private set + + override fun onCleared() { + isCleared = true + } } var globalViewModelCount = 0 diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/commonMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecorator.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/commonMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecorator.kt index 9ffd063720343..69f66417ada08 100644 --- a/lifecycle/lifecycle-viewmodel-navigation3/src/commonMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecorator.kt +++ b/lifecycle/lifecycle-viewmodel-navigation3/src/commonMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavEntryDecorator.kt @@ -64,11 +64,10 @@ public fun rememberViewModelStoreNavEntryDecorator( ViewModelStoreNavEntryDecoratorDefaults.removeViewModelStoreOnPop(), ): ViewModelStoreNavEntryDecorator { val currentRemoveViewModelStoreOnPop = rememberUpdatedState(removeViewModelStoreOnPop) - return remember(viewModelStoreOwner, currentRemoveViewModelStoreOnPop) { - ViewModelStoreNavEntryDecorator( - viewModelStoreOwner.viewModelStore, - removeViewModelStoreOnPop, - ) + return remember(viewModelStoreOwner) { + ViewModelStoreNavEntryDecorator(viewModelStoreOwner.viewModelStore) { + currentRemoveViewModelStoreOnPop.value.invoke() + } } } From 0def1a04bfc4535c77f93a6b5deccf6e96b398e5 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Fri, 14 Nov 2025 14:40:26 +0100 Subject: [PATCH 03/11] Extract navigation-common hosts tests to common Relnote: N/A Test: N/A Change-Id: Id8997f6110b86efc00bfac1cce7f3f4b7cc3fa05 --- navigation/navigation-common/build.gradle | 2 +- .../serialization/RouteFilledTest.kt | 63 --- .../AndroidNavigatorProviderTest.kt | 63 +++ .../AndroidNavArgumentGeneratorTest.kt | 411 +++++++++++++++ .../AndroidNavTypeConverterTest.kt | 101 ++++ .../navigation/NavigatorProviderTest.kt | 58 +- .../serialization/NavArgumentGeneratorTest.kt | 498 +++--------------- .../serialization/NavTypeConverterTest.kt | 79 +-- .../serialization/RoutePatternTest.kt | 45 +- .../androidx/navigation/NavHostController.kt | 3 +- navigation/navigation-testing/api/current.txt | 2 + .../api/restricted_current.txt | 2 + .../navigation-testing/bcv/native/current.txt | 14 + navigation/navigation-testing/build.gradle | 4 + .../testing/TestNavHostController.android.kt | 4 +- .../testing/TestNavigatorState.android.kt | 13 +- .../testing/TestNavHostController.kt | 27 + .../navigation/testing/TestNavigatorState.kt | 49 ++ .../TestNavHostController.nonAndroid.kt | 26 + .../testing/TestNavigatorState.nonAndroid.kt | 40 ++ 20 files changed, 843 insertions(+), 661 deletions(-) create mode 100644 navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/AndroidNavigatorProviderTest.kt create mode 100644 navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavArgumentGeneratorTest.kt create mode 100644 navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavTypeConverterTest.kt rename navigation/navigation-common/src/{androidHostTest => commonTest}/kotlin/androidx/navigation/NavigatorProviderTest.kt (75%) rename navigation/navigation-common/src/{androidHostTest => commonTest}/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt (69%) rename navigation/navigation-common/src/{androidHostTest => commonTest}/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt (91%) rename navigation/navigation-common/src/{androidHostTest => commonTest}/kotlin/androidx/navigation/serialization/RoutePatternTest.kt (91%) create mode 100644 navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavHostController.kt create mode 100644 navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavigatorState.kt create mode 100644 navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavHostController.nonAndroid.kt create mode 100644 navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavigatorState.nonAndroid.kt diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle index 1b81ebb94ff1d..80e78945dd6a8 100644 --- a/navigation/navigation-common/build.gradle +++ b/navigation/navigation-common/build.gradle @@ -68,6 +68,7 @@ androidXMultiplatform { commonTest.dependencies { implementation(libs.kotlinTest) implementation(project(":kruth:kruth")) + implementation(project(":navigation:navigation-testing")) } nonAndroidMain.dependsOn(commonMain) @@ -85,7 +86,6 @@ androidXMultiplatform { } androidHostTest.dependencies { - implementation(project(":navigation:navigation-testing")) implementation("androidx.arch.core:core-testing:2.2.0") implementation(libs.junit) implementation(libs.mockitoCore4) diff --git a/navigation/navigation-common/src/androidDeviceTest/kotlin/androidx/navigation/serialization/RouteFilledTest.kt b/navigation/navigation-common/src/androidDeviceTest/kotlin/androidx/navigation/serialization/RouteFilledTest.kt index 005f408892f0c..61b146648cbd6 100644 --- a/navigation/navigation-common/src/androidDeviceTest/kotlin/androidx/navigation/serialization/RouteFilledTest.kt +++ b/navigation/navigation-common/src/androidDeviceTest/kotlin/androidx/navigation/serialization/RouteFilledTest.kt @@ -20,23 +20,14 @@ import android.os.Bundle import androidx.navigation.NamedNavArgument import androidx.navigation.NavType import androidx.navigation.navArgument -import com.google.common.truth.Truth.assertThat import kotlin.test.assertFailsWith -import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -const val PATH_SERIAL_NAME = "www.test.com" - @RunWith(JUnit4::class) class RouteFilledTest { @@ -875,62 +866,8 @@ private fun assertThatRouteFilledFrom( return generateRouteWithArgs(obj, typeMap) } -internal fun String.isEqualTo(other: String) { - assertThat(this).isEqualTo(other) -} - -@Serializable -@SerialName(PATH_SERIAL_NAME) -private class ClassWithCompanionObject(val arg: Int) { - companion object TestObject -} - -@Serializable -@SerialName(PATH_SERIAL_NAME) -private class ClassWithCompanionParam(val arg: Int) { - companion object { - val companionVal: String = "hello" - } -} - -@Serializable @SerialName(PATH_SERIAL_NAME) internal object TestObject - -@Serializable -@SerialName(PATH_SERIAL_NAME) -internal object TestObjectWithArg { - val arg: Int = 0 -} - -@Serializable -private sealed class SealedClass { - abstract val arg: Int - - @Serializable - @SerialName(PATH_SERIAL_NAME) - // same value for arg and arg2 - class TestClass(val arg2: Int) : SealedClass() { - override val arg: Int - get() = arg2 - } -} - @JvmInline @Serializable @SerialName(PATH_SERIAL_NAME) value class TestValueClass(val arg: Int) -private class CustomSerializerClass(val longArg: Long) - -private class CustomSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG) - - override fun serialize(encoder: Encoder, value: CustomSerializerClass) = - encoder.encodeLong(value.longArg) - - override fun deserialize(decoder: Decoder): CustomSerializerClass = - CustomSerializerClass(decoder.decodeLong()) -} - -private interface TestInterface - private fun nullableIntArgument(name: String, hasDefaultValue: Boolean = false) = navArgument(name) { type = InternalNavType.IntNullableType diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/AndroidNavigatorProviderTest.kt b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/AndroidNavigatorProviderTest.kt new file mode 100644 index 0000000000000..3113600946748 --- /dev/null +++ b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/AndroidNavigatorProviderTest.kt @@ -0,0 +1,63 @@ +/* + * 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. + */ + +package androidx.navigation + +import androidx.kruth.assertThat +import kotlin.test.Test +import kotlin.test.fail + +class AndroidNavigatorProviderTest { + + @Test + fun addWithExplicitNameGetWithExplicitName() { + val provider = NavigatorProvider() + val navigator = EmptyNavigator() + provider.addNavigator("name", navigator) + + assertThat(provider.getNavigator("name")).isEqualTo(navigator) + try { + provider.getNavigator(EmptyNavigator::class.java) + fail("getNavigator(Class) with an invalid name should cause an IllegalStateException") + } catch (e: IllegalStateException) { + // Expected + } + } + + @Test + fun addWithExplicitNameGetWithMissingAnnotationName() { + val provider = NavigatorProvider() + val navigator = NoNameNavigator() + provider.addNavigator("name", navigator) + try { + provider.getNavigator(NoNameNavigator::class.java) + fail( + "getNavigator(Class) with no @Navigator.Name should cause an " + + "IllegalArgumentException" + ) + } catch (e: IllegalArgumentException) { + // Expected + } + } + + @Test + fun addWithAnnotationNameGetWithAnnotationName() { + val provider = NavigatorProvider() + val navigator = EmptyNavigator() + provider.addNavigator(navigator) + assertThat(provider.getNavigator(EmptyNavigator::class.java)).isEqualTo(navigator) + } +} diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavArgumentGeneratorTest.kt b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavArgumentGeneratorTest.kt new file mode 100644 index 0000000000000..3a440d7ccb6c7 --- /dev/null +++ b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavArgumentGeneratorTest.kt @@ -0,0 +1,411 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.navigation.serialization + +import android.os.Parcel +import android.os.Parcelable +import androidx.kruth.assertThat +import androidx.navigation.CollectionNavType +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavType +import androidx.navigation.navArgument +import androidx.navigation.serialization.NavArgumentGeneratorTest.TestEnum +import androidx.navigation.serialization.NavArgumentGeneratorTest.TestEnumCustomSerialName +import androidx.savedstate.SavedState +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +class AndroidNavArgumentGeneratorTest { + + @Test + fun convertToEnumList() { + @Serializable class TestClass(val arg: List) + + val converted = serializer().generateNavArguments() + val expected = + navArgument("arg") { + type = InternalAndroidNavType.EnumListType(TestEnum::class.java) + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToEnumListNullable() { + @Serializable class TestClass(val arg: List?) + + val converted = serializer().generateNavArguments() + val expected = + navArgument("arg") { + type = InternalAndroidNavType.EnumListType(TestEnum::class.java) + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToParcelable() { + @Serializable + class TestParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + } + + @Serializable class TestClass(val arg: TestParcelable) + + val navType = + object : NavType(false) { + override fun put(bundle: SavedState, key: String, value: TestParcelable) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = TestParcelable() + } + + val converted = + serializer().generateNavArguments(mapOf(typeOf() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToParcelableNullable() { + @Serializable + class TestParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + } + + @Serializable class TestClass(val arg: TestParcelable?) + + val navType = + object : NavType(true) { + override fun put(bundle: SavedState, key: String, value: TestParcelable?) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = TestParcelable() + } + + val converted = + serializer() + .generateNavArguments(mapOf(typeOf() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToParcelableArray() { + @Serializable + class TestParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + } + + @Serializable class TestClass(val arg: Array) + + val navType = + object : NavType>(false) { + override fun put(bundle: SavedState, key: String, value: Array) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = emptyArray() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf>() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToParcelableArrayNullable() { + @Serializable + class TestParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + } + + @Serializable class TestClass(val arg: Array?) + + val navType = + object : NavType>(true) { + override fun put(bundle: SavedState, key: String, value: Array) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = emptyArray() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf?>() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToSerializable() { + @Serializable class TestSerializable : java.io.Serializable + + @Serializable class TestClass(val arg: TestSerializable) + + val navType = + object : NavType(false) { + override fun put(bundle: SavedState, key: String, value: TestSerializable) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = TestSerializable() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToSerializableNullable() { + @Serializable class TestSerializable : java.io.Serializable + + @Serializable class TestClass(val arg: TestSerializable?) + + val navType = + object : NavType(true) { + override fun put(bundle: SavedState, key: String, value: TestSerializable) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = TestSerializable() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToSerializableArray() { + @Serializable class TestSerializable : java.io.Serializable + + @Serializable class TestClass(val arg: Array) + + val navType = + object : NavType>(false) { + override fun put(bundle: SavedState, key: String, value: Array) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = emptyArray() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf>() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToSerializableArrayNullable() { + @Serializable class TestSerializable : java.io.Serializable + + @Serializable class TestClass(val arg: Array?) + + val navType = + object : NavType>(true) { + override fun put(bundle: SavedState, key: String, value: Array) {} + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = emptyArray() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf?>() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToEnum() { + @Serializable class TestClass(val arg: TestEnum) + + val expected = + navArgument("arg") { + type = NavType.EnumType(TestEnum::class.java) + nullable = false + } + val converted = serializer().generateNavArguments() + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToTopLevelEnum() { + @Serializable class TestClass(val arg: TestTopLevelEnum) + + val expected = + navArgument("arg") { + type = NavType.EnumType(TestTopLevelEnum::class.java) + nullable = false + } + val converted = serializer().generateNavArguments() + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToEnumNullable() { + @Serializable class TestClass(val arg: TestEnum?) + + @Suppress("UNCHECKED_CAST") + val expected = + navArgument("arg") { + type = + InternalAndroidNavType.EnumNullableType(TestEnum::class.java as Class?>) + nullable = true + } + val converted = serializer().generateNavArguments() + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToNestedEnum() { + @Serializable class TestClass(val arg: EnumWrapper.NestedEnum) + + val expected = + navArgument("arg") { + type = NavType.EnumType(EnumWrapper.NestedEnum::class.java) + nullable = false + } + val converted = serializer().generateNavArguments() + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToEnumOverriddenSerialNameIllegal() { + @Serializable class TestClass(val arg: TestEnumCustomSerialName) + + val exception = + assertFailsWith { + serializer().generateNavArguments() + } + assertThat(exception.message) + .isEqualTo( + "Cannot find class with name \"MyCustomSerialName\". Ensure that the " + + "serialName for this argument is the default fully qualified name." + + "\nIf the build is minified, try annotating the Enum class " + + "with \"androidx.annotation.Keep\" to ensure the Enum is not removed." + ) + } + + @Test + fun convertToEnumArray() { + @Serializable class TestClass(val arg: Array) + val navType = + object : CollectionNavType>(false) { + override fun put(bundle: SavedState, key: String, value: Array) {} + + override fun serializeAsValues(value: Array) = emptyList() + + override fun emptyCollection(): Array = emptyArray() + + override fun get(bundle: SavedState, key: String) = null + + override fun parseValue(value: String) = emptyArray() + } + val converted = + serializer() + .generateNavArguments(mapOf(typeOf>() to navType)) + val expected = + navArgument("arg") { + type = navType + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + // writing our own assert so we don't need to override NamedNavArgument's equals + // and hashcode which will need to be public api. + private fun assertThat(actual: List) = actual + + @Serializable + private class EnumWrapper { + enum class NestedEnum { + ONE, + TWO, + } + } +} + +enum class TestTopLevelEnum { + TEST +} diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavTypeConverterTest.kt b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavTypeConverterTest.kt new file mode 100644 index 0000000000000..ea9afed0148db --- /dev/null +++ b/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/AndroidNavTypeConverterTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.navigation.serialization + +import android.os.Parcel +import android.os.Parcelable +import androidx.kruth.assertThat +import androidx.navigation.NavType +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +class AndroidNavTypeConverterTest { + + @Test + fun matchCustomParcelable() { + val descriptor = serializer().descriptor + val kType = typeOf() + assertThat(descriptor.matchKType(kType)).isTrue() + } + + @Test + fun matchCustomSerializable() { + val descriptor = serializer().descriptor + val kType = typeOf() + assertThat(descriptor.matchKType(kType)).isTrue() + } + + @Test + fun getNavTypeParcelable() { + val type = serializer().descriptor.getNavType() + assertThat(type).isEqualTo(UNKNOWN) + } + + @Test + fun getNavTypeParcelableArray() { + val type = serializer>().descriptor.getNavType() + assertThat(type).isEqualTo(UNKNOWN) + } + + @Test + fun getNavTypeSerializable() { + val type = serializer().descriptor.getNavType() + assertThat(type).isEqualTo(UNKNOWN) + } + + @Test + fun getNavTypeSerializableArray() { + val type = serializer>().descriptor.getNavType() + assertThat(type).isEqualTo(UNKNOWN) + } + + @Test + fun getNavTypeEnumSerializable() { + val type = serializer().descriptor.getNavType() + assertThat(type).isEqualTo(NavType.EnumType(TestEnum::class.java)) + } + + @Test + fun getNavTypeEnumArraySerializable() { + val type = serializer>().descriptor.getNavType() + assertThat(type).isEqualTo(UNKNOWN) + } + + @Test + fun matchEnumClass() { + val descriptor = serializer().descriptor + val kType = typeOf() + assertThat(descriptor.matchKType(kType)).isTrue() + } + + @Serializable + enum class TestEnum { + First, + Second, + } + + @Serializable + class TestParcelable(val arg: Int, val arg2: String) : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + } + + @Serializable class TestSerializable(val arg: Int, val arg2: String) : java.io.Serializable +} diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/NavigatorProviderTest.kt b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/NavigatorProviderTest.kt similarity index 75% rename from navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/NavigatorProviderTest.kt rename to navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/NavigatorProviderTest.kt index 32741f146440d..91cb232f93343 100644 --- a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/NavigatorProviderTest.kt +++ b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/NavigatorProviderTest.kt @@ -16,16 +16,13 @@ package androidx.navigation -import android.os.Bundle +import androidx.kruth.assertThat +import androidx.kruth.assertWithMessage import androidx.navigation.testing.TestNavigatorState -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import org.junit.Assert.fail -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@RunWith(JUnit4::class) +import androidx.savedstate.SavedState +import kotlin.test.Test +import kotlin.test.fail + class NavigatorProviderTest { @Test fun addWithMissingAnnotationName() { @@ -50,45 +47,6 @@ class NavigatorProviderTest { assertThat(provider.getNavigator("name")).isEqualTo(navigator) } - @Test - fun addWithExplicitNameGetWithExplicitName() { - val provider = NavigatorProvider() - val navigator = EmptyNavigator() - provider.addNavigator("name", navigator) - - assertThat(provider.getNavigator("name")).isEqualTo(navigator) - try { - provider.getNavigator(EmptyNavigator::class.java) - fail("getNavigator(Class) with an invalid name should cause an IllegalStateException") - } catch (e: IllegalStateException) { - // Expected - } - } - - @Test - fun addWithExplicitNameGetWithMissingAnnotationName() { - val provider = NavigatorProvider() - val navigator = NoNameNavigator() - provider.addNavigator("name", navigator) - try { - provider.getNavigator(NoNameNavigator::class.java) - fail( - "getNavigator(Class) with no @Navigator.Name should cause an " + - "IllegalArgumentException" - ) - } catch (e: IllegalArgumentException) { - // Expected - } - } - - @Test - fun addWithAnnotationNameGetWithAnnotationName() { - val provider = NavigatorProvider() - val navigator = EmptyNavigator() - provider.addNavigator(navigator) - assertThat(provider.getNavigator(EmptyNavigator::class.java)).isEqualTo(navigator) - } - @Test fun addWithAnnotationNameGetWithExplicitName() { val provider = NavigatorProvider() @@ -167,7 +125,7 @@ class NoNameNavigator : Navigator() { override fun navigate( destination: NavDestination, - args: Bundle?, + args: SavedState?, navOptions: NavOptions?, navigatorExtras: Extras?, ): NavDestination? { @@ -193,7 +151,7 @@ internal open class EmptyNavigator : Navigator() { override fun navigate( destination: NavDestination, - args: Bundle?, + args: SavedState?, navOptions: NavOptions?, navigatorExtras: Extras?, ): NavDestination? { diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt similarity index 69% rename from navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt rename to navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt index 9d33a61c5712b..7d8bed5b6a1c3 100644 --- a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt +++ b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavArgumentGeneratorTest.kt @@ -16,27 +16,22 @@ package androidx.navigation.serialization -import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable -import androidx.navigation.CollectionNavType +import androidx.kruth.assertThat import androidx.navigation.NamedNavArgument import androidx.navigation.NavArgument import androidx.navigation.NavType import androidx.navigation.navArgument -import com.google.common.truth.Truth.assertThat +import androidx.savedstate.SavedState +import kotlin.jvm.JvmInline import kotlin.reflect.typeOf +import kotlin.test.Test import kotlin.test.assertFailsWith import kotlin.test.fail import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.serializer -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -@RunWith(JUnit4::class) class NavArgumentGeneratorTest { @Test fun convertToInt() { @@ -652,365 +647,6 @@ class NavArgumentGeneratorTest { assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() } - @Test - fun convertToEnumList() { - @Serializable class TestClass(val arg: List) - - val converted = serializer().generateNavArguments() - val expected = - navArgument("arg") { - type = InternalAndroidNavType.EnumListType(TestEnum::class.java) - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToEnumListNullable() { - @Serializable class TestClass(val arg: List?) - - val converted = serializer().generateNavArguments() - val expected = - navArgument("arg") { - type = InternalAndroidNavType.EnumListType(TestEnum::class.java) - nullable = true - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToParcelable() { - @Serializable - class TestParcelable : Parcelable { - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) {} - } - - @Serializable class TestClass(val arg: TestParcelable) - - val navType = - object : NavType(false) { - override fun put(bundle: Bundle, key: String, value: TestParcelable) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = TestParcelable() - } - - val converted = - serializer().generateNavArguments(mapOf(typeOf() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToParcelableNullable() { - @Serializable - class TestParcelable : Parcelable { - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) {} - } - - @Serializable class TestClass(val arg: TestParcelable?) - - val navType = - object : NavType(true) { - override fun put(bundle: Bundle, key: String, value: TestParcelable?) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = TestParcelable() - } - - val converted = - serializer() - .generateNavArguments(mapOf(typeOf() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = true - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToParcelableArray() { - @Serializable - class TestParcelable : Parcelable { - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) {} - } - - @Serializable class TestClass(val arg: Array) - - val navType = - object : NavType>(false) { - override fun put(bundle: Bundle, key: String, value: Array) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = emptyArray() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf>() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToParcelableArrayNullable() { - @Serializable - class TestParcelable : Parcelable { - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) {} - } - - @Serializable class TestClass(val arg: Array?) - - val navType = - object : NavType>(true) { - override fun put(bundle: Bundle, key: String, value: Array) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = emptyArray() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf?>() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = true - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToSerializable() { - @Serializable class TestSerializable : java.io.Serializable - - @Serializable class TestClass(val arg: TestSerializable) - - val navType = - object : NavType(false) { - override fun put(bundle: Bundle, key: String, value: TestSerializable) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = TestSerializable() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToSerializableNullable() { - @Serializable class TestSerializable : java.io.Serializable - - @Serializable class TestClass(val arg: TestSerializable?) - - val navType = - object : NavType(true) { - override fun put(bundle: Bundle, key: String, value: TestSerializable) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = TestSerializable() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = true - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToSerializableArray() { - @Serializable class TestSerializable : java.io.Serializable - - @Serializable class TestClass(val arg: Array) - - val navType = - object : NavType>(false) { - override fun put(bundle: Bundle, key: String, value: Array) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = emptyArray() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf>() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToSerializableArrayNullable() { - @Serializable class TestSerializable : java.io.Serializable - - @Serializable class TestClass(val arg: Array?) - - val navType = - object : NavType>(true) { - override fun put(bundle: Bundle, key: String, value: Array) {} - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = emptyArray() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf?>() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = true - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToEnum() { - @Serializable class TestClass(val arg: TestEnum) - - val expected = - navArgument("arg") { - type = NavType.EnumType(TestEnum::class.java) - nullable = false - } - val converted = serializer().generateNavArguments() - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToTopLevelEnum() { - @Serializable class TestClass(val arg: TestTopLevelEnum) - - val expected = - navArgument("arg") { - type = NavType.EnumType(TestTopLevelEnum::class.java) - nullable = false - } - val converted = serializer().generateNavArguments() - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToEnumNullable() { - @Serializable class TestClass(val arg: TestEnum?) - - @Suppress("UNCHECKED_CAST") - val expected = - navArgument("arg") { - type = - InternalAndroidNavType.EnumNullableType(TestEnum::class.java as Class?>) - nullable = true - } - val converted = serializer().generateNavArguments() - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToNestedEnum() { - @Serializable class TestClass(val arg: EnumWrapper.NestedEnum) - - val expected = - navArgument("arg") { - type = NavType.EnumType(EnumWrapper.NestedEnum::class.java) - nullable = false - } - val converted = serializer().generateNavArguments() - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - - @Test - fun convertToEnumOverriddenSerialNameIllegal() { - @Serializable class TestClass(val arg: TestEnumCustomSerialName) - - val exception = - assertFailsWith { - serializer().generateNavArguments() - } - assertThat(exception.message) - .isEqualTo( - "Cannot find class with name \"MyCustomSerialName\". Ensure that the " + - "serialName for this argument is the default fully qualified name." + - "\nIf the build is minified, try annotating the Enum class " + - "with \"androidx.annotation.Keep\" to ensure the Enum is not removed." - ) - } - - @Test - fun convertToEnumArray() { - @Serializable class TestClass(val arg: Array) - val navType = - object : CollectionNavType>(false) { - override fun put(bundle: Bundle, key: String, value: Array) {} - - override fun serializeAsValues(value: Array) = emptyList() - - override fun emptyCollection(): Array = emptyArray() - - override fun get(bundle: Bundle, key: String) = null - - override fun parseValue(value: String) = emptyArray() - } - val converted = - serializer() - .generateNavArguments(mapOf(typeOf>() to navType)) - val expected = - navArgument("arg") { - type = navType - nullable = false - } - assertThat(converted).containsExactlyInOrder(expected) - assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() - } - @Test fun convertValueClass() { // test value class as destination route @@ -1027,9 +663,9 @@ class NavArgumentGeneratorTest { @Serializable class TestClass(val arg: TestValueClass) val navType = object : NavType(false) { - override fun put(bundle: Bundle, key: String, value: TestValueClass) {} + override fun put(bundle: SavedState, key: String, value: TestValueClass) {} - override fun get(bundle: Bundle, key: String): TestValueClass? = null + override fun get(bundle: SavedState, key: String): TestValueClass? = null override fun parseValue(value: String): TestValueClass = TestValueClass(0) } @@ -1111,9 +747,9 @@ class NavArgumentGeneratorTest { val CustomNavType = object : NavType>(false) { - override fun put(bundle: Bundle, key: String, value: ArrayList) {} + override fun put(bundle: SavedState, key: String, value: ArrayList) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1136,9 +772,9 @@ class NavArgumentGeneratorTest { val CustomNavType = object : NavType?>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList?) {} + override fun put(bundle: SavedState, key: String, value: ArrayList?) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1161,9 +797,9 @@ class NavArgumentGeneratorTest { object : NavType>(false) { override val name = "customNavType" - override fun put(bundle: Bundle, key: String, value: ArrayList) {} + override fun put(bundle: SavedState, key: String, value: ArrayList) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1227,18 +863,18 @@ class NavArgumentGeneratorTest { val CustomStringList = object : NavType?>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList?) {} + override fun put(bundle: SavedState, key: String, value: ArrayList?) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } val CustomIntList = object : NavType>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList) {} + override fun put(bundle: SavedState, key: String, value: ArrayList) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1276,18 +912,18 @@ class NavArgumentGeneratorTest { val CustomStringList = object : NavType?>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList?) {} + override fun put(bundle: SavedState, key: String, value: ArrayList?) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } val CustomIntList = object : NavType>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList) {} + override fun put(bundle: SavedState, key: String, value: ArrayList) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1323,9 +959,9 @@ class NavArgumentGeneratorTest { val CustomStringList = object : NavType>>(false) { - override fun put(bundle: Bundle, key: String, value: ArrayList>) {} + override fun put(bundle: SavedState, key: String, value: ArrayList>) {} - override fun get(bundle: Bundle, key: String): ArrayList> = + override fun get(bundle: SavedState, key: String): ArrayList> = arrayListOf() override fun parseValue(value: String): ArrayList> = arrayListOf() @@ -1349,9 +985,9 @@ class NavArgumentGeneratorTest { val CustomIntList = object : NavType>(true) { - override fun put(bundle: Bundle, key: String, value: ArrayList) {} + override fun put(bundle: SavedState, key: String, value: ArrayList) {} - override fun get(bundle: Bundle, key: String): ArrayList = arrayListOf() + override fun get(bundle: SavedState, key: String): ArrayList = arrayListOf() override fun parseValue(value: String): ArrayList = arrayListOf() } @@ -1379,9 +1015,9 @@ class NavArgumentGeneratorTest { fun convertPrioritizesProvidedNavType() { val CustomIntNavType = object : NavType(true) { - override fun put(bundle: Bundle, key: String, value: Int) {} + override fun put(bundle: SavedState, key: String, value: Int) {} - override fun get(bundle: Bundle, key: String): Int = 0 + override fun get(bundle: SavedState, key: String): Int = 0 override fun parseValue(value: String): Int = 0 } @@ -1499,47 +1135,6 @@ class NavArgumentGeneratorTest { // and hashcode which will need to be public api. private fun assertThat(actual: List) = actual - private fun List.containsExactlyInOrder( - vararg expectedArgs: NamedNavArgument - ) { - if (expectedArgs.size != this.size) { - fail("expected list has size ${expectedArgs.size} and actual list has size $size}") - } - for (i in indices) { - val actual = this[i] - val expected = expectedArgs[i] - if (expected.name != actual.name) { - fail("expected name ${expected.name}, was actually ${actual.name}") - } - - if (!expected.argument.isEqual(actual.argument)) { - fail( - """expected ${expected.name} to be: - | ${expected.argument} - | but was: - | ${actual.argument} - """ - .trimMargin() - ) - } - } - } - - private fun NavArgument.isEqual(other: NavArgument): Boolean { - if (this === other) return true - if (javaClass != other.javaClass) return false - if (isNullable != other.isNullable) return false - if (isDefaultValuePresent != other.isDefaultValuePresent) return false - if (isDefaultValueUnknown != other.isDefaultValueUnknown) return false - if (type != other.type) return false - // In context of serialization, we can only tell if defaultValue is present but don't know - // actual value, so we cannot compare it to the generated defaultValue. But if - // there is no defaultValue, we expect them both to be null. - return if (!isDefaultValuePresent) { - defaultValue == null && other.defaultValue == null - } else true - } - enum class TestEnum { TEST } @@ -1548,16 +1143,43 @@ class NavArgumentGeneratorTest { enum class TestEnumCustomSerialName { TEST } +} - @Serializable - private class EnumWrapper { - enum class NestedEnum { - ONE, - TWO, +internal fun List.containsExactlyInOrder(vararg expectedArgs: NamedNavArgument) { + if (expectedArgs.size != this.size) { + fail("expected list has size ${expectedArgs.size} and actual list has size $size}") + } + for (i in indices) { + val actual = this[i] + val expected = expectedArgs[i] + if (expected.name != actual.name) { + fail("expected name ${expected.name}, was actually ${actual.name}") + } + + if (!expected.argument.isEqual(actual.argument)) { + fail( + """expected ${expected.name} to be: + | ${expected.argument} + | but was: + | ${actual.argument} + """ + .trimMargin() + ) } } } -enum class TestTopLevelEnum { - TEST +internal fun NavArgument.isEqual(other: NavArgument): Boolean { + if (this === other) return true + if (this::class != this::class) return false + if (isNullable != other.isNullable) return false + if (isDefaultValuePresent != other.isDefaultValuePresent) return false + if (isDefaultValueUnknown != other.isDefaultValueUnknown) return false + if (type != other.type) return false + // In context of serialization, we can only tell if defaultValue is present but don't know + // actual value, so we cannot compare it to the generated defaultValue. But if + // there is no defaultValue, we expect them both to be null. + return if (!isDefaultValuePresent) { + defaultValue == null && other.defaultValue == null + } else true } diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt similarity index 91% rename from navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt rename to navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt index 7c04b96b2033e..19f9388da473e 100644 --- a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt +++ b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/NavTypeConverterTest.kt @@ -16,10 +16,8 @@ package androidx.navigation.serialization -import android.os.Parcel -import android.os.Parcelable +import androidx.kruth.assertThat import androidx.navigation.NavType -import com.google.common.truth.Truth.assertThat import kotlin.reflect.typeOf import kotlin.test.Test import kotlin.test.assertFailsWith @@ -32,10 +30,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.serializer -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -@RunWith(JUnit4::class) class NavTypeConverterTest { @Test @@ -426,13 +421,6 @@ class NavTypeConverterTest { assertThat(descriptor.matchKType(kType)).isTrue() } - @Test - fun matchEnumClass() { - val descriptor = serializer().descriptor - val kType = typeOf() - assertThat(descriptor.matchKType(kType)).isTrue() - } - @Test fun matchWrongTypeParam() { val descriptor = serializer>().descriptor @@ -519,20 +507,6 @@ class NavTypeConverterTest { assertThat(descriptor.matchKType(kType)).isTrue() } - @Test - fun matchCustomParcelable() { - val descriptor = serializer().descriptor - val kType = typeOf() - assertThat(descriptor.matchKType(kType)).isTrue() - } - - @Test - fun matchCustomSerializable() { - val descriptor = serializer().descriptor - val kType = typeOf() - assertThat(descriptor.matchKType(kType)).isTrue() - } - @Test fun matchCustomTypeNativeTypeParam() { @Serializable class TestClass @@ -718,42 +692,6 @@ class NavTypeConverterTest { assertThat(stringType).isEqualTo(NavType.StringArrayType) } - @Test - fun getNavTypeParcelable() { - val type = serializer().descriptor.getNavType() - assertThat(type).isEqualTo(UNKNOWN) - } - - @Test - fun getNavTypeParcelableArray() { - val type = serializer>().descriptor.getNavType() - assertThat(type).isEqualTo(UNKNOWN) - } - - @Test - fun getNavTypeSerializable() { - val type = serializer().descriptor.getNavType() - assertThat(type).isEqualTo(UNKNOWN) - } - - @Test - fun getNavTypeSerializableArray() { - val type = serializer>().descriptor.getNavType() - assertThat(type).isEqualTo(UNKNOWN) - } - - @Test - fun getNavTypeEnumSerializable() { - val type = serializer().descriptor.getNavType() - assertThat(type).isEqualTo(NavType.EnumType(TestEnum::class.java)) - } - - @Test - fun getNavTypeEnumArraySerializable() { - val type = serializer>().descriptor.getNavType() - assertThat(type).isEqualTo(UNKNOWN) - } - @Test fun getNavTypeUnsupportedArray() { assertThat(serializer>().descriptor.getNavType()).isEqualTo(UNKNOWN) @@ -796,27 +734,12 @@ class NavTypeConverterTest { val arg: String = "test" } - @Serializable - enum class TestEnum { - First, - Second, - } - @Serializable class ParamDerivedTwo : Param() @Serializable class ParamDerived : Param() @Serializable open class Param - @Serializable - class TestParcelable(val arg: Int, val arg2: String) : Parcelable { - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) {} - } - - @Serializable class TestSerializable(val arg: Int, val arg2: String) : java.io.Serializable - class ArgClass class ArgClassSerializer : KSerializer { diff --git a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt similarity index 91% rename from navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt rename to navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt index 92e9e0a3ece53..6365a0e4f1160 100644 --- a/navigation/navigation-common/src/androidHostTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt +++ b/navigation/navigation-common/src/commonTest/kotlin/androidx/navigation/serialization/RoutePatternTest.kt @@ -16,12 +16,13 @@ package androidx.navigation.serialization -import android.os.Bundle +import androidx.kruth.assertThat import androidx.navigation.CollectionNavType import androidx.navigation.NavType -import com.google.common.truth.Truth.assertThat +import androidx.savedstate.SavedState import kotlin.reflect.KType import kotlin.reflect.typeOf +import kotlin.test.Test import kotlin.test.assertFailsWith import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName @@ -33,13 +34,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.serializer -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 const val PATH_SERIAL_NAME = "www.test.com" -@RunWith(JUnit4::class) class RoutePatternTest { @Test @@ -214,9 +211,9 @@ class RoutePatternTest { override val name: String get() = "CustomType" - override fun put(bundle: Bundle, key: String, value: CustomType) {} + override fun put(bundle: SavedState, key: String, value: CustomType) {} - override fun get(bundle: Bundle, key: String): CustomType? = null + override fun get(bundle: SavedState, key: String): CustomType? = null override fun parseValue(value: String): CustomType = CustomType() @@ -240,9 +237,9 @@ class RoutePatternTest { override val name: String get() = "CustomType" - override fun put(bundle: Bundle, key: String, value: CustomType) {} + override fun put(bundle: SavedState, key: String, value: CustomType) {} - override fun get(bundle: Bundle, key: String): CustomType? = null + override fun get(bundle: SavedState, key: String): CustomType? = null override fun parseValue(value: String): CustomType = CustomType(NestedCustomType()) @@ -264,9 +261,9 @@ class RoutePatternTest { override val name: String get() = "CustomType" - override fun put(bundle: Bundle, key: String, value: CustomSerializerClass) {} + override fun put(bundle: SavedState, key: String, value: CustomSerializerClass) {} - override fun get(bundle: Bundle, key: String): CustomSerializerClass? = null + override fun get(bundle: SavedState, key: String): CustomSerializerClass? = null override fun parseValue(value: String): CustomSerializerClass = CustomSerializerClass(1L) @@ -291,9 +288,9 @@ class RoutePatternTest { override val name: String get() = "CustomType" - override fun put(bundle: Bundle, key: String, value: CustomType) {} + override fun put(bundle: SavedState, key: String, value: CustomType) {} - override fun get(bundle: Bundle, key: String): CustomType? = null + override fun get(bundle: SavedState, key: String): CustomType? = null override fun parseValue(value: String): CustomType = CustomType() @@ -319,13 +316,13 @@ class RoutePatternTest { get() = "CustomType" override fun put( - bundle: Bundle, + bundle: SavedState, key: String, value: CustomType>, ) {} override fun get( - bundle: Bundle, + bundle: SavedState, key: String, ): CustomType>? = null @@ -378,9 +375,9 @@ class RoutePatternTest { override val name: String get() = "CustomType" - override fun put(bundle: Bundle, key: String, value: CustomType) {} + override fun put(bundle: SavedState, key: String, value: CustomType) {} - override fun get(bundle: Bundle, key: String): CustomType? = null + override fun get(bundle: SavedState, key: String): CustomType? = null override fun parseValue(value: String): CustomType = CustomType() @@ -471,13 +468,13 @@ class RoutePatternTest { val type = object : CollectionNavType>(false) { - override fun put(bundle: Bundle, key: String, value: List) {} + override fun put(bundle: SavedState, key: String, value: List) {} override fun serializeAsValues(value: List): List = emptyList() override fun emptyCollection(): List = emptyList() - override fun get(bundle: Bundle, key: String): List? = null + override fun get(bundle: SavedState, key: String): List? = null override fun parseValue(value: String): List = listOf() @@ -494,19 +491,19 @@ private fun assertThatRoutePatternFrom( map: Map> = emptyMap(), ) = serializer.generateRoutePattern(map) -private fun String.isEqualTo(other: String) { +internal fun String.isEqualTo(other: String) { assertThat(this).isEqualTo(other) } @Serializable @SerialName(PATH_SERIAL_NAME) -private class ClassWithCompanionObject(val arg: Int) { +internal class ClassWithCompanionObject(val arg: Int) { companion object TestObject } @Serializable @SerialName(PATH_SERIAL_NAME) -private class ClassWithCompanionParam(val arg: Int) { +internal class ClassWithCompanionParam(val arg: Int) { companion object { val companionVal: String = "hello" } @@ -547,4 +544,4 @@ internal class CustomSerializer : KSerializer { CustomSerializerClass(decoder.decodeLong()) } -private interface TestInterface +internal interface TestInterface diff --git a/navigation/navigation-runtime/src/commonMain/kotlin/androidx/navigation/NavHostController.kt b/navigation/navigation-runtime/src/commonMain/kotlin/androidx/navigation/NavHostController.kt index b0febdf6dc4e7..5f04baf7d1bc0 100644 --- a/navigation/navigation-runtime/src/commonMain/kotlin/androidx/navigation/NavHostController.kt +++ b/navigation/navigation-runtime/src/commonMain/kotlin/androidx/navigation/NavHostController.kt @@ -26,8 +26,7 @@ import androidx.lifecycle.ViewModelStore * from a navigation host via [NavHost.navController] or by using one of the utility methods on the * [Navigation] class. */ -@Suppress("KmpModifierMismatch") // actuals are open -public expect class NavHostController : NavController { +public expect open class NavHostController : NavController { /** * Sets the host's [LifecycleOwner]. * diff --git a/navigation/navigation-testing/api/current.txt b/navigation/navigation-testing/api/current.txt index bf013f544f0f8..d3f0621faaef8 100644 --- a/navigation/navigation-testing/api/current.txt +++ b/navigation/navigation-testing/api/current.txt @@ -23,6 +23,8 @@ package androidx.navigation.testing { ctor public TestNavigatorState(optional android.content.Context? context); ctor public TestNavigatorState(optional android.content.Context? context, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); ctor @BytecodeOnly public TestNavigatorState(android.content.Context!, kotlinx.coroutines.CoroutineDispatcher!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public TestNavigatorState(optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); + ctor @BytecodeOnly public TestNavigatorState(kotlinx.coroutines.CoroutineDispatcher!, int, kotlin.jvm.internal.DefaultConstructorMarker!); method public androidx.navigation.NavBackStackEntry createBackStackEntry(androidx.navigation.NavDestination destination, android.os.Bundle? arguments); method public androidx.navigation.NavBackStackEntry restoreBackStackEntry(androidx.navigation.NavBackStackEntry previouslySavedEntry); } diff --git a/navigation/navigation-testing/api/restricted_current.txt b/navigation/navigation-testing/api/restricted_current.txt index bf013f544f0f8..d3f0621faaef8 100644 --- a/navigation/navigation-testing/api/restricted_current.txt +++ b/navigation/navigation-testing/api/restricted_current.txt @@ -23,6 +23,8 @@ package androidx.navigation.testing { ctor public TestNavigatorState(optional android.content.Context? context); ctor public TestNavigatorState(optional android.content.Context? context, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); ctor @BytecodeOnly public TestNavigatorState(android.content.Context!, kotlinx.coroutines.CoroutineDispatcher!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public TestNavigatorState(optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); + ctor @BytecodeOnly public TestNavigatorState(kotlinx.coroutines.CoroutineDispatcher!, int, kotlin.jvm.internal.DefaultConstructorMarker!); method public androidx.navigation.NavBackStackEntry createBackStackEntry(androidx.navigation.NavDestination destination, android.os.Bundle? arguments); method public androidx.navigation.NavBackStackEntry restoreBackStackEntry(androidx.navigation.NavBackStackEntry previouslySavedEntry); } diff --git a/navigation/navigation-testing/bcv/native/current.txt b/navigation/navigation-testing/bcv/native/current.txt index cdfa01c2ca5ad..cc6411075d430 100644 --- a/navigation/navigation-testing/bcv/native/current.txt +++ b/navigation/navigation-testing/bcv/native/current.txt @@ -6,4 +6,18 @@ // - Show declarations: true // Library unique name: +final class androidx.navigation.testing/TestNavHostController : androidx.navigation/NavHostController { // androidx.navigation.testing/TestNavHostController|null[0] + constructor () // androidx.navigation.testing/TestNavHostController.|(){}[0] + + final val backStack // androidx.navigation.testing/TestNavHostController.backStack|{}backStack[0] + final fun (): kotlin.collections/List // androidx.navigation.testing/TestNavHostController.backStack.|(){}[0] +} + +final class androidx.navigation.testing/TestNavigatorState : androidx.navigation/NavigatorState { // androidx.navigation.testing/TestNavigatorState|null[0] + constructor (kotlinx.coroutines/CoroutineDispatcher = ...) // androidx.navigation.testing/TestNavigatorState.|(kotlinx.coroutines.CoroutineDispatcher){}[0] + + final fun createBackStackEntry(androidx.navigation/NavDestination, androidx.savedstate/SavedState?): androidx.navigation/NavBackStackEntry // androidx.navigation.testing/TestNavigatorState.createBackStackEntry|createBackStackEntry(androidx.navigation.NavDestination;androidx.savedstate.SavedState?){}[0] + final fun restoreBackStackEntry(androidx.navigation/NavBackStackEntry): androidx.navigation/NavBackStackEntry // androidx.navigation.testing/TestNavigatorState.restoreBackStackEntry|restoreBackStackEntry(androidx.navigation.NavBackStackEntry){}[0] +} + final fun (androidx.lifecycle/SavedStateHandle.Companion).androidx.navigation.testing/invoke(kotlin/Any, kotlin.collections/Map> = ...): androidx.lifecycle/SavedStateHandle // androidx.navigation.testing/invoke|invoke@androidx.lifecycle.SavedStateHandle.Companion(kotlin.Any;kotlin.collections.Map>){}[0] diff --git a/navigation/navigation-testing/build.gradle b/navigation/navigation-testing/build.gradle index 1f22fe6da25a6..9ec495b89292e 100644 --- a/navigation/navigation-testing/build.gradle +++ b/navigation/navigation-testing/build.gradle @@ -61,6 +61,10 @@ androidXMultiplatform { implementation(project(":lifecycle:lifecycle-viewmodel-savedstate")) } + nonAndroidMain.dependsOn(commonMain) + jvmStubsMain.dependsOn(nonAndroidMain) + linuxx64StubsMain.dependsOn(nonAndroidMain) + commonTest.dependencies { implementation(project(":kruth:kruth")) } diff --git a/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavHostController.android.kt b/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavHostController.android.kt index 9c447bd8b9bf6..9ea1e95539c31 100644 --- a/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavHostController.android.kt +++ b/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavHostController.android.kt @@ -25,10 +25,10 @@ import androidx.savedstate.savedState import java.lang.IllegalArgumentException /** Subclass of [NavHostController] that offers additional APIs for testing Navigation. */ -public class TestNavHostController(context: Context) : NavHostController(context) { +public actual class TestNavHostController(context: Context) : NavHostController(context) { /** Gets an immutable copy of the [elements][NavBackStackEntry] currently on the back stack. */ - public val backStack: List + public actual val backStack: List get() = currentBackStack.value init { diff --git a/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavigatorState.android.kt b/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavigatorState.android.kt index 486025c900473..3c189b13ac874 100644 --- a/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavigatorState.android.kt +++ b/navigation/navigation-testing/src/androidMain/kotlin/androidx/navigation/testing/TestNavigatorState.android.kt @@ -44,12 +44,17 @@ import kotlinx.coroutines.withContext * updated as they are added and removed from the state. This work is kicked off on the * [coroutineDispatcher]. */ -public class TestNavigatorState +public actual class TestNavigatorState @JvmOverloads constructor( private val context: Context? = null, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate, ) : NavigatorState() { + + public actual constructor( + coroutineDispatcher: CoroutineDispatcher + ) : this(null, coroutineDispatcher) + internal val navContext = NavContext(context) private val viewModelStoreProvider = @@ -63,7 +68,7 @@ constructor( private val savedStates = mutableMapOf() private val entrySavedState = mutableMapOf() - override fun createBackStackEntry( + public actual override fun createBackStackEntry( destination: NavDestination, arguments: SavedState?, ): NavBackStackEntry = @@ -79,7 +84,9 @@ constructor( * Restore a previously saved [NavBackStackEntry]. You must have previously called [pop] with * [previouslySavedEntry] and `true`. */ - public fun restoreBackStackEntry(previouslySavedEntry: NavBackStackEntry): NavBackStackEntry { + public actual fun restoreBackStackEntry( + previouslySavedEntry: NavBackStackEntry + ): NavBackStackEntry { val savedState = checkNotNull(savedStates[previouslySavedEntry.id]) { "restoreBackStackEntry(previouslySavedEntry) must be passed a NavBackStackEntry " + diff --git a/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavHostController.kt b/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavHostController.kt new file mode 100644 index 0000000000000..69b75cdcb6e33 --- /dev/null +++ b/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavHostController.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.navigation.testing + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController + +/** Subclass of [NavHostController] that offers additional APIs for testing Navigation. */ +public expect class TestNavHostController : NavHostController { + + /** Gets an immutable copy of the [elements][NavBackStackEntry] currently on the back stack. */ + public val backStack: List +} diff --git a/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavigatorState.kt new file mode 100644 index 0000000000000..7893fcb5126a9 --- /dev/null +++ b/navigation/navigation-testing/src/commonMain/kotlin/androidx/navigation/testing/TestNavigatorState.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.navigation.testing + +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.NavigatorState +import androidx.savedstate.SavedState +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +/** + * An implementation of [NavigatorState] that allows testing a [androidx.navigation.Navigator] in + * isolation (i.e., without requiring a [androidx.navigation.NavController]). + * + * The [Lifecycle] of all [NavBackStackEntry] instances added to this TestNavigatorState will be + * updated as they are added and removed from the state. This work is kicked off on the + * [coroutineDispatcher]. + */ +public expect class TestNavigatorState( + coroutineDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate +) : NavigatorState { + + /** + * Restore a previously saved [NavBackStackEntry]. You must have previously called [pop] with + * [previouslySavedEntry] and `true`. + */ + public fun restoreBackStackEntry(previouslySavedEntry: NavBackStackEntry): NavBackStackEntry + + public override fun createBackStackEntry( + destination: NavDestination, + arguments: SavedState?, + ): NavBackStackEntry +} diff --git a/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavHostController.nonAndroid.kt b/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavHostController.nonAndroid.kt new file mode 100644 index 0000000000000..ecaff7779dd7e --- /dev/null +++ b/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavHostController.nonAndroid.kt @@ -0,0 +1,26 @@ +/* + * 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.navigation.testing + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import androidx.navigation.implementedInJetBrainsFork + +public actual class TestNavHostController : NavHostController() { + public actual val backStack: List + get() = implementedInJetBrainsFork() +} diff --git a/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavigatorState.nonAndroid.kt b/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavigatorState.nonAndroid.kt new file mode 100644 index 0000000000000..3eb37ff47ba2e --- /dev/null +++ b/navigation/navigation-testing/src/nonAndroidMain/kotlin/androidx/navigation/testing/TestNavigatorState.nonAndroid.kt @@ -0,0 +1,40 @@ +/* + * 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.navigation.testing + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.NavigatorState +import androidx.navigation.implementedInJetBrainsFork +import androidx.savedstate.SavedState +import kotlinx.coroutines.CoroutineDispatcher + +public actual class TestNavigatorState +actual constructor(coroutineDispatcher: CoroutineDispatcher) : NavigatorState() { + public actual fun restoreBackStackEntry( + previouslySavedEntry: NavBackStackEntry + ): NavBackStackEntry { + implementedInJetBrainsFork() + } + + public actual override fun createBackStackEntry( + destination: NavDestination, + arguments: SavedState?, + ): NavBackStackEntry { + implementedInJetBrainsFork() + } +} From a954958e0533c6d75f191b77fafa9bea8374281f Mon Sep 17 00:00:00 2001 From: jbwoods Date: Wed, 14 Jan 2026 20:39:07 +0000 Subject: [PATCH 04/11] Update NavEntryDecorator KDoc to use entry.Content() Replaces `entry.content.invoke(entry.key)` with `entry.Content()` in the KDoc examples. This ensures we are no longer using private APIs in the samples. Test: all tests pass Bug: 467164535 Change-Id: I09fe681fb241e9a169594d14d70dfc8ca42e6ba7 --- .../kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 77a6c0bd92acc..ca2b892352146 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 @@ -29,7 +29,7 @@ import androidx.compose.runtime.Immutable * val decorator = NavEntryDecorator { entry -> * ... * CompositionLocalProvider(LocalMyStateProvider provides myState) { - * entry.content.invoke(entry.key) + * entry.Content() * } * } * ``` @@ -39,7 +39,7 @@ import androidx.compose.runtime.Immutable * val decorator = NavEntryDecorator { entry -> * ... * MyComposableFunction { - * entry.content.invoke(entry.key) + * entry.Content() * } * } * ``` From e820d3f7a02bd3439fb2fa55973ea4bfe7efc6bb Mon Sep 17 00:00:00 2001 From: Tiffany Lu Date: Mon, 12 Jan 2026 21:25:39 +0000 Subject: [PATCH 05/11] CameraPipe FrameBuffer takes 0 capacity A framebuffer with 0 capacity means that the user does not need to pull frames out of the buffer. This is commonly used in preview or viewfinder. Bug: 473901206 Test: FrameBufferImplTest Change-Id: I1ab90035b509a2e9f88129a3ec986e8051087b66 --- .../pipe/framegraph/FrameBufferImpl.kt | 12 ++- .../pipe/framegraph/FrameBufferImplTest.kt | 98 ++++++++++++++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImpl.kt index 3e18a16aa6f82..50f0fc449836b 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImpl.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImpl.kt @@ -60,13 +60,23 @@ internal class FrameBufferImpl( override val frameFlow: SharedFlow = _frameFlow.asSharedFlow() init { - require(capacity > 0) { "FrameBuffer capacity must be greater than 0" } + require(capacity >= 0) { "FrameBuffer capacity must be greater than or equal to 0" } } private val _size = MutableStateFlow(0) override val size: StateFlow = _size.asStateFlow() override fun onFrameStarted(frameReference: FrameReference) { + // If capacity is 0, emit the reference and exit early. + if (capacity == 0) { + synchronized(lock) { + if (!closed) { + _frameFlow.tryEmit(frameReference) + } + } + return + } + val acquiredFrame = frameReference.tryAcquire() val entryToAdd: BufferEntry = diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImplTest.kt index c960d5b433c81..3ac2cd18ab725 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImplTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/framegraph/FrameBufferImplTest.kt @@ -174,9 +174,11 @@ class FrameBufferImplTest { assertThat(frameBuffer.size.value).isEqualTo(0) } - @Test(expected = IllegalArgumentException::class) - fun initialization_zeroCapacity_throwsException() { - createFrameBuffer(capacity = 0) + @Test + fun initialization_zeroCapacity_initializeSuccessfully() { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.capacity).isEqualTo(0) } @Test(expected = IllegalArgumentException::class) @@ -225,6 +227,22 @@ class FrameBufferImplTest { } } + @Test + fun onFrameStarted_zeroCapacity_doesNotBufferFrame() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + val frameReference = createTestFrame(1) + + // Simulate onFrameStarted being called. + frameBuffer.onFrameStarted(frameReference) + advanceUntilIdle() + + // Assert that the buffer size remains 0. + assertThat(frameBuffer.size.value).isEqualTo(0) + assertThat(frameBuffer.peekFirstReference()).isNull() + assertThat(frameBuffer.peekAllReferences()).isEmpty() + } + @Test fun removeFirstReference_emptyBuffer_returnsNull() = testScope.runTest { @@ -232,6 +250,14 @@ class FrameBufferImplTest { assertThat(frameBuffer.size.value).isEqualTo(0) } + @Test + fun removeFirstReference_zeroCapacityBuffer_returnsNull() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.removeFirstReference()).isNull() + } + @Test fun removeFirstReference_removesCorrectFrame_updatesSize() = testScope.runTest { @@ -269,6 +295,14 @@ class FrameBufferImplTest { assertThat(frameBuffer.size.value).isEqualTo(0) } + @Test + fun removeLastReference_zeroCapacityBuffer_returnsNull() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.removeLastReference()).isNull() + } + @Test fun removeLastReference_removesCorrectFrame_updatesSize() = testScope.runTest { @@ -306,6 +340,14 @@ class FrameBufferImplTest { assertThat(frameBuffer.size.value).isEqualTo(0) } + @Test + fun removeAllReference_zeroCapacityBuffer_doesNotThowErrors() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + frameBuffer.removeAllReferences() + } + @Test fun removeAllReferences_returnsAllFramesInOrder_updatesSize() = testScope.runTest { @@ -338,6 +380,14 @@ class FrameBufferImplTest { fun peekFirstReference_emptyBuffer_returnsNull() = testScope.runTest { assertThat(frameBuffer.peekFirstReference()).isNull() } + @Test + fun peekFirstReference_zeroCapacityBuffer_returnsNull() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.peekFirstReference()).isNull() + } + @Test fun peekFirstReference_returnsFrame_doesNotChangeSize() = testScope.runTest { @@ -366,6 +416,14 @@ class FrameBufferImplTest { fun peekLastReference_emptyBuffer_returnsNull() = testScope.runTest { assertThat(frameBuffer.peekLastReference()).isNull() } + @Test + fun peekLastReference_zeroCapacityBuffer_returnsNull() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.peekLastReference()).isNull() + } + @Test fun peekLastReference_returnsFrame_doesNotChangeSize() = testScope.runTest { @@ -396,6 +454,14 @@ class FrameBufferImplTest { fun peekAllReferences_emptyBuffer_returnsEmptyList() = testScope.runTest { assertThat(frameBuffer.peekAllReferences()).isEmpty() } + @Test + fun peekAllReference_zeroCapacityBuffer_returnsEmptyList() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + + assertThat(frameBuffer.peekAllReferences()).isEmpty() + } + @Test fun peekAllReferences_returnsAllFramesInOrder_doesNotChangeSize() = testScope.runTest { @@ -424,6 +490,32 @@ class FrameBufferImplTest { assertThat(frameBuffer.peekAllReferences()).isEmpty() } + @Test + fun onFrameAvailable_zeroCapacity_flowEmitted() = + testScope.runTest { + val frameBuffer = createFrameBuffer(capacity = 0) + val frameRef1 = createTestFrame(1) + val ready = CompletableDeferred() + val resultsChannel = Channel(Channel.UNLIMITED) + + val job = + backgroundScope.launch { + frameBuffer.frameFlow + .onStart { ready.complete(Unit) } + .collect { frame -> resultsChannel.send(frame) } + } + + ready.await() + frameBuffer.onFrameStarted(frameRef1) + advanceUntilIdle() + + val receivedFrame = resultsChannel.receive() + assertThat(receivedFrame.frameNumber).isEqualTo(frameRef1.frameNumber) + assertThat(frameBuffer.size.value).isEqualTo(0) + assertThat(frameBuffer.peekFirstReference()).isNull() + job.cancel() + } + @Test fun onFrameAvailable_flowEmitted() = testScope.runTest { From 8dd6cad20a7b21b8fe123a15672b440e413147fd Mon Sep 17 00:00:00 2001 From: Sravani Vadlamani Date: Thu, 15 Jan 2026 14:05:51 +0000 Subject: [PATCH 06/11] Update wear compose demo versionCode = 93 versionName = "1.93" Incremented versionCode to from 92 to 93 and versionName from 1.92 to 1.93 Bug: 476062001 Fixes: 476062001 Test: Manual install and version check on device. APK loads and runs. Change-Id: I27a97bd7f6ebbcaeb9a306cfe6d188a37eab1007 --- wear/compose/integration-tests/demos/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle index 55edbec9ce07f..67e9f8890d7ed 100644 --- a/wear/compose/integration-tests/demos/build.gradle +++ b/wear/compose/integration-tests/demos/build.gradle @@ -26,8 +26,8 @@ android { defaultConfig { applicationId = "androidx.wear.compose.integration.demos" minSdk { version = release(25) } - versionCode = 92 - versionName = "1.92" + versionCode = 93 + versionName = "1.93" } buildTypes { From 474f3e45918122164889e5a04802b7dd000c530e Mon Sep 17 00:00:00 2001 From: Pierluigi Fimiano Date: Thu, 15 Jan 2026 13:26:21 +0000 Subject: [PATCH 07/11] Fix multiple dataStore instance in the same process. Bug: 474292165 Test: Manual and Unit Change-Id: Ifda88f0a0f0f7700b17af7990adcd5ed277b0003 --- .../glance/wear/cache/WearWidgetCache.kt | 24 ++++++++++++------- .../wear/parcel/WearWidgetProviderImpl.kt | 2 +- .../glance/wear/cache/WearWidgetCacheTest.kt | 22 +++++++++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/glance/wear/wear/src/main/java/androidx/glance/wear/cache/WearWidgetCache.kt b/glance/wear/wear/src/main/java/androidx/glance/wear/cache/WearWidgetCache.kt index 92221c58db9b7..c59543eab860f 100644 --- a/glance/wear/wear/src/main/java/androidx/glance/wear/cache/WearWidgetCache.kt +++ b/glance/wear/wear/src/main/java/androidx/glance/wear/cache/WearWidgetCache.kt @@ -18,6 +18,7 @@ package androidx.glance.wear.cache import android.content.Context import android.util.Log +import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataStore import androidx.datastore.core.IOException import androidx.datastore.core.Serializer @@ -30,16 +31,22 @@ import java.io.InputStream import java.io.OutputStream import kotlinx.coroutines.flow.first +private const val DEFAULT_FILE_NAME = "androidx_glance_wear_widget_cache.pb" +private val Context.dataStore: DataStore by + dataStore(fileName = DEFAULT_FILE_NAME, serializer = WearWidgetCacheSerializer) + /** * Caches widget information, including container specs and instance-to-type mappings. The cache is * backed by a [DataStore] file. * - * @param context The application context - * @param fileName The name of the file to use for the cache. + * @param dataStore The [DataStore] to use for the cache. */ -internal class WearWidgetCache(private val context: Context, fileName: String = DEFAULT_FILE_NAME) { - private val Context.dataStore: DataStore by - dataStore(fileName = fileName, serializer = WearWidgetCacheSerializer) +internal class WearWidgetCache +@VisibleForTesting +internal constructor(private val dataStore: DataStore) { + + /** Creates a new [WearWidgetCache] instance. */ + constructor(context: Context) : this(context.dataStore) /** * Updates the cache atomically. @@ -49,7 +56,7 @@ internal class WearWidgetCache(private val context: Context, fileName: String = */ suspend fun update(block: WidgetCacheUpdateScope.() -> Unit): Boolean { return try { - context.dataStore.updateData { cacheProto -> + dataStore.updateData { cacheProto -> val scope = WidgetCacheUpdateScope(cacheProto) scope.block() scope.toProto() @@ -70,7 +77,7 @@ internal class WearWidgetCache(private val context: Context, fileName: String = suspend fun getContainerSpec( @ContainerInfo.ContainerType containerType: Int ): WidgetContainerSpec? { - val cacheProto = context.dataStore.data.first() + val cacheProto = dataStore.data.first() return cacheProto.container_type_to_spec[containerType]?.let { WidgetContainerSpec.fromProto(it) } @@ -83,7 +90,7 @@ internal class WearWidgetCache(private val context: Context, fileName: String = * @return The container type, or `null` if it doesn't exist in the cache. */ suspend fun getInstanceType(instanceId: WidgetInstanceId): Int? { - val cacheProto = context.dataStore.data.first() + val cacheProto = dataStore.data.first() return cacheProto.instance_id_to_type[instanceId.flattenToString()] } @@ -130,7 +137,6 @@ internal class WearWidgetCache(private val context: Context, fileName: String = } internal companion object { - internal const val DEFAULT_FILE_NAME = "androidx_glance_wear_widget_cache.pb" private const val TAG = "WearWidgetCache" } } diff --git a/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt b/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt index f00785947da23..bf657e6750685 100644 --- a/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt +++ b/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt @@ -42,7 +42,7 @@ internal class WearWidgetProviderImpl( private val widget: GlanceWearWidget, ) : IWearWidgetProvider.Stub() { - private val widgetCache: WearWidgetCache by lazy { WearWidgetCache(context) } + private val widgetCache: WearWidgetCache = WearWidgetCache(context) override fun getApiVersion(): Int = API_VERSION diff --git a/glance/wear/wear/src/test/java/androidx/glance/wear/cache/WearWidgetCacheTest.kt b/glance/wear/wear/src/test/java/androidx/glance/wear/cache/WearWidgetCacheTest.kt index cd3261238f258..d815a098f6e42 100644 --- a/glance/wear/wear/src/test/java/androidx/glance/wear/cache/WearWidgetCacheTest.kt +++ b/glance/wear/wear/src/test/java/androidx/glance/wear/cache/WearWidgetCacheTest.kt @@ -16,13 +16,15 @@ package androidx.glance.wear.cache -import android.content.Context +import androidx.datastore.core.DataStoreFactory import androidx.glance.wear.ContainerInfo import androidx.glance.wear.WidgetInstanceId -import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -30,8 +32,19 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [Config.TARGET_SDK]) class WearWidgetCacheTest { - private val context: Context = ApplicationProvider.getApplicationContext() - private val cacheUnderTest = WearWidgetCache(context, DATASTORE_FILE_NAME) + + @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + private lateinit var cacheUnderTest: WearWidgetCache + + @Before + fun setUp() { + val dataStore = + DataStoreFactory.create( + serializer = WearWidgetCacheSerializer, + produceFile = { tmpFolder.newFile(DATASTORE_FILE_NAME) }, + ) + cacheUnderTest = WearWidgetCache(dataStore) + } @Test fun setAndGetContainerSpec_restoresValue() = runTest { @@ -45,7 +58,6 @@ class WearWidgetCacheTest { @Test fun setAndGetContainerSpec_withMultipleTypes_restoresValues() = runTest { - val cache = WearWidgetCache(context, DATASTORE_FILE_NAME) val spec1 = WidgetContainerSpec(300f, 400f) val spec2 = WidgetContainerSpec(100f, 200f) From dd0e8e570fbfd3ebfde0d00944bdf20856bf877a Mon Sep 17 00:00:00 2001 From: "Oleksandr.Karpovich" Date: Wed, 24 Dec 2025 11:09:37 +0100 Subject: [PATCH 08/11] Move AbstractMainTestClock to commonMain It's an upstreaming from JB fork. AbstractMainTestClock has nothing target specific and it's preferred to avoid code duplication. Test: N/A Change-Id: Iaefbb7b81bfa9392c604ef2c174522288e33c3f2 --- .../kotlin/androidx/compose/ui/test/AbstractMainTestClock.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename compose/ui/ui-test/src/{jvmAndAndroidMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.jvmAndAndroid.kt => commonMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.kt} (100%) diff --git a/compose/ui/ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.jvmAndAndroid.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.kt similarity index 100% rename from compose/ui/ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.jvmAndAndroid.kt rename to compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AbstractMainTestClock.kt From 486c040983fc69974d6a67f1a011f891db89795d Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Tue, 13 Jan 2026 16:30:09 +0000 Subject: [PATCH 09/11] Deprecate SafeIterableMap in Arch Mark SafeIterableMap and FastSafeIterableMap as deprecated. These classes are internal legacy utilities and are not in use anymore. The associated tests have been moved from 'androidx.collection' to 'androidx.arch.core.internal'. By placing the tests in the same package as the deprecated classes, we eliminate the need for explicit imports. This allows the @SuppressWarnings("deprecation") annotation to correctly silence all warnings, which is not possible on explicit import statements. Bug: N/A Test: existing passes Change-Id: I6a5ae5399f43d48da3645b0a79b5519c792bfff1 --- .../core-common/api/restricted_current.txt | 40 +++++++++---------- .../core/internal/FastSafeIterableMap.java | 2 + .../arch/core/internal/SafeIterableMap.java | 2 + .../internal}/FastSafeIterableMapTest.java | 7 ++-- .../core/internal}/SafeIterableMapTest.java | 7 ++-- 5 files changed, 30 insertions(+), 28 deletions(-) rename arch/core/core-common/src/test/java/androidx/{collection => arch/core/internal}/FastSafeIterableMapTest.java (94%) rename arch/core/core-common/src/test/java/androidx/{collection => arch/core/internal}/SafeIterableMapTest.java (99%) diff --git a/arch/core/core-common/api/restricted_current.txt b/arch/core/core-common/api/restricted_current.txt index 1ef0e0fd985ff..e59b99951ef30 100644 --- a/arch/core/core-common/api/restricted_current.txt +++ b/arch/core/core-common/api/restricted_current.txt @@ -1,32 +1,32 @@ // Signature format: 4.0 package androidx.arch.core.internal { - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap extends androidx.arch.core.internal.SafeIterableMap { - ctor public FastSafeIterableMap(); - method public java.util.Map.Entry? ceil(K!); - method public boolean contains(K!); + @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap extends androidx.arch.core.internal.SafeIterableMap { + ctor @Deprecated public FastSafeIterableMap(); + method @Deprecated public java.util.Map.Entry? ceil(K!); + method @Deprecated public boolean contains(K!); } - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap implements java.lang.Iterable!> { - ctor public SafeIterableMap(); - method public java.util.Iterator!> descendingIterator(); - method public java.util.Map.Entry? eldest(); - method protected androidx.arch.core.internal.SafeIterableMap.Entry? get(K!); - method public java.util.Iterator!> iterator(); - method public androidx.arch.core.internal.SafeIterableMap.IteratorWithAdditions iteratorWithAdditions(); - method public java.util.Map.Entry? newest(); - method public V! putIfAbsent(K, V); - method public V! remove(K); - method public int size(); + @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap implements java.lang.Iterable!> { + ctor @Deprecated public SafeIterableMap(); + method @Deprecated public java.util.Iterator!> descendingIterator(); + method @Deprecated public java.util.Map.Entry? eldest(); + method @Deprecated protected androidx.arch.core.internal.SafeIterableMap.Entry? get(K!); + method @Deprecated public java.util.Iterator!> iterator(); + method @Deprecated public androidx.arch.core.internal.SafeIterableMap.IteratorWithAdditions iteratorWithAdditions(); + method @Deprecated public java.util.Map.Entry? newest(); + method @Deprecated public V! putIfAbsent(K, V); + method @Deprecated public V! remove(K); + method @Deprecated public int size(); } - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove implements java.util.Iterator!> { - method public boolean hasNext(); - method public java.util.Map.Entry! next(); + @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove implements java.util.Iterator!> { + method @Deprecated public boolean hasNext(); + method @Deprecated public java.util.Map.Entry! next(); } - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove { - ctor public SafeIterableMap.SupportRemove(); + @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove { + ctor @Deprecated public SafeIterableMap.SupportRemove(); } } diff --git a/arch/core/core-common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java b/arch/core/core-common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java index de698d6724d5c..c24791334a34f 100644 --- a/arch/core/core-common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java +++ b/arch/core/core-common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java @@ -31,7 +31,9 @@ * * @param Key type * @param Value type + * @deprecated These are internal legacy utilities and should not be used. */ +@Deprecated @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap extends SafeIterableMap { diff --git a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java index 8b3d08b8f6d37..ea78235e67048 100644 --- a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java +++ b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java @@ -31,7 +31,9 @@ * * @param Key type * @param Value type + * @deprecated These are internal legacy utilities and should not be used. */ +@Deprecated @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap implements Iterable> { diff --git a/arch/core/core-common/src/test/java/androidx/collection/FastSafeIterableMapTest.java b/arch/core/core-common/src/test/java/androidx/arch/core/internal/FastSafeIterableMapTest.java similarity index 94% rename from arch/core/core-common/src/test/java/androidx/collection/FastSafeIterableMapTest.java rename to arch/core/core-common/src/test/java/androidx/arch/core/internal/FastSafeIterableMapTest.java index 9ef9e12d346f3..1778576565005 100644 --- a/arch/core/core-common/src/test/java/androidx/collection/FastSafeIterableMapTest.java +++ b/arch/core/core-common/src/test/java/androidx/arch/core/internal/FastSafeIterableMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 The Android Open Source Project + * Copyright 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,17 @@ * limitations under the License. */ -package androidx.collection; +package androidx.arch.core.internal; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import androidx.arch.core.internal.FastSafeIterableMap; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +@SuppressWarnings("deprecation") @RunWith(JUnit4.class) public class FastSafeIterableMapTest { @Test diff --git a/arch/core/core-common/src/test/java/androidx/collection/SafeIterableMapTest.java b/arch/core/core-common/src/test/java/androidx/arch/core/internal/SafeIterableMapTest.java similarity index 99% rename from arch/core/core-common/src/test/java/androidx/collection/SafeIterableMapTest.java rename to arch/core/core-common/src/test/java/androidx/arch/core/internal/SafeIterableMapTest.java index f425f6b35d4bd..d625ddeb524d0 100644 --- a/arch/core/core-common/src/test/java/androidx/collection/SafeIterableMapTest.java +++ b/arch/core/core-common/src/test/java/androidx/arch/core/internal/SafeIterableMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 The Android Open Source Project + * Copyright 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,12 @@ * limitations under the License. */ -package androidx.collection; +package androidx.arch.core.internal; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import androidx.arch.core.internal.SafeIterableMap; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -30,6 +28,7 @@ import java.util.Iterator; import java.util.Map.Entry; +@SuppressWarnings("deprecation") @RunWith(JUnit4.class) public class SafeIterableMapTest { From 7494c4217606835ce0ca1dcbcb05c5c5063b19c9 Mon Sep 17 00:00:00 2001 From: Melvin Moreno Date: Thu, 15 Jan 2026 11:23:44 -0600 Subject: [PATCH 10/11] Update Glimmer version Test: n/a Change-Id: Icdbaa51f5850941678c24831f8a8b0ccb2129ff2 --- libraryversions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraryversions.toml b/libraryversions.toml index 4eec0a43a661c..8af6b489d4cf8 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -183,7 +183,7 @@ WINDOW_SIDECAR = "1.0.0-rc01" WORK = "2.12.0-alpha01" XR_ARCORE = "1.0.0-alpha10" XR_COMPOSE = "1.0.0-alpha10" -XR_GLIMMER = "1.0.0-alpha04" +XR_GLIMMER = "1.0.0-alpha05" XR_PROJECTED = "1.0.0-alpha04" XR_RUNTIME = "1.0.0-alpha10" XR_SCENECORE = "1.0.0-alpha11" From ad78d95768ffb0c857248b01e2b485cbf1da2282 Mon Sep 17 00:00:00 2001 From: Lucas Muller Oliveira Date: Wed, 14 Jan 2026 16:12:38 +0000 Subject: [PATCH 11/11] Re-map FULLSCREEN to LARGE in Widget params. Providers should see the FULLSCREEN compat mode as a LARGE widget, providing the same layout it would for large (although the actual dp size may vary). The renderer will still send FULLSCREEN in the AIDL so that the right size can be cached. Test: unit tests Change-Id: I67cc16027a67d29845404499665fcaafab9547a7 --- .../androidx/glance/wear/WearWidgetParams.kt | 13 ++++++++++ .../glance/wear/WearWidgetParamsTest.kt | 25 ++++++++++++++++++ .../wear/parcel/WearWidgetProviderImplTest.kt | 26 +++++++++++++++++++ .../wear/parcel/WearWidgetProviderImpl.kt | 12 ++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/glance/wear/wear-core/src/main/java/androidx/glance/wear/WearWidgetParams.kt b/glance/wear/wear-core/src/main/java/androidx/glance/wear/WearWidgetParams.kt index f124651ccbb1e..dd94b52db8b33 100644 --- a/glance/wear/wear-core/src/main/java/androidx/glance/wear/WearWidgetParams.kt +++ b/glance/wear/wear-core/src/main/java/androidx/glance/wear/WearWidgetParams.kt @@ -58,6 +58,19 @@ public constructor( public val cornerRadiusDp: Float, ) { + @RestrictTo(LIBRARY_GROUP) + public fun withContainerType(containerType: Int = this.containerType): WearWidgetParams { + return WearWidgetParams( + instanceId = instanceId, + containerType = containerType, + widthDp = widthDp, + heightDp = heightDp, + horizontalPaddingDp = horizontalPaddingDp, + verticalPaddingDp = verticalPaddingDp, + cornerRadiusDp = cornerRadiusDp, + ) + } + /** Converts this object to [WearWidgetRequestParcel]. */ @RestrictTo(LIBRARY_GROUP) public fun toParcel(): WearWidgetRequestParcel { diff --git a/glance/wear/wear-core/src/test/java/androidx/glance/wear/WearWidgetParamsTest.kt b/glance/wear/wear-core/src/test/java/androidx/glance/wear/WearWidgetParamsTest.kt index 7bdb0dce1c5f3..cdcf4631758ac 100644 --- a/glance/wear/wear-core/src/test/java/androidx/glance/wear/WearWidgetParamsTest.kt +++ b/glance/wear/wear-core/src/test/java/androidx/glance/wear/WearWidgetParamsTest.kt @@ -49,4 +49,29 @@ class WearWidgetParamsTest { assertThat(restoredParams.verticalPaddingDp).isEqualTo(originalParams.verticalPaddingDp) assertThat(restoredParams.cornerRadiusDp).isEqualTo(originalParams.cornerRadiusDp) } + + @Test + fun withContainerType_matchesOriginalParamsExceptModified() { + val originalParams = + WearWidgetParams( + instanceId = WidgetInstanceId("ns", 123), + containerType = ContainerInfo.CONTAINER_TYPE_SMALL, + widthDp = 200.5f, + heightDp = 300.25f, + horizontalPaddingDp = 9f, + verticalPaddingDp = 8f, + cornerRadiusDp = 16f, + ) + + val modifiedParams = + originalParams.withContainerType(containerType = ContainerInfo.CONTAINER_TYPE_LARGE) + + assertThat(modifiedParams.instanceId).isEqualTo(originalParams.instanceId) + assertThat(modifiedParams.containerType).isEqualTo(ContainerInfo.CONTAINER_TYPE_LARGE) + assertThat(modifiedParams.widthDp).isEqualTo(originalParams.widthDp) + assertThat(modifiedParams.heightDp).isEqualTo(originalParams.heightDp) + assertThat(modifiedParams.horizontalPaddingDp).isEqualTo(originalParams.horizontalPaddingDp) + assertThat(modifiedParams.verticalPaddingDp).isEqualTo(originalParams.verticalPaddingDp) + assertThat(modifiedParams.cornerRadiusDp).isEqualTo(originalParams.cornerRadiusDp) + } } diff --git a/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetProviderImplTest.kt b/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetProviderImplTest.kt index fc80e2574d12b..18542bbd28e57 100644 --- a/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetProviderImplTest.kt +++ b/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetProviderImplTest.kt @@ -23,6 +23,7 @@ import androidx.compose.remote.player.core.RemoteDocument import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.glance.wear.ActiveWearWidgetHandle +import androidx.glance.wear.ContainerInfo.Companion.CONTAINER_TYPE_FULLSCREEN import androidx.glance.wear.ContainerInfo.Companion.CONTAINER_TYPE_LARGE import androidx.glance.wear.ContainerInfo.Companion.CONTAINER_TYPE_SMALL import androidx.glance.wear.GlanceWearWidget @@ -86,6 +87,29 @@ class WearWidgetProviderImplTest { contentChannel.receive() assertThat(testWidget.lastRequestedInstanceId).isEqualTo(widgetParams.instanceId) + assertThat(testWidget.lastRequestedContainerType).isEqualTo(CONTAINER_TYPE_LARGE) + } + + @Test + fun onWidgetRequest_remapsFullscreenToLarge() = runTest { + val widgetParams = + WearWidgetParams( + instanceId = WidgetInstanceId("namespace", 17), + containerType = CONTAINER_TYPE_FULLSCREEN, + widthDp = 200f, + heightDp = 200f, + horizontalPaddingDp = 8f, + verticalPaddingDp = 8f, + cornerRadiusDp = 16f, + ) + val channelWidgetCallback = ChannelWidgetCallback(this, contentChannel) + val provider = WearWidgetProviderImpl(context, testName, mainScope, testWidget) + + provider.onWidgetRequest(widgetParams.toParcel(), channelWidgetCallback) + contentChannel.receive() + + assertThat(testWidget.lastRequestedInstanceId).isEqualTo(widgetParams.instanceId) + assertThat(testWidget.lastRequestedContainerType).isEqualTo(CONTAINER_TYPE_LARGE) } @Test @@ -212,6 +236,7 @@ class WearWidgetProviderImplTest { private class TestGlanceWearWidget : GlanceWearWidget() { var lastRequestedInstanceId: WidgetInstanceId? = null + var lastRequestedContainerType: Int? = null var addedHandle: ActiveWearWidgetHandle? = null var removedHandle: ActiveWearWidgetHandle? = null var enableFailureMode = false @@ -222,6 +247,7 @@ class WearWidgetProviderImplTest { params: WearWidgetParams, ): WearWidgetData { lastRequestedInstanceId = params.instanceId + lastRequestedContainerType = params.containerType if (enableFailureMode) { throw Exception("Test exception") } diff --git a/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt b/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt index f00785947da23..c5ff75890191a 100644 --- a/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt +++ b/glance/wear/wear/src/main/java/androidx/glance/wear/parcel/WearWidgetProviderImpl.kt @@ -20,6 +20,7 @@ import android.content.ComponentName import android.content.Context import android.util.Log import androidx.glance.wear.ActiveWearWidgetHandle +import androidx.glance.wear.ContainerInfo import androidx.glance.wear.GlanceWearWidget import androidx.glance.wear.WearWidgetParams import androidx.glance.wear.cache.WearWidgetCache @@ -54,7 +55,16 @@ internal class WearWidgetProviderImpl( requireNotNull(callback) { "Invalid widget callback." } mainScope.launch { // TODO: Report errors in the callback if any of the following steps fail. - val params = WearWidgetParams.fromParcel(requestParcel) + val params = + WearWidgetParams.fromParcel(requestParcel).let { requestParams -> + if (requestParams.containerType == ContainerInfo.CONTAINER_TYPE_FULLSCREEN) { + requestParams.withContainerType( + containerType = ContainerInfo.CONTAINER_TYPE_LARGE + ) + } else { + requestParams + } + } launch { widgetCache.update {