Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions turbo/src/main/assets/js/turbo_bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ abstract class TurboWebBottomSheetDialogFragment : TurboBottomSheetDialogFragmen
webDelegate.onViewCreated()
}

override fun onDestroyView() {
super.onDestroyView()
webDelegate.onDestroyView()
}

override fun activityResultLauncher(requestCode: Int): ActivityResultLauncher<Intent>? {
return when (requestCode) {
TURBO_REQUEST_CODE_FILES -> webDelegate.fileChooserResultLauncher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,6 +38,11 @@ abstract class TurboWebFragment : TurboFragment(), TurboWebFragmentCallback {
webDelegate.onViewCreated()
}

override fun onDestroyView() {
super.onDestroyView()
webDelegate.onDestroyView()
}

override fun onStart() {
super.onStart()

Expand Down
35 changes: 32 additions & 3 deletions turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class TurboSessionTest {

assertThat(session.restoreCurrentVisit(callback)).isTrue()
verify(callback, times(2)).visitCompleted(false)
verify(webView, times(1)).restoreCurrentVisit()
}

@Test
Expand Down