|
|
|
@ -0,0 +1,632 @@
|
|
|
|
|
/*
|
|
|
|
|
* Copyright © 2023 Harsh Shandilya.
|
|
|
|
|
* Use of this source code is governed by an MIT-style
|
|
|
|
|
* license that can be found in the LICENSE file or at
|
|
|
|
|
* https://opensource.org/licenses/MIT.
|
|
|
|
|
*/
|
|
|
|
|
package com.google.accompanist.web
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
|
import android.os.Bundle
|
|
|
|
|
import android.view.ViewGroup.LayoutParams
|
|
|
|
|
import android.webkit.WebChromeClient
|
|
|
|
|
import android.webkit.WebResourceError
|
|
|
|
|
import android.webkit.WebResourceRequest
|
|
|
|
|
import android.webkit.WebView
|
|
|
|
|
import android.webkit.WebViewClient
|
|
|
|
|
import android.widget.FrameLayout
|
|
|
|
|
import androidx.activity.compose.BackHandler
|
|
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
|
import androidx.compose.runtime.Immutable
|
|
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
|
|
|
import androidx.compose.runtime.Stable
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
|
|
import androidx.compose.runtime.saveable.Saver
|
|
|
|
|
import androidx.compose.runtime.saveable.mapSaver
|
|
|
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
|
|
|
import androidx.compose.runtime.setValue
|
|
|
|
|
import androidx.compose.runtime.snapshotFlow
|
|
|
|
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
import androidx.compose.ui.viewinterop.AndroidView
|
|
|
|
|
import com.google.accompanist.web.LoadingState.Finished
|
|
|
|
|
import com.google.accompanist.web.LoadingState.Loading
|
|
|
|
|
import dev.msfjarvis.claw.core.coroutines.DefaultDispatcherProvider
|
|
|
|
|
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
|
|
|
|
import kotlinx.collections.immutable.ImmutableMap
|
|
|
|
|
import kotlinx.collections.immutable.persistentMapOf
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
|
|
|
|
*
|
|
|
|
|
* If you require more customisation you are most likely better rolling your own and using this
|
|
|
|
|
* wrapper as an example.
|
|
|
|
|
*
|
|
|
|
|
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it is
|
|
|
|
|
* incorrectly sizing, use the layoutParams composable function instead.
|
|
|
|
|
*
|
|
|
|
|
* @param state The webview state holder where the Uri to load is defined.
|
|
|
|
|
* @param modifier A compose modifier
|
|
|
|
|
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
|
|
|
|
* the WebView back.
|
|
|
|
|
* @param navigator An optional navigator object that can be used to control the WebView's
|
|
|
|
|
* navigation from outside the composable.
|
|
|
|
|
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
|
|
|
|
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will
|
|
|
|
|
* be subsequently overwritten after this lambda is called.
|
|
|
|
|
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
|
|
|
|
* you need to save and restore state in this WebView.
|
|
|
|
|
* @param client Provides access to WebViewClient via subclassing
|
|
|
|
|
* @param chromeClient Provides access to WebChromeClient via subclassing
|
|
|
|
|
* @param factory An optional WebView factory for using a custom subclass of WebView
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun WebView(
|
|
|
|
|
state: WebViewState,
|
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
|
captureBackPresses: Boolean = true,
|
|
|
|
|
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
|
|
|
|
onCreated: (WebView) -> Unit = {},
|
|
|
|
|
onDispose: (WebView) -> Unit = {},
|
|
|
|
|
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
|
|
|
|
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
|
|
|
|
factory: ((Context) -> WebView)? = null,
|
|
|
|
|
) {
|
|
|
|
|
BoxWithConstraints(modifier) {
|
|
|
|
|
// WebView changes it's layout strategy based on
|
|
|
|
|
// it's layoutParams. We convert from Compose Modifier to
|
|
|
|
|
// layout params here.
|
|
|
|
|
val width =
|
|
|
|
|
if (constraints.hasFixedWidth) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
|
|
|
|
val height =
|
|
|
|
|
if (constraints.hasFixedHeight) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
|
|
|
|
|
|
|
|
|
val layoutParams = FrameLayout.LayoutParams(width, height)
|
|
|
|
|
|
|
|
|
|
WebView(
|
|
|
|
|
state,
|
|
|
|
|
layoutParams,
|
|
|
|
|
Modifier,
|
|
|
|
|
captureBackPresses,
|
|
|
|
|
navigator,
|
|
|
|
|
onCreated,
|
|
|
|
|
onDispose,
|
|
|
|
|
client,
|
|
|
|
|
chromeClient,
|
|
|
|
|
factory
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
|
|
|
|
*
|
|
|
|
|
* If you require more customisation you are most likely better rolling your own and using this
|
|
|
|
|
* wrapper as an example.
|
|
|
|
|
*
|
|
|
|
|
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it is
|
|
|
|
|
* incorrectly sizing, use the layoutParams composable function instead.
|
|
|
|
|
*
|
|
|
|
|
* @param state The webview state holder where the Uri to load is defined.
|
|
|
|
|
* @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView.
|
|
|
|
|
* @param modifier A compose modifier
|
|
|
|
|
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
|
|
|
|
* the WebView back.
|
|
|
|
|
* @param navigator An optional navigator object that can be used to control the WebView's
|
|
|
|
|
* navigation from outside the composable.
|
|
|
|
|
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
|
|
|
|
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will
|
|
|
|
|
* be subsequently overwritten after this lambda is called.
|
|
|
|
|
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
|
|
|
|
* you need to save and restore state in this WebView.
|
|
|
|
|
* @param client Provides access to WebViewClient via subclassing
|
|
|
|
|
* @param chromeClient Provides access to WebChromeClient via subclassing
|
|
|
|
|
* @param factory An optional WebView factory for using a custom subclass of WebView
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun WebView(
|
|
|
|
|
state: WebViewState,
|
|
|
|
|
layoutParams: FrameLayout.LayoutParams,
|
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
|
captureBackPresses: Boolean = true,
|
|
|
|
|
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
|
|
|
|
onCreated: (WebView) -> Unit = {},
|
|
|
|
|
onDispose: (WebView) -> Unit = {},
|
|
|
|
|
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
|
|
|
|
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
|
|
|
|
factory: ((Context) -> WebView)? = null,
|
|
|
|
|
) {
|
|
|
|
|
val webView = state.webView
|
|
|
|
|
|
|
|
|
|
BackHandler(captureBackPresses && navigator.canGoBack) { webView?.goBack() }
|
|
|
|
|
|
|
|
|
|
webView?.let { wv ->
|
|
|
|
|
LaunchedEffect(wv, navigator) { with(navigator) { wv.handleNavigationEvents() } }
|
|
|
|
|
|
|
|
|
|
LaunchedEffect(wv, state) {
|
|
|
|
|
snapshotFlow { state.content }
|
|
|
|
|
.collect { content ->
|
|
|
|
|
when (content) {
|
|
|
|
|
is WebContent.Url -> {
|
|
|
|
|
wv.loadUrl(content.url, content.additionalHttpHeaders)
|
|
|
|
|
}
|
|
|
|
|
is WebContent.Data -> {
|
|
|
|
|
wv.loadDataWithBaseURL(
|
|
|
|
|
content.baseUrl,
|
|
|
|
|
content.data,
|
|
|
|
|
content.mimeType,
|
|
|
|
|
content.encoding,
|
|
|
|
|
content.historyUrl
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
is WebContent.Post -> {
|
|
|
|
|
wv.postUrl(content.url, content.postData)
|
|
|
|
|
}
|
|
|
|
|
is WebContent.NavigatorOnly -> {
|
|
|
|
|
// NO-OP
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the state of the client and chrome client
|
|
|
|
|
// This is done internally to ensure they always are the same instance as the
|
|
|
|
|
// parent Web composable
|
|
|
|
|
client.state = state
|
|
|
|
|
client.navigator = navigator
|
|
|
|
|
chromeClient.state = state
|
|
|
|
|
|
|
|
|
|
AndroidView(
|
|
|
|
|
factory = { context ->
|
|
|
|
|
(factory?.invoke(context) ?: WebView(context))
|
|
|
|
|
.apply {
|
|
|
|
|
onCreated(this)
|
|
|
|
|
|
|
|
|
|
this.layoutParams = layoutParams
|
|
|
|
|
|
|
|
|
|
state.viewState?.let { this.restoreState(it) }
|
|
|
|
|
|
|
|
|
|
webChromeClient = chromeClient
|
|
|
|
|
webViewClient = client
|
|
|
|
|
}
|
|
|
|
|
.also { state.webView = it }
|
|
|
|
|
},
|
|
|
|
|
modifier = modifier,
|
|
|
|
|
onRelease = { onDispose(it) }
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AccompanistWebViewClient
|
|
|
|
|
*
|
|
|
|
|
* A parent class implementation of WebViewClient that can be subclassed to add custom behaviour.
|
|
|
|
|
*
|
|
|
|
|
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
|
|
|
|
* class that can be overriden if further custom behaviour is required.
|
|
|
|
|
*/
|
|
|
|
|
public open class AccompanistWebViewClient : WebViewClient() {
|
|
|
|
|
public open lateinit var state: WebViewState
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
public open lateinit var navigator: WebViewNavigator
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
|
|
|
|
super.onPageStarted(view, url, favicon)
|
|
|
|
|
state.loadingState = Loading(0.0f)
|
|
|
|
|
state.errorsForCurrentRequest.clear()
|
|
|
|
|
state.pageTitle = null
|
|
|
|
|
state.pageIcon = null
|
|
|
|
|
|
|
|
|
|
state.lastLoadedUrl = url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
|
|
|
super.onPageFinished(view, url)
|
|
|
|
|
state.loadingState = Finished
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {
|
|
|
|
|
super.doUpdateVisitedHistory(view, url, isReload)
|
|
|
|
|
|
|
|
|
|
navigator.canGoBack = view.canGoBack()
|
|
|
|
|
navigator.canGoForward = view.canGoForward()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onReceivedError(
|
|
|
|
|
view: WebView,
|
|
|
|
|
request: WebResourceRequest?,
|
|
|
|
|
error: WebResourceError?
|
|
|
|
|
) {
|
|
|
|
|
super.onReceivedError(view, request, error)
|
|
|
|
|
|
|
|
|
|
if (error != null) {
|
|
|
|
|
state.errorsForCurrentRequest.add(WebViewError(request, error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AccompanistWebChromeClient
|
|
|
|
|
*
|
|
|
|
|
* A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour.
|
|
|
|
|
*
|
|
|
|
|
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
|
|
|
|
* class that can be overridden if further custom behaviour is required.
|
|
|
|
|
*/
|
|
|
|
|
public open class AccompanistWebChromeClient : WebChromeClient() {
|
|
|
|
|
public open lateinit var state: WebViewState
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
override fun onReceivedTitle(view: WebView, title: String?) {
|
|
|
|
|
super.onReceivedTitle(view, title)
|
|
|
|
|
state.pageTitle = title
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onReceivedIcon(view: WebView, icon: Bitmap?) {
|
|
|
|
|
super.onReceivedIcon(view, icon)
|
|
|
|
|
state.pageIcon = icon
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
|
|
|
|
super.onProgressChanged(view, newProgress)
|
|
|
|
|
if (state.loadingState is Finished) return
|
|
|
|
|
state.loadingState = Loading(newProgress / 100.0f)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class WebContent {
|
|
|
|
|
public data class Url(
|
|
|
|
|
val url: String,
|
|
|
|
|
val additionalHttpHeaders: Map<String, String> = emptyMap(),
|
|
|
|
|
) : WebContent()
|
|
|
|
|
|
|
|
|
|
public data class Data(
|
|
|
|
|
val data: String,
|
|
|
|
|
val baseUrl: String? = null,
|
|
|
|
|
val encoding: String = "utf-8",
|
|
|
|
|
val mimeType: String? = null,
|
|
|
|
|
val historyUrl: String? = null
|
|
|
|
|
) : WebContent()
|
|
|
|
|
|
|
|
|
|
public data class Post(val url: String, val postData: ByteArray) : WebContent() {
|
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
|
|
|
if (this === other) return true
|
|
|
|
|
if (javaClass != other?.javaClass) return false
|
|
|
|
|
|
|
|
|
|
other as Post
|
|
|
|
|
|
|
|
|
|
if (url != other.url) return false
|
|
|
|
|
if (!postData.contentEquals(other.postData)) return false
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun hashCode(): Int {
|
|
|
|
|
var result = url.hashCode()
|
|
|
|
|
result = 31 * result + postData.contentHashCode()
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public data object NavigatorOnly : WebContent()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun WebContent.withUrl(url: String) =
|
|
|
|
|
when (this) {
|
|
|
|
|
is WebContent.Url -> copy(url = url)
|
|
|
|
|
else -> WebContent.Url(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Sealed class for constraining possible loading states. See [Loading] and [Finished]. */
|
|
|
|
|
public sealed class LoadingState {
|
|
|
|
|
/** Describes a WebView that has not yet loaded for the first time. */
|
|
|
|
|
public data object Initializing : LoadingState()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Describes a webview between `onPageStarted` and `onPageFinished` events, contains a [progress]
|
|
|
|
|
* property which is updated by the webview.
|
|
|
|
|
*/
|
|
|
|
|
public data class Loading(val progress: Float) : LoadingState()
|
|
|
|
|
|
|
|
|
|
/** Describes a webview that has finished loading content. */
|
|
|
|
|
public data object Finished : LoadingState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A state holder to hold the state for the WebView. In most cases this will be remembered using the
|
|
|
|
|
* rememberWebViewState(uri) function.
|
|
|
|
|
*/
|
|
|
|
|
@Stable
|
|
|
|
|
public class WebViewState(webContent: WebContent) {
|
|
|
|
|
public var lastLoadedUrl: String? by mutableStateOf(null)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
/** The content being loaded by the WebView */
|
|
|
|
|
public var content: WebContent by mutableStateOf(webContent)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with
|
|
|
|
|
* progress) or the data loading has [LoadingState.Finished]. See [LoadingState]
|
|
|
|
|
*/
|
|
|
|
|
public var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
/** Whether the webview is currently loading data in its main frame */
|
|
|
|
|
public val isLoading: Boolean
|
|
|
|
|
get() = loadingState !is Finished
|
|
|
|
|
|
|
|
|
|
/** The title received from the loaded content of the current page */
|
|
|
|
|
public var pageTitle: String? by mutableStateOf(null)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
/** the favicon received from the loaded content of the current page */
|
|
|
|
|
public var pageIcon: Bitmap? by mutableStateOf(null)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A list for errors captured in the last load. Reset when a new page is loaded. Errors could be
|
|
|
|
|
* from any resource (iframe, image, etc.), not just for the main page. For more fine grained
|
|
|
|
|
* control use the OnError callback of the WebView.
|
|
|
|
|
*/
|
|
|
|
|
public val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The saved view state from when the view was destroyed last. To restore state, use the navigator
|
|
|
|
|
* and only call loadUrl if the bundle is null. See WebViewSaveStateSample.
|
|
|
|
|
*/
|
|
|
|
|
public var viewState: Bundle? = null
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
// We need access to this in the state saver. An internal DisposableEffect or AndroidView
|
|
|
|
|
// onDestroy is called after the state saver and so can't be used.
|
|
|
|
|
internal var webView by mutableStateOf<WebView?>(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Allows control over the navigation of a WebView from outside the composable. E.g. for performing
|
|
|
|
|
* a back navigation in response to the user clicking the "up" button in a TopAppBar.
|
|
|
|
|
*
|
|
|
|
|
* @see [rememberWebViewNavigator]
|
|
|
|
|
*/
|
|
|
|
|
@Stable
|
|
|
|
|
public class WebViewNavigator(
|
|
|
|
|
private val coroutineScope: CoroutineScope,
|
|
|
|
|
private val dispatcherProvider: DispatcherProvider,
|
|
|
|
|
) {
|
|
|
|
|
private sealed interface NavigationEvent {
|
|
|
|
|
data object Back : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data object Forward : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data object Reload : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data object StopLoading : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data class LoadUrl(
|
|
|
|
|
val url: String,
|
|
|
|
|
val additionalHttpHeaders: Map<String, String> = emptyMap()
|
|
|
|
|
) : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data class LoadHtml(
|
|
|
|
|
val html: String,
|
|
|
|
|
val baseUrl: String? = null,
|
|
|
|
|
val mimeType: String? = null,
|
|
|
|
|
val encoding: String? = "utf-8",
|
|
|
|
|
val historyUrl: String? = null
|
|
|
|
|
) : NavigationEvent
|
|
|
|
|
|
|
|
|
|
data class PostUrl(val url: String, val postData: ByteArray) : NavigationEvent {
|
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
|
|
|
if (this === other) return true
|
|
|
|
|
if (javaClass != other?.javaClass) return false
|
|
|
|
|
|
|
|
|
|
other as PostUrl
|
|
|
|
|
|
|
|
|
|
if (url != other.url) return false
|
|
|
|
|
if (!postData.contentEquals(other.postData)) return false
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun hashCode(): Int {
|
|
|
|
|
var result = url.hashCode()
|
|
|
|
|
result = 31 * result + postData.contentHashCode()
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val navigationEvents: MutableSharedFlow<NavigationEvent> = MutableSharedFlow(replay = 1)
|
|
|
|
|
|
|
|
|
|
// Use Dispatchers.Main to ensure that the webview methods are called on UI thread
|
|
|
|
|
internal suspend fun WebView.handleNavigationEvents(): Nothing =
|
|
|
|
|
withContext(dispatcherProvider.main()) {
|
|
|
|
|
navigationEvents.collect { event ->
|
|
|
|
|
when (event) {
|
|
|
|
|
is NavigationEvent.Back -> goBack()
|
|
|
|
|
is NavigationEvent.Forward -> goForward()
|
|
|
|
|
is NavigationEvent.Reload -> reload()
|
|
|
|
|
is NavigationEvent.StopLoading -> stopLoading()
|
|
|
|
|
is NavigationEvent.LoadHtml ->
|
|
|
|
|
loadDataWithBaseURL(
|
|
|
|
|
event.baseUrl,
|
|
|
|
|
event.html,
|
|
|
|
|
event.mimeType,
|
|
|
|
|
event.encoding,
|
|
|
|
|
event.historyUrl
|
|
|
|
|
)
|
|
|
|
|
is NavigationEvent.LoadUrl -> {
|
|
|
|
|
loadUrl(event.url, event.additionalHttpHeaders)
|
|
|
|
|
}
|
|
|
|
|
is NavigationEvent.PostUrl -> {
|
|
|
|
|
postUrl(event.url, event.postData)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** True when the web view is able to navigate backwards, false otherwise. */
|
|
|
|
|
public var canGoBack: Boolean by mutableStateOf(false)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
/** True when the web view is able to navigate forwards, false otherwise. */
|
|
|
|
|
public var canGoForward: Boolean by mutableStateOf(false)
|
|
|
|
|
internal set
|
|
|
|
|
|
|
|
|
|
public fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
navigationEvents.emit(NavigationEvent.LoadUrl(url, additionalHttpHeaders))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public fun loadHtml(
|
|
|
|
|
html: String,
|
|
|
|
|
baseUrl: String? = null,
|
|
|
|
|
mimeType: String? = null,
|
|
|
|
|
encoding: String? = "utf-8",
|
|
|
|
|
historyUrl: String? = null
|
|
|
|
|
) {
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
navigationEvents.emit(NavigationEvent.LoadHtml(html, baseUrl, mimeType, encoding, historyUrl))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public fun postUrl(url: String, postData: ByteArray) {
|
|
|
|
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.PostUrl(url, postData)) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Navigates the webview back to the previous page. */
|
|
|
|
|
public fun navigateBack() {
|
|
|
|
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Navigates the webview forward after going back from a page. */
|
|
|
|
|
public fun navigateForward() {
|
|
|
|
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Reloads the current page in the webview. */
|
|
|
|
|
public fun reload() {
|
|
|
|
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Stops the current page load (if one is loading). */
|
|
|
|
|
public fun stopLoading() {
|
|
|
|
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided
|
|
|
|
|
* override.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun rememberWebViewNavigator(
|
|
|
|
|
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
|
|
|
|
dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(),
|
|
|
|
|
): WebViewNavigator =
|
|
|
|
|
remember(coroutineScope) { WebViewNavigator(coroutineScope, dispatcherProvider) }
|
|
|
|
|
|
|
|
|
|
/** A wrapper class to hold errors from the WebView. */
|
|
|
|
|
@Immutable
|
|
|
|
|
public data class WebViewError(
|
|
|
|
|
/** The request the error came from. */
|
|
|
|
|
val request: WebResourceRequest?,
|
|
|
|
|
/** The error that was reported. */
|
|
|
|
|
val error: WebResourceError
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a WebView state that is remembered across Compositions.
|
|
|
|
|
*
|
|
|
|
|
* @param url The url to load in the WebView
|
|
|
|
|
* @param additionalHttpHeaders Optional, additional HTTP headers that are passed to
|
|
|
|
|
* [WebView.loadUrl]. Note that these headers are used for all subsequent requests of the WebView.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun rememberWebViewState(
|
|
|
|
|
url: String,
|
|
|
|
|
additionalHttpHeaders: ImmutableMap<String, String> = persistentMapOf()
|
|
|
|
|
): WebViewState =
|
|
|
|
|
// Rather than using .apply {} here we will recreate the state, this prevents
|
|
|
|
|
// a recomposition loop when the webview updates the url itself.
|
|
|
|
|
remember {
|
|
|
|
|
WebViewState(WebContent.Url(url = url, additionalHttpHeaders = additionalHttpHeaders))
|
|
|
|
|
}
|
|
|
|
|
.apply {
|
|
|
|
|
this.content = WebContent.Url(url = url, additionalHttpHeaders = additionalHttpHeaders)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a WebView state that is remembered across Compositions.
|
|
|
|
|
*
|
|
|
|
|
* @param data The uri to load in the WebView
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun rememberWebViewStateWithHTMLData(
|
|
|
|
|
data: String,
|
|
|
|
|
baseUrl: String? = null,
|
|
|
|
|
encoding: String = "utf-8",
|
|
|
|
|
mimeType: String? = null,
|
|
|
|
|
historyUrl: String? = null
|
|
|
|
|
): WebViewState =
|
|
|
|
|
remember { WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl)) }
|
|
|
|
|
.apply { this.content = WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl) }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a WebView state that is remembered across Compositions.
|
|
|
|
|
*
|
|
|
|
|
* @param url The url to load in the WebView
|
|
|
|
|
* @param postData The data to be posted to the WebView with the url
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun rememberWebViewState(url: String, postData: ByteArray): WebViewState =
|
|
|
|
|
// Rather than using .apply {} here we will recreate the state, this prevents
|
|
|
|
|
// a recomposition loop when the webview updates the url itself.
|
|
|
|
|
remember { WebViewState(WebContent.Post(url = url, postData = postData)) }
|
|
|
|
|
.apply { this.content = WebContent.Post(url = url, postData = postData) }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a WebView state that is remembered across Compositions and saved across activity
|
|
|
|
|
* recreation. When using saved state, you cannot change the URL via recomposition. The only way to
|
|
|
|
|
* load a URL is via a WebViewNavigator.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
public fun rememberSaveableWebViewState(): WebViewState =
|
|
|
|
|
rememberSaveable(saver = WebStateSaver) { WebViewState(WebContent.NavigatorOnly) }
|
|
|
|
|
|
|
|
|
|
public val WebStateSaver: Saver<WebViewState, Any> = run {
|
|
|
|
|
val pageTitleKey = "pagetitle"
|
|
|
|
|
val lastLoadedUrlKey = "lastloaded"
|
|
|
|
|
val stateBundle = "bundle"
|
|
|
|
|
|
|
|
|
|
mapSaver(
|
|
|
|
|
save = {
|
|
|
|
|
val viewState = Bundle().apply { it.webView?.saveState(this) }
|
|
|
|
|
mapOf(
|
|
|
|
|
pageTitleKey to it.pageTitle,
|
|
|
|
|
lastLoadedUrlKey to it.lastLoadedUrl,
|
|
|
|
|
stateBundle to viewState
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
restore = {
|
|
|
|
|
WebViewState(WebContent.NavigatorOnly).apply {
|
|
|
|
|
this.pageTitle = it[pageTitleKey] as String?
|
|
|
|
|
this.lastLoadedUrl = it[lastLoadedUrlKey] as String?
|
|
|
|
|
this.viewState = it[stateBundle] as Bundle?
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|