Perf(ToolView): Optimize webview loading performance
This commit is contained in:
@@ -7,10 +7,10 @@
|
|||||||
<!-- es-module-shims -->
|
<!-- es-module-shims -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module" id="appSrc">{{replace_code}}</script>
|
<script type="module" id="appDictSrc">{{replace_dict_code}}</script>
|
||||||
|
<script type="module" id="appBaseSrc">{{replace_base_code}}</script>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<div
|
<div style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
|
||||||
style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
|
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,22 +10,21 @@ import android.util.Log
|
|||||||
import android.webkit.ConsoleMessage
|
import android.webkit.ConsoleMessage
|
||||||
import android.webkit.ValueCallback
|
import android.webkit.ValueCallback
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -59,12 +58,14 @@ internal fun ToolViewRoute(
|
|||||||
viewModel: ToolViewScreenViewModel = hiltViewModel(),
|
viewModel: ToolViewScreenViewModel = hiltViewModel(),
|
||||||
onBackClick: () -> Unit
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val isPreview by viewModel.isPreview.collectAsStateWithLifecycle()
|
|
||||||
val toolViewUiState by viewModel.toolViewUiState.collectAsStateWithLifecycle()
|
val toolViewUiState by viewModel.toolViewUiState.collectAsStateWithLifecycle()
|
||||||
|
val webViewInstanceState by viewModel.webviewInstance.collectAsStateWithLifecycle()
|
||||||
|
val isPreview by viewModel.isPreview.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ToolViewScreen(
|
ToolViewScreen(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
toolViewUiState = toolViewUiState,
|
toolViewUiState = toolViewUiState,
|
||||||
|
webViewInstanceState = webViewInstanceState,
|
||||||
isPreview = isPreview,
|
isPreview = isPreview,
|
||||||
onBackClick = onBackClick
|
onBackClick = onBackClick
|
||||||
)
|
)
|
||||||
@@ -76,48 +77,39 @@ internal fun ToolViewRoute(
|
|||||||
internal fun ToolViewScreen(
|
internal fun ToolViewScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
toolViewUiState: ToolViewUiState,
|
toolViewUiState: ToolViewUiState,
|
||||||
|
webViewInstanceState: WebViewInstanceState,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
onBackClick: () -> Unit
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var fileChooserCallback by remember { mutableStateOf<ValueCallback<Array<Uri>>?>(null) }
|
|
||||||
|
|
||||||
val fileChooserLauncher =
|
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
it.data?.data?.let { uri ->
|
|
||||||
fileChooserCallback?.onReceiveValue(arrayOf(uri))
|
|
||||||
} ?: {
|
|
||||||
fileChooserCallback?.onReceiveValue(emptyArray())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileChooserCallback?.onReceiveValue(emptyArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val permissionLauncher =
|
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
|
|
||||||
Permissions.continuation?.resume(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier,
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0)
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.windowInsetsPadding(
|
.windowInsetsPadding(
|
||||||
WindowInsets.safeDrawing.only(
|
WindowInsets.safeDrawing.only(
|
||||||
WindowInsetsSides.Horizontal
|
WindowInsetsSides.Horizontal
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
OxygenTopAppBar(
|
TopBar(
|
||||||
|
toolViewUiState = toolViewUiState,
|
||||||
|
isPreview = isPreview,
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
Content(
|
||||||
|
toolViewUiState = toolViewUiState,
|
||||||
|
webViewInstanceState = webViewInstanceState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
toolViewUiState: ToolViewUiState,
|
||||||
|
isPreview: Boolean,
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) = OxygenTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = when (toolViewUiState) {
|
text = when (toolViewUiState) {
|
||||||
@@ -138,6 +130,26 @@ internal fun ToolViewScreen(
|
|||||||
),
|
),
|
||||||
onNavigationClick = onBackClick
|
onNavigationClick = onBackClick
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(
|
||||||
|
toolViewUiState: ToolViewUiState,
|
||||||
|
webViewInstanceState: WebViewInstanceState
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var fileChooserCallback by remember { mutableStateOf<ValueCallback<Array<Uri>>?>(null) }
|
||||||
|
|
||||||
|
val fileChooserLauncher = rememberFileChooserLauncher(fileChooserCallback)
|
||||||
|
|
||||||
|
val permissionLauncher = rememberPermissionLauncher()
|
||||||
|
|
||||||
|
when (webViewInstanceState) {
|
||||||
|
WebViewInstanceState.Loading -> {
|
||||||
|
Indicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
is WebViewInstanceState.Success -> {
|
||||||
when (toolViewUiState) {
|
when (toolViewUiState) {
|
||||||
ToolViewUiState.Loading -> {
|
ToolViewUiState.Loading -> {
|
||||||
Indicator()
|
Indicator()
|
||||||
@@ -162,7 +174,87 @@ internal fun ToolViewScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding(),
|
.imePadding(),
|
||||||
state = webViewState,
|
state = webViewState,
|
||||||
chromeClient = remember {
|
chromeClient = rememberChromeClient(
|
||||||
|
context = context,
|
||||||
|
fileChooserLauncher = fileChooserLauncher
|
||||||
|
) {
|
||||||
|
fileChooserCallback = it
|
||||||
|
},
|
||||||
|
onCreated = initWebView(
|
||||||
|
context = context,
|
||||||
|
permissionLauncher = permissionLauncher
|
||||||
|
),
|
||||||
|
factory = {
|
||||||
|
webViewInstanceState.webView
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberFileChooserLauncher(fileChooserCallback: ValueCallback<Array<Uri>>?) =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
it.data?.data?.let { uri ->
|
||||||
|
fileChooserCallback?.onReceiveValue(arrayOf(uri))
|
||||||
|
} ?: {
|
||||||
|
fileChooserCallback?.onReceiveValue(emptyArray())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileChooserCallback?.onReceiveValue(emptyArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberPermissionLauncher() =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
|
Permissions.continuation?.resume(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun initWebView(
|
||||||
|
context: Context,
|
||||||
|
permissionLauncher: ManagedActivityResultLauncher<String, Boolean>
|
||||||
|
) = { webview: WebView ->
|
||||||
|
webview.settings.javaScriptEnabled = true
|
||||||
|
webview.settings.domStorageEnabled = true
|
||||||
|
webview.addJavascriptInterface(
|
||||||
|
NativeWebApi(context = context, webView = webview, permissionLauncher),
|
||||||
|
"NativeApi"
|
||||||
|
)
|
||||||
|
webview.setDownloadListener { url, userAgent, _, mimetype, _ ->
|
||||||
|
if (!listOf("http://", "https://").any(url::startsWith)) {
|
||||||
|
webview.evaluateJavascript(
|
||||||
|
"alert('${
|
||||||
|
ResourcesUtils.getString(
|
||||||
|
context = context,
|
||||||
|
resId = R.string.core_can_only_download_http_https,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
}')"
|
||||||
|
) {}
|
||||||
|
return@setDownloadListener
|
||||||
|
}
|
||||||
|
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
||||||
|
addRequestHeader("User-Agent", userAgent)
|
||||||
|
setMimeType(mimetype)
|
||||||
|
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
}
|
||||||
|
val downloadManager: DownloadManager =
|
||||||
|
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
downloadManager.enqueue(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberChromeClient(
|
||||||
|
context: Context,
|
||||||
|
fileChooserLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||||
|
processCallback: (ValueCallback<Array<Uri>>?) -> Unit
|
||||||
|
) = remember {
|
||||||
object : AccompanistWebChromeClient() {
|
object : AccompanistWebChromeClient() {
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
consoleMessage?.let {
|
consoleMessage?.let {
|
||||||
@@ -186,7 +278,7 @@ internal fun ToolViewScreen(
|
|||||||
filePathCallback: ValueCallback<Array<Uri>>?,
|
filePathCallback: ValueCallback<Array<Uri>>?,
|
||||||
fileChooserParams: FileChooserParams?
|
fileChooserParams: FileChooserParams?
|
||||||
): Boolean {
|
): Boolean {
|
||||||
fileChooserCallback = filePathCallback
|
processCallback(filePathCallback)
|
||||||
val intent = fileChooserParams?.createIntent()
|
val intent = fileChooserParams?.createIntent()
|
||||||
?: Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
?: Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
@@ -205,40 +297,4 @@ internal fun ToolViewScreen(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onCreated = {
|
|
||||||
it.settings.javaScriptEnabled = true
|
|
||||||
it.settings.domStorageEnabled = true
|
|
||||||
it.addJavascriptInterface(
|
|
||||||
NativeWebApi(context = context, webView = it, permissionLauncher),
|
|
||||||
"NativeApi"
|
|
||||||
)
|
|
||||||
it.setDownloadListener { url, userAgent, _, mimetype, _ ->
|
|
||||||
if (!listOf("http://", "https://").any(url::startsWith)) {
|
|
||||||
it.evaluateJavascript(
|
|
||||||
"alert('${
|
|
||||||
ResourcesUtils.getString(
|
|
||||||
context = context,
|
|
||||||
resId = R.string.core_can_only_download_http_https,
|
|
||||||
url
|
|
||||||
)
|
|
||||||
}')"
|
|
||||||
) {}
|
|
||||||
return@setDownloadListener
|
|
||||||
}
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
|
||||||
addRequestHeader("User-Agent", userAgent)
|
|
||||||
setMimeType(mimetype)
|
|
||||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
||||||
}
|
|
||||||
val downloadManager: DownloadManager =
|
|
||||||
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
downloadManager.enqueue(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.view
|
package top.fatweb.oxygen.toolbox.ui.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.webkit.WebView
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -27,6 +30,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ToolViewScreenViewModel @Inject constructor(
|
class ToolViewScreenViewModel @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
storeRepository: StoreRepository,
|
storeRepository: StoreRepository,
|
||||||
toolRepository: ToolRepository,
|
toolRepository: ToolRepository,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
@@ -55,6 +59,15 @@ class ToolViewScreenViewModel @Inject constructor(
|
|||||||
initialValue = ToolViewUiState.Loading,
|
initialValue = ToolViewUiState.Loading,
|
||||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val webviewInstance = flow<WebViewInstanceState> {
|
||||||
|
val webviewInstance = WebView(context)
|
||||||
|
emit(WebViewInstanceState.Success(webviewInstance))
|
||||||
|
}.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
initialValue = WebViewInstanceState.Loading,
|
||||||
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toolViewUiState(
|
private fun toolViewUiState(
|
||||||
@@ -141,12 +154,22 @@ sealed interface ToolViewUiState {
|
|||||||
data object Loading : ToolViewUiState
|
data object Loading : ToolViewUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface WebViewInstanceState {
|
||||||
|
data class Success(
|
||||||
|
val webView: WebView
|
||||||
|
) : WebViewInstanceState
|
||||||
|
|
||||||
|
data object Loading : WebViewInstanceState
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
private fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
|
private fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
|
||||||
val dist = Base64.decodeToStringWithZip(distBase64)
|
val dist = Base64.decodeToStringWithZip(distBase64)
|
||||||
val base = Base64.decodeToStringWithZip(baseBase64)
|
val base = Base64.decodeToStringWithZip(baseBase64)
|
||||||
|
|
||||||
return toolViewTemplate.replace("{{replace_code}}", "$dist\n$base")
|
return toolViewTemplate
|
||||||
|
.replace(oldValue = "{{replace_dict_code}}", newValue = dist)
|
||||||
|
.replace(oldValue = "{{replace_base_code}}", newValue = base)
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val IS_PREVIEW = "IS_PREVIEW"
|
private const val IS_PREVIEW = "IS_PREVIEW"
|
||||||
|
|||||||
Reference in New Issue
Block a user