optimize webview

This commit is contained in:
2026-05-12 19:27:09 +03:30
parent fe17187df2
commit 9ec64fc179
15 changed files with 227 additions and 53 deletions
Generated
+1
View File
@@ -0,0 +1 @@
PSP
-4
View File
@@ -10,10 +10,6 @@
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="practice.PSP.tis">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="tis_app"> <SelectionState runConfigName="tis_app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection /> <DialogSelection />
+1
View File
@@ -43,6 +43,7 @@ android {
} }
dependencies { dependencies {
implementation(project(":design_system"))
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
+3 -1
View File
@@ -17,7 +17,9 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.PSP"> android:theme="@style/Theme.PSP"
android:hardwareAccelerated="true"
>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -0,0 +1,9 @@
package com.example.psp.di
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object AppModule
@@ -2,6 +2,7 @@ package com.example.psp.screen
import android.app.Activity import android.app.Activity
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -12,18 +13,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.example.design_system.util.snackbar.SnackbarController
import com.example.design_system.util.snackbar.SnackbarEvent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun WebViewScreen( fun WebViewScreen(
url: String, url: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
var webView: WebView? by remember { mutableStateOf(null) } var webView: WebView? by remember { mutableStateOf(null) }
val canGoBack = remember { mutableStateOf(false) } val canGoBack = remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
var lastBackTime by remember { mutableLongStateOf(0L) } var lastBackTime by remember { mutableLongStateOf(0L) }
@@ -37,7 +40,9 @@ fun WebViewScreen(
} else { } else {
lastBackTime = now lastBackTime = now
scope.launch { scope.launch {
snackbarHostState.showSnackbar("Press back again to exit") SnackbarController.sendEvent(
event = SnackbarEvent(message = "برای خروج یک بار دیگر بر روی دکمه‌ بازگشت کلیک کنید.")
)
} }
} }
} }
@@ -62,7 +67,6 @@ fun WebViewScreen(
} }
) )
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding -> ) { padding ->
AndroidView( AndroidView(
modifier = Modifier modifier = Modifier
@@ -80,13 +84,29 @@ fun WebViewScreen(
canGoBack.value = view?.canGoBack() ?: false canGoBack.value = view?.canGoBack() ?: false
} }
} }
settings.javaScriptEnabled = true
loadUrl(url) overScrollMode = android.view.View.OVER_SCROLL_NEVER
// Apply comprehensive WebView settings
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
loadsImagesAutomatically = true
allowFileAccess = true
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
webView = this webView = this
} }
}, },
update = { view -> update = {
webView = view it.loadUrl(url)
} }
) )
} }
@@ -0,0 +1,55 @@
package com.example.psp.viewmodel
import android.app.Application
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class WebViewViewModel @Inject constructor(
application: Application
) : AndroidViewModel(application) {
private var _preloadedWebView: WebView? = null
val preloadedWebView: WebView?
get() = _preloadedWebView
init {
preloadWebView()
}
private fun preloadWebView() {
viewModelScope.launch {
_preloadedWebView = WebView(getApplication()).apply {
overScrollMode = android.view.View.OVER_SCROLL_NEVER
webChromeClient = android.webkit.WebChromeClient()
// Apply the same settings as in WebViewScreen
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
loadsImagesAutomatically = true
allowFileAccess = true
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
// Load the URL to preload
loadUrl("https://tis.shift-am.ir")
}
}
}
override fun onCleared() {
super.onCleared()
_preloadedWebView?.destroy()
_preloadedWebView = null
}
}
@@ -2,13 +2,14 @@ package com.example.design_system
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -21,8 +22,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.example.core.model.PrintEntity import com.example.core.model.PrintEntity
import com.example.design_system.util.PspJavaScriptInterface import com.example.design_system.util.PspJavaScriptInterface
import com.example.design_system.util.WebViewManager
import com.example.design_system.util.snackbar.SnackbarController import com.example.design_system.util.snackbar.SnackbarController
import com.example.design_system.util.snackbar.SnackbarEvent import com.example.design_system.util.snackbar.SnackbarEvent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("JavascriptInterface") @SuppressLint("JavascriptInterface")
@@ -30,9 +33,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun PspWebView( fun PspWebView(
url: String, url: String,
webView: WebView?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onWebView: (WebView) -> Unit,
onPayClick: (amount: Long, id: String?) -> Unit, onPayClick: (amount: Long, id: String?) -> Unit,
onDeviceInfo: () -> String, onDeviceInfo: () -> String,
onPrintClick: (printEntities: List<PrintEntity>) -> Unit, onPrintClick: (printEntities: List<PrintEntity>) -> Unit,
@@ -42,7 +43,8 @@ fun PspWebView(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
var lastBackTime by remember { mutableLongStateOf(0L) } var lastBackTime by remember { mutableLongStateOf(0L) }
var isPaymentLaunching by remember { mutableStateOf(false) } var webView by remember { mutableStateOf<WebView?>(null) }
// Use rememberUpdatedState to ensure the bridge always calls the latest callbacks // Use rememberUpdatedState to ensure the bridge always calls the latest callbacks
val currentOnPayClick by rememberUpdatedState(onPayClick) val currentOnPayClick by rememberUpdatedState(onPayClick)
@@ -69,7 +71,7 @@ fun PspWebView(
scope.launch { scope.launch {
SnackbarController.sendEvent( SnackbarController.sendEvent(
SnackbarEvent( SnackbarEvent(
message = "Press back again to exit" message = "برای خروج، دوباره بر روی بازگشت کلیک کنید."
) )
) )
} }
@@ -77,42 +79,36 @@ fun PspWebView(
} }
} }
DisposableEffect(Unit) {
webView = WebViewManager.getWebView(context)
onDispose {
webView?.let { WebViewManager.releaseWebView(it) }
}
}
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { ctx -> factory = { ctx ->
WebView(ctx).apply { (webView ?: WebViewManager.getWebView(ctx)).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
) )
overScrollMode = View.OVER_SCROLL_NEVER
webViewClient = object : WebViewClient() { webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
canGoBack.value = view?.canGoBack() ?: false canGoBack.value = view?.canGoBack() ?: false
} }
} }
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
cacheMode = WebSettings.LOAD_DEFAULT
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
userAgentString =
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0 Mobile Safari/537.36"
}
addJavascriptInterface(bridge, "NativeBridge") addJavascriptInterface(bridge, "NativeBridge")
loadUrl(url) loadUrl(url)
onWebView(this)
} }
}, },
update = { view -> update = {
onWebView(view) it.loadUrl(url)
} }
) )
@@ -0,0 +1,60 @@
package com.example.design_system.util
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.webkit.WebSettings
import android.webkit.WebView
import java.util.Stack
@SuppressLint("StaticFieldLeak")
object WebViewManager {
private const val POOL_SIZE = 1
private val webViewPool = Stack<WebView>()
fun preload(context: Context) {
if (webViewPool.isEmpty()) {
repeat(POOL_SIZE) {
val webView = createWebView(context)
webViewPool.push(webView)
}
}
}
fun getWebView(context: Context): WebView {
return if (webViewPool.isNotEmpty()) {
webViewPool.pop()
} else {
createWebView(context)
}
}
fun releaseWebView(webView: WebView) {
if (webViewPool.size < POOL_SIZE) {
webViewPool.push(webView)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(context: Context): WebView {
return WebView(context).apply {
setLayerType(View.LAYER_TYPE_HARDWARE, null)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
loadsImagesAutomatically = true
allowFileAccess = false
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
userAgentString = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0 Mobile Safari/537.36"
}
}
}
}
Vendored Regular → Executable
View File
+5 -1
View File
@@ -6,7 +6,11 @@ plugins {
android { android {
namespace = "com.example.p3" namespace = "com.example.p3"
compileSdk = 36 compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig { defaultConfig {
minSdk = 24 minSdk = 24
+1
View File
@@ -50,6 +50,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("debug")
} }
} }
compileOptions { compileOptions {
+4 -1
View File
@@ -24,7 +24,10 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.PSP" android:theme="@style/Theme.PSP"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
>
<uses-library android:name="android.device" android:required="false" /> <uses-library android:name="android.device" android:required="false" />
<activity <activity
@@ -1,7 +1,13 @@
package com.example.tis_app package com.example.tis_app
import android.app.Application import android.app.Application
import com.example.design_system.util.WebViewManager
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp @HiltAndroidApp
class PspApplication : Application() class PspApplication : Application() {
override fun onCreate() {
super.onCreate()
WebViewManager.preload(this)
}
}
@@ -7,6 +7,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -15,9 +17,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.example.core.getAndroidId import com.example.core.getAndroidId
import com.example.core.getDeviceName import com.example.core.getDeviceName
import com.example.design_system.PspWebView import com.example.design_system.PspWebView
import com.example.design_system.util.WebViewManager
import com.example.tis_app.viewmodel.WebViewModel import com.example.tis_app.viewmodel.WebViewModel
private const val tisIp = "194.59.214.243:8090" private const val tisWebUrl = "https://tis.shift-am.ir"
@Composable @Composable
fun TisWebViewScreen( fun TisWebViewScreen(
@@ -26,8 +29,19 @@ fun TisWebViewScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
var webView: WebView? by remember { mutableStateOf(null) } var webView: WebView by remember { mutableStateOf(WebViewManager.getWebView(context)) }
var currentPaymentId by rememberSaveable { mutableStateOf<String?>(null) } var currentPaymentId by rememberSaveable { mutableStateOf<String?>(null) }
var isPaymentProcessing by rememberSaveable { mutableStateOf(false) }
var isPrinting by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val deviceInfo by remember {
mutableStateOf(
viewModel.deviceInfoJson(
androidId = getAndroidId(context),
getDeviceName()
)
)
}
// launcher to handle POS app result // launcher to handle POS app result
val posLauncher = rememberLauncherForActivityResult( val posLauncher = rememberLauncherForActivityResult(
@@ -38,34 +52,40 @@ fun TisWebViewScreen(
activityResult = result activityResult = result
) )
webView?.post {
webView?.evaluateJavascript( webView.post {
webView.evaluateJavascript(
viewModel.postPaymentResult(paymentResultEntity = paymentResultEntity), viewModel.postPaymentResult(paymentResultEntity = paymentResultEntity),
null null
) )
} }
isPaymentProcessing = false
} }
PspWebView( PspWebView(
url = tisIp, url = tisWebUrl,
webView = webView,
modifier = modifier, modifier = modifier,
onWebView = { updatedWebView -> webView = updatedWebView },
onPayClick = { amount: Long, id: String? -> onPayClick = { amount: Long, id: String? ->
if (isPaymentProcessing) return@PspWebView
isPaymentProcessing = true
currentPaymentId = id currentPaymentId = id
viewModel.pay(amount) viewModel.pay(amount)
.also(posLauncher::launch) .also(posLauncher::launch)
}, },
onPrintClick = { printEntities -> onPrintClick = { printEntities ->
viewModel.print(printEntities) if (isPrinting) return@PspWebView
isPrinting = true
coroutineScope.launch {
try {
viewModel.print(printEntities)
} finally {
isPrinting = false
}
}
}, },
onDeviceInfo = { onDeviceInfo = {
viewModel.deviceInfoJson( deviceInfo
androidId = getAndroidId(context),
getDeviceName()
)
} }
) )
} }