From 67ba864025db17a44f681ea63936fb6aeb52622a Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Fri, 5 Apr 2024 12:40:53 -0400 Subject: [PATCH] Expose guest Redwood version to host (#1939) In order to ensure the guest is fully setup such that we can query this, we have to defer creating the host-side bridge until we have received some changes. --- .../api/android/redwood-treehouse-guest.api | 1 + .../api/jvm/redwood-treehouse-guest.api | 1 + .../api/redwood-treehouse-guest.klib.api | 2 + .../redwood/treehouse/StandardAppLifecycle.kt | 4 ++ .../app/cash/redwood/treehouse/CodeSession.kt | 3 ++ .../redwood/treehouse/TreehouseAppContent.kt | 39 ++++++++++--------- .../redwood/treehouse/ZiplineCodeSession.kt | 14 ++++++- .../treehouse/AbstractFrameClockTest.kt | 2 + .../cash/redwood/treehouse/FakeAppService.kt | 7 ++++ .../cash/redwood/treehouse/FakeCodeSession.kt | 6 +++ .../api/android/redwood-treehouse.api | 1 + .../api/jvm/redwood-treehouse.api | 1 + .../api/redwood-treehouse.klib.api | 2 + redwood-treehouse/api/zipline-api.toml | 3 ++ .../cash/redwood/treehouse/AppLifecycle.kt | 9 ++++- 15 files changed, 75 insertions(+), 20 deletions(-) diff --git a/redwood-treehouse-guest/api/android/redwood-treehouse-guest.api b/redwood-treehouse-guest/api/android/redwood-treehouse-guest.api index 80862822d2..7b89931c5d 100644 --- a/redwood-treehouse-guest/api/android/redwood-treehouse-guest.api +++ b/redwood-treehouse-guest/api/android/redwood-treehouse-guest.api @@ -2,6 +2,7 @@ public final class app/cash/redwood/treehouse/StandardAppLifecycle : app/cash/re public synthetic fun (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close ()V public final fun getFrameClock ()Landroidx/compose/runtime/MonotonicFrameClock; + public fun getGuestProtocolVersion-7jYel6c ()Ljava/lang/String; public fun sendFrame (J)V public fun start (Lapp/cash/redwood/treehouse/AppLifecycle$Host;)V } diff --git a/redwood-treehouse-guest/api/jvm/redwood-treehouse-guest.api b/redwood-treehouse-guest/api/jvm/redwood-treehouse-guest.api index 80862822d2..7b89931c5d 100644 --- a/redwood-treehouse-guest/api/jvm/redwood-treehouse-guest.api +++ b/redwood-treehouse-guest/api/jvm/redwood-treehouse-guest.api @@ -2,6 +2,7 @@ public final class app/cash/redwood/treehouse/StandardAppLifecycle : app/cash/re public synthetic fun (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close ()V public final fun getFrameClock ()Landroidx/compose/runtime/MonotonicFrameClock; + public fun getGuestProtocolVersion-7jYel6c ()Ljava/lang/String; public fun sendFrame (J)V public fun start (Lapp/cash/redwood/treehouse/AppLifecycle$Host;)V } diff --git a/redwood-treehouse-guest/api/redwood-treehouse-guest.klib.api b/redwood-treehouse-guest/api/redwood-treehouse-guest.klib.api index f2cfe30ca5..ff2fc2558b 100644 --- a/redwood-treehouse-guest/api/redwood-treehouse-guest.klib.api +++ b/redwood-treehouse-guest/api/redwood-treehouse-guest.klib.api @@ -12,5 +12,7 @@ final class app.cash.redwood.treehouse/StandardAppLifecycle : app.cash.redwood.t final fun start(app.cash.redwood.treehouse/AppLifecycle.Host) // app.cash.redwood.treehouse/StandardAppLifecycle.start|start(app.cash.redwood.treehouse.AppLifecycle.Host){}[0] final val frameClock // app.cash.redwood.treehouse/StandardAppLifecycle.frameClock|{}frameClock[0] final fun (): androidx.compose.runtime/MonotonicFrameClock // app.cash.redwood.treehouse/StandardAppLifecycle.frameClock.|(){}[0] + final val guestProtocolVersion // app.cash.redwood.treehouse/StandardAppLifecycle.guestProtocolVersion|{}guestProtocolVersion[0] + final fun (): app.cash.redwood.protocol/RedwoodVersion // app.cash.redwood.treehouse/StandardAppLifecycle.guestProtocolVersion.|(){}[0] } final fun (app.cash.redwood.treehouse/TreehouseUi).app.cash.redwood.treehouse/asZiplineTreehouseUi(app.cash.redwood.treehouse/StandardAppLifecycle): app.cash.redwood.treehouse/ZiplineTreehouseUi // app.cash.redwood.treehouse/asZiplineTreehouseUi|asZiplineTreehouseUi@app.cash.redwood.treehouse.TreehouseUi(app.cash.redwood.treehouse.StandardAppLifecycle){}[0] diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt index c96e9ad591..cd0293a73e 100644 --- a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt +++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt @@ -23,6 +23,7 @@ import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.protocol.guest.ProtocolBridge import app.cash.redwood.protocol.guest.ProtocolMismatchHandler +import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.treehouse.AppLifecycle.Host import app.cash.zipline.ZiplineApiMismatchException import kotlin.coroutines.CoroutineContext @@ -38,6 +39,9 @@ public class StandardAppLifecycle( private var started = false private lateinit var host: Host + override val guestProtocolVersion: RedwoodVersion + get() = guestRedwoodVersion + internal val hostProtocolVersion: RedwoodVersion get() { return try { host.hostProtocolVersion diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt index 1167d4cdfe..70e0412c01 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.protocol.RedwoodVersion import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -55,6 +56,8 @@ internal abstract class CodeSession( abstract val json: Json + abstract val guestProtocolVersion: RedwoodVersion + fun start() { dispatchers.checkUi() scope.launch(dispatchers.zipline) { diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt index e6c7ec0d5f..04ca3ab5d1 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt @@ -18,7 +18,6 @@ package app.cash.redwood.treehouse import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.Event import app.cash.redwood.protocol.EventSink -import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.host.ProtocolBridge import app.cash.redwood.protocol.host.ProtocolFactory import app.cash.redwood.ui.OnBackPressedCallback @@ -313,7 +312,10 @@ private class ViewContentCodeBinding( /** Only accessed on [TreehouseDispatchers.ui]. Null before [initView] and after [cancel]. */ private var viewOrNull: TreehouseView<*>? = null - /** Only accessed on [TreehouseDispatchers.ui]. Null before [initView] and after [cancel]. */ + /** + * Only accessed on [TreehouseDispatchers.ui]. + * Null before [initView]+[receiveChangesOnUiDispatcher] and after [cancel]. + */ private var bridgeOrNull: ProtocolBridge<*>? = null /** Only accessed on [TreehouseDispatchers.zipline]. */ @@ -351,18 +353,6 @@ private class ViewContentCodeBinding( view.saveCallback = this - @Suppress("UNCHECKED_CAST") // We don't have a type parameter for the widget type. - bridgeOrNull = ProtocolBridge( - // TODO Wire through guest version. Wanted this from AppLifecycle but it's bound too late. - guestVersion = RedwoodVersion.Unknown, - container = view.children as Widget.Children, - factory = view.widgetSystem.widgetFactory( - json = codeSession.json, - protocolMismatchHandler = eventPublisher.widgetProtocolMismatchHandler, - ) as ProtocolFactory, - eventSink = this, - ) - // Apply all the changes received before we had a view to apply them to. while (true) { val changes = changesAwaitingInitView.removeFirstOrNull() ?: break @@ -388,14 +378,12 @@ private class ViewContentCodeBinding( } private fun receiveChangesOnUiDispatcher(changes: List) { - val view = viewOrNull - val bridge = bridgeOrNull - if (canceled) { return } - if (view == null || bridge == null) { + val view = viewOrNull + if (view == null) { if (changesAwaitingInitView.isEmpty()) { // Unblock coroutines suspended on TreehouseAppContent.awaitContent(). val currentState = stateFlow.value @@ -414,6 +402,21 @@ private class ViewContentCodeBinding( return } + var bridge = bridgeOrNull + if (bridge == null) { + @Suppress("UNCHECKED_CAST") // We don't have a type parameter for the widget type. + bridge = ProtocolBridge( + guestVersion = codeSession.guestProtocolVersion, + container = view.children as Widget.Children, + factory = view.widgetSystem.widgetFactory( + json = codeSession.json, + protocolMismatchHandler = eventPublisher.widgetProtocolMismatchHandler, + ) as ProtocolFactory, + eventSink = this, + ) + bridgeOrNull = bridge + } + if (changesCount++ == 0) { view.reset() codeEventPublisher.onCodeLoaded(view, isInitialLaunch) diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ZiplineCodeSession.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ZiplineCodeSession.kt index ae0dbaf965..e81621b586 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ZiplineCodeSession.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ZiplineCodeSession.kt @@ -15,7 +15,9 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.protocol.RedwoodVersion import app.cash.zipline.Zipline +import app.cash.zipline.ZiplineApiMismatchException import app.cash.zipline.ZiplineScope import app.cash.zipline.withScope import kotlinx.coroutines.CoroutineScope @@ -35,12 +37,22 @@ internal class ZiplineCodeSession( appService = appService, ) { private val ziplineScope = ZiplineScope() + private lateinit var appLifecycle: AppLifecycle override val json: Json get() = zipline.json + override val guestProtocolVersion: RedwoodVersion + get() { + return try { + appLifecycle.guestProtocolVersion + } catch (_: ZiplineApiMismatchException) { + RedwoodVersion.Unknown + } + } + override fun ziplineStart() { - val appLifecycle = appService.withScope(ziplineScope).appLifecycle + appLifecycle = appService.withScope(ziplineScope).appLifecycle val host = RealAppLifecycleHost( appLifecycle = appLifecycle, diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/AbstractFrameClockTest.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/AbstractFrameClockTest.kt index ef89c9caad..37a1f9dbf6 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/AbstractFrameClockTest.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/AbstractFrameClockTest.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.treehouse.AppLifecycle.Host import assertk.all import assertk.assertThat @@ -42,6 +43,7 @@ abstract class AbstractFrameClockTest { val frameTimes = Channel(Channel.UNLIMITED) val appLifecycle = object : AppLifecycle { + override val guestProtocolVersion get() = RedwoodVersion.Unknown override fun start(host: Host) { } override fun sendFrame(timeNanos: Long) { diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeAppService.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeAppService.kt index 63c51fc199..39bcb42d77 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeAppService.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeAppService.kt @@ -15,6 +15,9 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.protocol.RedwoodVersion +import app.cash.redwood.protocol.host.hostRedwoodVersion + internal class FakeAppService private constructor( private val name: String, private val eventLog: EventLog, @@ -26,6 +29,10 @@ internal class FakeAppService private constructor( get() = mutableUis.toList() override val appLifecycle = object : AppLifecycle { + override val guestProtocolVersion: RedwoodVersion + // Use latest host version as the guest version to avoid any compatibility behavior. + get() = hostRedwoodVersion + override fun start(host: AppLifecycle.Host) { eventLog += "$name.appLifecycle.start()" } diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeSession.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeSession.kt index fee693446c..9d2af2321d 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeSession.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeSession.kt @@ -15,6 +15,8 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.protocol.RedwoodVersion +import app.cash.redwood.protocol.host.hostRedwoodVersion import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.json.Json @@ -33,6 +35,10 @@ internal class FakeCodeSession( override val json: Json get() = Json + override val guestProtocolVersion: RedwoodVersion + // Use latest host version as the guest version to avoid any compatibility behavior. + get() = hostRedwoodVersion + override fun ziplineStart() { eventLog += "$name.start()" } diff --git a/redwood-treehouse/api/android/redwood-treehouse.api b/redwood-treehouse/api/android/redwood-treehouse.api index 489902bc52..589fed80e0 100644 --- a/redwood-treehouse/api/android/redwood-treehouse.api +++ b/redwood-treehouse/api/android/redwood-treehouse.api @@ -1,5 +1,6 @@ public abstract interface class app/cash/redwood/treehouse/AppLifecycle : app/cash/zipline/ZiplineService { public static final field Companion Lapp/cash/redwood/treehouse/AppLifecycle$Companion; + public abstract fun getGuestProtocolVersion-7jYel6c ()Ljava/lang/String; public abstract fun sendFrame (J)V public abstract fun start (Lapp/cash/redwood/treehouse/AppLifecycle$Host;)V } diff --git a/redwood-treehouse/api/jvm/redwood-treehouse.api b/redwood-treehouse/api/jvm/redwood-treehouse.api index 489902bc52..589fed80e0 100644 --- a/redwood-treehouse/api/jvm/redwood-treehouse.api +++ b/redwood-treehouse/api/jvm/redwood-treehouse.api @@ -1,5 +1,6 @@ public abstract interface class app/cash/redwood/treehouse/AppLifecycle : app/cash/zipline/ZiplineService { public static final field Companion Lapp/cash/redwood/treehouse/AppLifecycle$Companion; + public abstract fun getGuestProtocolVersion-7jYel6c ()Ljava/lang/String; public abstract fun sendFrame (J)V public abstract fun start (Lapp/cash/redwood/treehouse/AppLifecycle$Host;)V } diff --git a/redwood-treehouse/api/redwood-treehouse.klib.api b/redwood-treehouse/api/redwood-treehouse.klib.api index c88bf2aba5..9baf59529b 100644 --- a/redwood-treehouse/api/redwood-treehouse.klib.api +++ b/redwood-treehouse/api/redwood-treehouse.klib.api @@ -18,6 +18,8 @@ abstract interface app.cash.redwood.treehouse/AppLifecycle : app.cash.zipline/Zi abstract fun (): app.cash.redwood.protocol/RedwoodVersion // app.cash.redwood.treehouse/AppLifecycle.Host.hostProtocolVersion.|(){}[0] final object Companion // app.cash.redwood.treehouse/AppLifecycle.Host.Companion|null[0] } + abstract val guestProtocolVersion // app.cash.redwood.treehouse/AppLifecycle.guestProtocolVersion|{}guestProtocolVersion[0] + abstract fun (): app.cash.redwood.protocol/RedwoodVersion // app.cash.redwood.treehouse/AppLifecycle.guestProtocolVersion.|(){}[0] final object Companion // app.cash.redwood.treehouse/AppLifecycle.Companion|null[0] } abstract interface app.cash.redwood.treehouse/AppService : app.cash.zipline/ZiplineService { // app.cash.redwood.treehouse/AppService|null[0] diff --git a/redwood-treehouse/api/zipline-api.toml b/redwood-treehouse/api/zipline-api.toml index c3fc110e19..2a73ae8ffd 100644 --- a/redwood-treehouse/api/zipline-api.toml +++ b/redwood-treehouse/api/zipline-api.toml @@ -9,6 +9,9 @@ functions = [ # fun start(app.cash.redwood.treehouse.AppLifecycle.Host): kotlin.Unit "Xd61ecfL", + + # val guestProtocolVersion: app.cash.redwood.protocol.RedwoodVersion + "0iFPbldz", ] [app.cash.redwood.treehouse.AppLifecycle.Host] diff --git a/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt b/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt index 88797cb19c..bb9a62baf9 100644 --- a/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt +++ b/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt @@ -25,6 +25,13 @@ import kotlinx.serialization.Contextual @ObjCName("AppLifecycle", exact = true) public interface AppLifecycle : ZiplineService { + /** + * The Redwood version of the guest. + * This may be used to alter the behavior to work around bugs discovered in the future, and to + * ensure the serialized protocol remains compatible with what the guest expects. + */ + public val guestProtocolVersion: RedwoodVersion + public fun start(host: Host) public fun sendFrame(timeNanos: @Contextual Long) @@ -34,7 +41,7 @@ public interface AppLifecycle : ZiplineService { /** * The Redwood version of the host. * This may be used to alter the behavior to work around bugs discovered in the future, and to - * ensure the serialized protocol is remains compatible with what the host expects. + * ensure the serialized protocol remains compatible with what the host expects. */ public val hostProtocolVersion: RedwoodVersion