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 { 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 { 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 67ba4bc25043b..7252c7ba8a47c 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 = @@ -2505,7 +2504,7 @@ internal class GapComposer( stackTraceForGroup(groupIndex, dataIndex) + parentStackTrace() } ?: emptyList() - return ComposeStackTrace(stackTrace) + return ComposeStackTrace(stackTrace, sourceMarkersEnabled) } @OptIn(ComposeToolingApi::class) @@ -2516,7 +2515,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 } 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 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/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..e1515c35a3293 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 @@ -42,7 +43,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 @@ -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 { 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) 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" 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() + } } } 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() + } +} 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() * } * } * ``` 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 {