diff --git a/turbo/src/main/assets/js/turbo_bridge.js b/turbo/src/main/assets/js/turbo_bridge.js index ff439f61..977ca7a5 100644 --- a/turbo/src/main/assets/js/turbo_bridge.js +++ b/turbo/src/main/assets/js/turbo_bridge.js @@ -57,6 +57,22 @@ } } + restoreCurrentVisit() { + // A synthetic "restore" visit to the currently rendered location can occur when + // visiting a web -> native -> back to web screen. In this situation, the connect() + // callback (from Stimulus) in bridge component controllers will not be called, + // since they are already connected. We need to notify the web bridge library + // that the webview has been reattached to manually trigger connect() and notify + // the native app so the native bridge component view state can be restored. + document.dispatchEvent(new Event("native:restore")) + } + + cacheSnapshot() { + if (window.Turbo) { + Turbo.session.view.cacheSnapshot() + } + } + // Current visit issueRequestForVisitWithIdentifier(identifier) { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt index b60c8fa1..d34c8c48 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt @@ -18,6 +18,7 @@ import dev.hotwire.turbo.session.TurboSession import dev.hotwire.turbo.session.TurboSessionCallback import dev.hotwire.turbo.session.TurboSessionModalResult import dev.hotwire.turbo.util.dispatcherProvider +import dev.hotwire.turbo.util.location import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit @@ -132,6 +133,24 @@ internal class TurboWebFragmentDelegate( } } + /** + * Should be called by the implementing Fragment during + * [androidx.fragment.app.Fragment.onDestroyView]. + */ + fun onDestroyView() { + // Manually cache a snapshot of the WebView when navigating from a + // web screen to a native screen. This allows a "restore" visit when + // revisiting this location again. + + val navHost = navDestination.sessionNavHostFragment + val currentBackStackEntry = navHost.navController.currentBackStackEntry + val currentLocation = currentBackStackEntry?.location + + if (session().currentVisit?.location != currentLocation) { + session().cacheSnapshot() + } + } + /** * Should be called by the implementing Fragment during * [dev.hotwire.turbo.nav.TurboNavDestination.refresh] @@ -323,8 +342,8 @@ internal class TurboWebFragmentDelegate( // Visit every time the WebView is reattached to the current Fragment. if (isWebViewAttachedToNewDestination) { val currentSessionVisitRestored = !isInitialVisit && - session().currentVisit?.destinationIdentifier == identifier && - session().restoreCurrentVisit(this) + session().currentVisit?.destinationIdentifier == identifier && + session().restoreCurrentVisit(this) if (!currentSessionVisitRestored) { showProgressView(location) diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt index d7c37811..2abf9479 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt @@ -40,6 +40,11 @@ abstract class TurboWebBottomSheetDialogFragment : TurboBottomSheetDialogFragmen webDelegate.onViewCreated() } + override fun onDestroyView() { + super.onDestroyView() + webDelegate.onDestroyView() + } + override fun activityResultLauncher(requestCode: Int): ActivityResultLauncher? { return when (requestCode) { TURBO_REQUEST_CODE_FILES -> webDelegate.fileChooserResultLauncher diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt index 28403616..ed2233f1 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt @@ -9,11 +9,11 @@ import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher import dev.hotwire.turbo.R import dev.hotwire.turbo.delegates.TurboWebFragmentDelegate +import dev.hotwire.turbo.errors.TurboVisitError import dev.hotwire.turbo.session.TurboSessionModalResult import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient -import dev.hotwire.turbo.errors.TurboVisitError /** * The base class from which all web "standard" fragments (non-dialogs) in a @@ -38,6 +38,11 @@ abstract class TurboWebFragment : TurboFragment(), TurboWebFragmentCallback { webDelegate.onViewCreated() } + override fun onDestroyView() { + super.onDestroyView() + webDelegate.onDestroyView() + } + override fun onStart() { super.onStart() diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt index 42144997..39543ce1 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt @@ -5,7 +5,14 @@ import android.content.Context import android.graphics.Bitmap import android.net.http.SslError import android.util.SparseArray -import android.webkit.* +import android.webkit.HttpAuthHandler +import android.webkit.JavascriptInterface +import android.webkit.RenderProcessGoneDetail +import android.webkit.SslErrorHandler +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.webkit.WebResourceErrorCompat @@ -20,7 +27,11 @@ import dev.hotwire.turbo.errors.HttpError import dev.hotwire.turbo.errors.LoadError import dev.hotwire.turbo.errors.WebError import dev.hotwire.turbo.errors.WebSslError -import dev.hotwire.turbo.http.* +import dev.hotwire.turbo.http.TurboHttpClient +import dev.hotwire.turbo.http.TurboHttpRepository +import dev.hotwire.turbo.http.TurboOfflineRequestHandler +import dev.hotwire.turbo.http.TurboPreCacheRequest +import dev.hotwire.turbo.http.TurboWebViewRequestInterceptor import dev.hotwire.turbo.nav.TurboNavDestination import dev.hotwire.turbo.util.isHttpGetRequest import dev.hotwire.turbo.util.logEvent @@ -30,7 +41,7 @@ import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit import dev.hotwire.turbo.visit.TurboVisitAction import dev.hotwire.turbo.visit.TurboVisitOptions -import java.util.* +import java.util.Date /** * This class is primarily responsible for managing an instance of an Android WebView that will @@ -170,9 +181,27 @@ class TurboSession internal constructor( visitRendered(visit.identifier) visitCompleted(visit.identifier, restorationIdentifier) + webView.restoreCurrentVisit() + return true } + /** + * Cache a snapshot of the current visit. + */ + fun cacheSnapshot() { + if (!isReady) return + + currentVisit?.let { + logEvent("cacheSnapshot", + "location" to it.location, + "visitIdentifier" to it.identifier + ) + + webView.cacheSnapshot() + } + } + internal fun removeCallback(callback: TurboSessionCallback) { currentVisit?.let { visit -> if (visit.callback == callback) { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt index 9cf41e2e..083af0ee 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt @@ -60,6 +60,14 @@ open class TurboWebView @JvmOverloads constructor(context: Context, attrs: Attri runJavascript("turboNative.visitRenderedForColdBoot('$coldBootVisitIdentifier')") } + internal fun cacheSnapshot() { + runJavascript("turboNative.cacheSnapshot()") + } + + internal fun restoreCurrentVisit() { + runJavascript("turboNative.restoreCurrentVisit()") + } + internal fun installBridge(onBridgeInstalled: () -> Unit) { val script = "window.turboNative == null" val bridge = context.contentFromAsset("js/turbo_bridge.js") diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt index 47221757..58d10cd0 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt @@ -216,6 +216,7 @@ class TurboSessionTest { assertThat(session.restoreCurrentVisit(callback)).isTrue() verify(callback, times(2)).visitCompleted(false) + verify(webView, times(1)).restoreCurrentVisit() } @Test