Coverage Summary for Class: WebViewInAppViewHolder (cloud.mindbox.mobile_sdk.inapp.presentation.view)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| WebViewInAppViewHolder |
0%
(0/55)
|
0%
(0/168)
|
0%
(0/331)
|
0%
(0/2175)
|
| WebViewInAppViewHolder$appContext$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$bindWebViewBackAction$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/5)
|
| WebViewInAppViewHolder$Companion |
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$10 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$11 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$14 |
0%
(0/1)
|
|
0%
(0/5)
|
0%
(0/18)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$15 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/5)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$16 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/5)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$19 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| WebViewInAppViewHolder$createWebViewActionHandlers$1$9 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| WebViewInAppViewHolder$createWebViewController$1 |
0%
(0/4)
|
0%
(0/6)
|
0%
(0/12)
|
0%
(0/82)
|
| WebViewInAppViewHolder$createWebViewController$1$onPageFinished$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| WebViewInAppViewHolder$ErrorPayload |
0%
(0/2)
|
|
0%
(0/3)
|
0%
(0/7)
|
| WebViewInAppViewHolder$gatewayManager$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$getOrCreateMotionService$1$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/5)
|
| WebViewInAppViewHolder$gson$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$handleClickAction$lambda$8$$inlined$fromJson$1 |
0%
(0/1)
|
|
| WebViewInAppViewHolder$handleMotionStartAction$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| WebViewInAppViewHolder$handlePermissionAction$1 |
|
| WebViewInAppViewHolder$handleRequest$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/4)
|
0%
(0/48)
|
| WebViewInAppViewHolder$handleSettingsOpenAction$$inlined$fromJson$1 |
0%
(0/1)
|
|
| WebViewInAppViewHolder$hapticFeedbackExecutor$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/7)
|
| WebViewInAppViewHolder$hapticRequestValidator$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| WebViewInAppViewHolder$linkRouter$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/7)
|
| WebViewInAppViewHolder$localStateStore$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/7)
|
| WebViewInAppViewHolder$messageValidator$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| WebViewInAppViewHolder$mindboxNotificationManager$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$MotionStartPayload |
0%
(0/1)
|
|
0%
(0/4)
|
0%
(0/14)
|
| WebViewInAppViewHolder$NavigationInterceptedPayload |
0%
(0/2)
|
|
0%
(0/3)
|
0%
(0/7)
|
| WebViewInAppViewHolder$onContentPageLoaded$1$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/5)
|
| WebViewInAppViewHolder$onContentPageLoaded$1$2 |
0%
(0/1)
|
|
0%
(0/5)
|
0%
(0/27)
|
| WebViewInAppViewHolder$onContentPageLoaded$1$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/4)
|
| WebViewInAppViewHolder$operationExecutor$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| WebViewInAppViewHolder$parseMotionGestures$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/5)
|
0%
(0/28)
|
| WebViewInAppViewHolder$permissionManager$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$renderLayer$1 |
0%
(0/2)
|
0%
(0/12)
|
0%
(0/33)
|
0%
(0/235)
|
| WebViewInAppViewHolder$renderLayer$1$1$1 |
0%
(0/1)
|
0%
(0/6)
|
0%
(0/5)
|
0%
(0/46)
|
| WebViewInAppViewHolder$renderLayer$1$2$3$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/4)
|
| WebViewInAppViewHolder$renderLayer$1$3$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/4)
|
| WebViewInAppViewHolder$renderLayer$1$invokeSuspend$lambda$0$$inlined$fromJson$1 |
0%
(0/1)
|
|
| WebViewInAppViewHolder$sendActionAndAwaitResponse$2 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/3)
|
0%
(0/17)
|
| WebViewInAppViewHolder$sendActionInternal$1 |
0%
(0/1)
|
0%
(0/4)
|
0%
(0/2)
|
0%
(0/14)
|
| WebViewInAppViewHolder$sendBackAction$1 |
0%
(0/1)
|
|
0%
(0/3)
|
0%
(0/22)
|
| WebViewInAppViewHolder$sendMotionEvent$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/2)
|
0%
(0/16)
|
| WebViewInAppViewHolder$sendNavigationInterceptedEvent$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/8)
|
| WebViewInAppViewHolder$sessionStorageManager$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$SettingsOpenRequest |
0%
(0/3)
|
|
0%
(0/5)
|
0%
(0/12)
|
| WebViewInAppViewHolder$SettingsOpenTargetType |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/18)
|
| WebViewInAppViewHolder$startTimer$$inlined$timer$default$1 |
0%
(0/2)
|
|
| WebViewInAppViewHolder$timeProvider$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| WebViewInAppViewHolder$webViewPermissionRequester$2 |
0%
(0/1)
|
|
0%
(0/3)
|
0%
(0/14)
|
| WebViewInAppViewHolder$WhenMappings |
|
| Total |
0%
(0/113)
|
0%
(0/204)
|
0%
(0/458)
|
0%
(0/2919)
|
package cloud.mindbox.mobile_sdk.inapp.presentation.view
import android.app.Activity
import android.app.Application
import android.net.Uri
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import cloud.mindbox.mobile_sdk.*
import cloud.mindbox.mobile_sdk.annotations.InternalMindboxApi
import cloud.mindbox.mobile_sdk.di.mindboxInject
import cloud.mindbox.mobile_sdk.inapp.data.dto.BackgroundDto
import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager
import cloud.mindbox.mobile_sdk.inapp.data.validators.BridgeMessageValidator
import cloud.mindbox.mobile_sdk.inapp.data.validators.HapticRequestValidator
import cloud.mindbox.mobile_sdk.inapp.domain.extensions.executeWithFailureTracking
import cloud.mindbox.mobile_sdk.inapp.domain.extensions.sendFailureWithContext
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.PermissionManager
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppTypeWrapper
import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer
import cloud.mindbox.mobile_sdk.inapp.presentation.InAppCallback
import cloud.mindbox.mobile_sdk.inapp.presentation.MindboxNotificationManager
import cloud.mindbox.mobile_sdk.inapp.presentation.MindboxView
import androidx.lifecycle.ProcessLifecycleOwner
import cloud.mindbox.mobile_sdk.utils.TimeProvider
import cloud.mindbox.mobile_sdk.inapp.presentation.view.motion.MotionGesture
import cloud.mindbox.mobile_sdk.inapp.presentation.view.motion.MotionService
import cloud.mindbox.mobile_sdk.inapp.presentation.view.motion.MotionServiceProtocol
import cloud.mindbox.mobile_sdk.inapp.presentation.view.motion.MotionStartResult
import cloud.mindbox.mobile_sdk.inapp.webview.*
import cloud.mindbox.mobile_sdk.logger.mindboxLogD
import cloud.mindbox.mobile_sdk.logger.mindboxLogE
import cloud.mindbox.mobile_sdk.logger.mindboxLogI
import cloud.mindbox.mobile_sdk.logger.mindboxLogW
import cloud.mindbox.mobile_sdk.managers.DbManager
import cloud.mindbox.mobile_sdk.managers.GatewayManager
import cloud.mindbox.mobile_sdk.models.Configuration
import cloud.mindbox.mobile_sdk.models.getShortUserAgent
import cloud.mindbox.mobile_sdk.models.operation.request.FailureReason
import cloud.mindbox.mobile_sdk.utils.MindboxUtils.Stopwatch
import cloud.mindbox.mobile_sdk.utils.loggingRunCatching
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.util.Locale
import java.util.Timer
import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.timer
@OptIn(InternalMindboxApi::class)
internal class WebViewInAppViewHolder(
wrapper: InAppTypeWrapper<InAppType.WebView>,
controller: InAppViewHolder.InAppController,
inAppCallback: InAppCallback,
) : AbstractInAppViewHolder<InAppType.WebView>(wrapper, controller, inAppCallback) {
companion object {
private const val INIT_TIMEOUT_MS = 7_000L
private const val TIMER = "CLOSE_INAPP_TIMER"
private const val JS_RETURN = "true"
private const val JS_BRIDGE_CLASS = "window.bridgeMessagesHandlers"
private const val JS_BRIDGE = "$JS_BRIDGE_CLASS.emit"
private const val JS_CALL_BRIDGE = "(()=>{try{$JS_BRIDGE(%s);return!0}catch(_){return!1}})()"
private const val JS_CHECK_BRIDGE = "(() => typeof $JS_BRIDGE_CLASS !== 'undefined' && typeof $JS_BRIDGE === 'function')()"
private const val MOTION_GESTURE_KEY = "gesture"
private const val MOTION_GESTURES_KEY = "gestures"
}
private var closeInappTimer: Timer? = null
private var webViewController: WebViewController? = null
private var currentWebViewOrigin: String? = null
private var motionService: MotionServiceProtocol? = null
private fun bindWebViewBackAction(currentRoot: MindboxView, controller: WebViewController) {
bindBackAction(currentRoot) { sendBackAction(controller) }
}
private val pendingResponsesById: MutableMap<String, CompletableDeferred<BridgeMessage.Response>> =
ConcurrentHashMap()
private val gson: Gson by mindboxInject { this.gson }
private val timeProvider: TimeProvider by mindboxInject { timeProvider }
private val messageValidator: BridgeMessageValidator by lazy { BridgeMessageValidator() }
private val hapticRequestValidator: HapticRequestValidator by lazy { HapticRequestValidator() }
private val gatewayManager: GatewayManager by mindboxInject { gatewayManager }
private val sessionStorageManager: SessionStorageManager by mindboxInject { sessionStorageManager }
private val permissionManager: PermissionManager by mindboxInject { permissionManager }
private val mindboxNotificationManager: MindboxNotificationManager by mindboxInject { mindboxNotificationManager }
private val appContext: Application by mindboxInject { appContext }
private val operationExecutor: WebViewOperationExecutor by lazy {
MindboxWebViewOperationExecutor()
}
private val linkRouter: WebViewLinkRouter by lazy {
MindboxWebViewLinkRouter(appContext)
}
private val localStateStore: WebViewLocalStateStore by lazy {
WebViewLocalStateStore(appContext)
}
private val hapticFeedbackExecutor: HapticFeedbackExecutor by lazy {
HapticFeedbackExecutorImpl(appContext)
}
private val webViewPermissionRequester: WebViewPermissionRequester by lazy {
WebViewPermissionRequesterImpl(
context = appContext,
permissionManager = permissionManager
)
}
private var currentMindboxView: MindboxView? = null
override fun onBeforeShow(currentRoot: MindboxView) = Unit
override fun bind() {}
suspend fun sendActionAndAwaitResponse(
controller: WebViewController,
message: BridgeMessage.Request
): BridgeMessage.Response {
val responseDeferred: CompletableDeferred<BridgeMessage.Response> = CompletableDeferred()
pendingResponsesById[message.id] = responseDeferred
sendActionInternal(controller = controller, message = message) { error ->
if (responseDeferred.isActive) {
responseDeferred.completeExceptionally(
IllegalStateException("Failed to send message ${message.action} to WebView: $error")
)
}
}
return responseDeferred.await()
}
private fun sendActionInternal(
controller: WebViewController,
message: BridgeMessage,
onError: ((String?) -> Unit)? = null
) {
mindboxLogI("SDK -> send message $message")
val json: String = gson.toJson(message)
val escapedJson: String = JSONObject.quote(json)
controller.evaluateJavaScript(JS_CALL_BRIDGE.format(escapedJson)) { result ->
if (!checkEvaluateJavaScript(result)) {
onError?.invoke(result)
}
}
}
private fun createWebViewActionHandlers(
controller: WebViewController,
layer: Layer.WebViewLayer,
configuration: Configuration
): WebViewActionHandlers {
return WebViewActionHandlers().apply {
register(WebViewAction.CLICK, ::handleClickAction)
register(WebViewAction.CLOSE, ::handleCloseAction)
register(WebViewAction.LOG, ::handleLogAction)
register(WebViewAction.TOAST, ::handleToastAction)
register(WebViewAction.ALERT, ::handleAlertAction)
register(WebViewAction.ASYNC_OPERATION, ::handleAsyncOperationAction)
register(WebViewAction.OPEN_LINK, ::handleOpenLinkAction)
registerSuspend(WebViewAction.SYNC_OPERATION, ::handleSyncOperationAction)
registerSuspend(WebViewAction.LOCAL_STATE_GET, ::handleLocalStateGetAction)
registerSuspend(WebViewAction.LOCAL_STATE_SET, ::handleLocalStateSetAction)
registerSuspend(WebViewAction.LOCAL_STATE_INIT, ::handleLocalStateInitAction)
registerSuspend(WebViewAction.PERMISSION_REQUEST, ::handlePermissionAction)
register(WebViewAction.SETTINGS_OPEN, ::handleSettingsOpenAction)
register(WebViewAction.READY) {
handleReadyAction(
configuration = configuration,
insets = inAppLayout.webViewInsets,
params = layer.params,
inAppId = wrapper.inAppType.inAppId,
)
}
register(WebViewAction.INIT) {
handleInitAction(controller)
}
register(WebViewAction.HIDE) {
handleHideAction(controller)
}
register(WebViewAction.HAPTIC, ::handleHapticAction)
register(WebViewAction.MOTION_START, ::handleMotionStartAction)
register(WebViewAction.MOTION_STOP) { handleMotionStopAction() }
}
}
private fun handleHapticAction(message: BridgeMessage.Request): String {
val request = parseHapticRequest(message.payload)
if (!hapticRequestValidator.isValid(request)) return BridgeMessage.EMPTY_PAYLOAD
hapticFeedbackExecutor.execute(request = request)
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleMotionStartAction(message: BridgeMessage.Request): String {
val payload = requireNotNull(message.payload) { "Missing payload" }
val gestures = parseMotionGestures(payload)
require(gestures.isNotEmpty()) { "No valid gestures provided. Available: shake, flip" }
val result = getOrCreateMotionService().startMonitoring(gestures)
require(!result.allUnavailable) {
"No sensors available for: ${result.unavailable.joinToString { it.value }}"
}
return buildMotionStartPayload(result)
}
private fun buildMotionStartPayload(result: MotionStartResult): String {
if (result.unavailable.isEmpty()) return BridgeMessage.SUCCESS_PAYLOAD
return gson.toJson(
MotionStartPayload(unavailable = result.unavailable.map { it.value })
)
}
private fun handleMotionStopAction(): String {
motionService?.stopMonitoring()
return BridgeMessage.SUCCESS_PAYLOAD
}
private fun sendMotionEvent(gesture: MotionGesture, data: Map<String, String>) {
val controller: WebViewController = webViewController ?: return
val payload = JSONObject()
.apply {
put(MOTION_GESTURE_KEY, gesture.value)
data.forEach { (key, value) -> put(key, value) }
}
.toString()
val message: BridgeMessage.Request = BridgeMessage.createAction(
action = WebViewAction.MOTION_EVENT,
payload = payload,
)
sendActionInternal(controller, message) { error ->
mindboxLogW("[WebView] Motion: failed to send motion.event to JS: $error")
motionService?.stopMonitoring()
}
}
private fun parseMotionGestures(payload: String): Set<MotionGesture> {
return loggingRunCatching(defaultValue = emptySet()) {
val array = JSONObject(payload).optJSONArray(MOTION_GESTURES_KEY)
?: return@loggingRunCatching emptySet()
(0 until array.length())
.mapNotNull { i -> array.optString(i).enumValue<MotionGesture>() }
.toSet()
}
}
private fun handleReadyAction(
configuration: Configuration,
insets: InAppInsets,
params: Map<String, String>,
inAppId: String,
): String {
return DataCollector(
appContext = appContext,
sessionStorageManager = sessionStorageManager,
permissionManager = permissionManager,
gson = gson,
configuration = configuration,
params = params,
inAppInsets = insets,
inAppId = inAppId,
).get()
}
private fun activateFirstShowPresentation(
mindboxView: MindboxView,
controller: WebViewController,
) {
hideKeyboard(inAppLayout)
inAppLayout.requestFocus()
bindWebViewBackAction(mindboxView, controller)
controller.setVisibility(true)
}
private fun handleInitAction(controller: WebViewController): String {
stopTimer()
wrapper.inAppActionCallbacks.onInAppShown.onShown()
val mindboxView = currentMindboxView ?: run {
mindboxLogW("MindboxView is null when activating WebView In-App")
inAppController.close()
return BridgeMessage.UNKNOWN_ERROR_PAYLOAD
}
activateFirstShowPresentation(
mindboxView = mindboxView,
controller = controller,
)
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleClickAction(message: BridgeMessage.Request): String {
runCatching {
val actionDto: BackgroundDto.LayerDto.ImageLayerDto.ActionDto =
gson.fromJson<BackgroundDto.LayerDto.ImageLayerDto.ActionDto>(message.payload).getOrThrow()
val actionResult: Pair<String?, String?> = when (actionDto) {
is BackgroundDto.LayerDto.ImageLayerDto.ActionDto.RedirectUrlActionDto ->
actionDto.value to actionDto.intentPayload
is BackgroundDto.LayerDto.ImageLayerDto.ActionDto.PushPermissionActionDto ->
"" to actionDto.intentPayload
}
val url: String? = actionResult.first
val payload: String? = actionResult.second
inAppCallback.onInAppClick(
wrapper.inAppType.inAppId,
url ?: "",
payload ?: ""
)
}
mindboxLogI("In-app completed by webview action with data: ${message.payload}")
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleCloseAction(message: BridgeMessage): String {
motionService?.stopMonitoring()
inAppCallback.onInAppDismissed(wrapper.inAppType.inAppId)
mindboxLogI("In-app dismissed by webview action ${message.action} with payload ${message.payload}")
inAppController.close()
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleHideAction(controller: WebViewController): String {
controller.setVisibility(false)
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleLogAction(message: BridgeMessage.Request): String {
mindboxLogI("JS: ${message.payload}")
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleToastAction(message: BridgeMessage.Request): String {
webViewController?.view?.context?.let { context ->
Toast.makeText(context, message.payload, Toast.LENGTH_LONG).show()
}
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleAlertAction(message: BridgeMessage.Request): String {
webViewController?.view?.context?.let { context ->
AlertDialog.Builder(context)
.setMessage(message.payload)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.show()
}
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleAsyncOperationAction(message: BridgeMessage.Request): String {
operationExecutor.executeAsyncOperation(appContext, message.payload)
return BridgeMessage.EMPTY_PAYLOAD
}
private fun handleOpenLinkAction(message: BridgeMessage.Request): String {
linkRouter.executeOpenLink(message.payload)
.getOrElse { error: Throwable ->
throw IllegalStateException(error.message ?: "Navigation error")
}
return BridgeMessage.SUCCESS_PAYLOAD
}
private suspend fun handleSyncOperationAction(message: BridgeMessage.Request): String {
return operationExecutor.executeSyncOperation(message.payload)
}
private fun handleLocalStateGetAction(message: BridgeMessage.Request): String {
val payload: String = message.payload ?: BridgeMessage.EMPTY_PAYLOAD
return localStateStore.getState(payload)
}
private fun handleLocalStateSetAction(message: BridgeMessage.Request): String {
val payload: String = message.payload ?: BridgeMessage.EMPTY_PAYLOAD
return localStateStore.setState(payload)
}
private fun handleLocalStateInitAction(message: BridgeMessage.Request): String {
val payload: String = message.payload ?: BridgeMessage.EMPTY_PAYLOAD
return localStateStore.initState(payload)
}
private suspend fun handlePermissionAction(message: BridgeMessage.Request): String {
val payload: String = message.payload ?: BridgeMessage.EMPTY_PAYLOAD
val typeString: String? = JSONObject(payload).getString(PERMISSION_PAYLOAD_TYPE_FIELD_NAME)
val type: PermissionType? = runCatching { typeString.enumValue<PermissionType>() }.getOrNull()
requireNotNull(type) { "Unknown permission type: $typeString" }
val activity: Activity? = webViewController?.view?.context?.safeAs<Activity>()
checkNotNull(activity) { "Not found activity for permission request" }
val permissionRequestResult: PermissionActionResponse = webViewPermissionRequester.requestPermission(
activity,
type
)
return gson.toJson(permissionRequestResult)
}
private fun handleSettingsOpenAction(message: BridgeMessage.Request): String {
val payload: String = message.payload ?: BridgeMessage.EMPTY_PAYLOAD
val settingsOpenRequest: SettingsOpenRequest? = gson.fromJson<SettingsOpenRequest>(payload).getOrNull()
requireNotNull(settingsOpenRequest)
val targetType = settingsOpenRequest.target.enumValue<SettingsOpenTargetType>()
val activity: Activity? = webViewController?.view?.context?.safeAs<Activity>()
checkNotNull(activity) { "Not found activity for open settings" }
when (targetType) {
SettingsOpenTargetType.NOTIFICATIONS -> mindboxNotificationManager.openNotificationSettings(activity, settingsOpenRequest.channelId)
SettingsOpenTargetType.APPLICATION -> mindboxNotificationManager.openApplicationSettings(activity)
}
return BridgeMessage.SUCCESS_PAYLOAD
}
private fun createWebViewController(layer: Layer.WebViewLayer): WebViewController {
mindboxLogI("Creating WebView for In-App: ${wrapper.inAppType.inAppId} with layer ${layer.type}")
val controller: WebViewController = WebViewController.create(currentDialog.context, BuildConfig.DEBUG)
val view: WebViewPlatformView = controller.view
view.layoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
controller.setEventListener(object : WebViewEventListener {
override fun onPageFinished(url: String?) {
mindboxLogD("onPageFinished: $url")
currentWebViewOrigin = resolveOrigin(url) ?: currentWebViewOrigin
webViewController?.evaluateJavaScript(JS_CHECK_BRIDGE, ::checkEvaluateJavaScript)
}
override fun onShouldOverrideUrlLoading(url: String?, isForMainFrame: Boolean?): Boolean {
return handleShouldOverrideUrlLoading(url = url, isForMainFrame = isForMainFrame)
}
override fun onError(error: WebViewError) {
mindboxLogE("WebView error: code=${error.code}, description=${error.description}, url=${error.url}")
if (error.isForMainFrame == true) {
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_PRESENTATION_FAILED,
errorDescription = "WebView error: code=${error.code}, description=${error.description}, url=${error.url}"
)
inAppController.close()
}
}
})
return controller
}
private fun handleShouldOverrideUrlLoading(url: String?, isForMainFrame: Boolean?): Boolean {
if (isForMainFrame != true) {
return false
}
if (shouldAllowLocalNavigation(url)) {
return false
}
val normalizedUrl: String = url?.trim().orEmpty()
sendNavigationInterceptedEvent(url = normalizedUrl)
return true
}
private fun sendNavigationInterceptedEvent(url: String) {
val controller: WebViewController = webViewController ?: return
val payload: String = gson.toJson(NavigationInterceptedPayload(url = url))
val message: BridgeMessage.Request = BridgeMessage.createAction(
action = WebViewAction.NAVIGATION_INTERCEPTED,
payload = payload
)
sendActionInternal(controller, message) { error ->
mindboxLogW("Failed to send navigationIntercepted event to WebView: $error")
}
}
private fun shouldAllowLocalNavigation(url: String?): Boolean {
if (url.isNullOrBlank()) {
return true
}
val normalizedUrl: String = url.trim()
if (normalizedUrl.startsWith("#")) {
return true
}
if (normalizedUrl.startsWith("about:blank")) {
return true
}
val targetOrigin: String = resolveOrigin(normalizedUrl) ?: return false
val sourceOrigin: String = currentWebViewOrigin ?: return false
return targetOrigin == sourceOrigin
}
private fun resolveOrigin(url: String?): String? {
if (url.isNullOrBlank()) {
return null
}
val parsedUri: Uri = runCatching { url.toUri() }.getOrNull() ?: return null
val scheme: String = parsedUri.scheme?.lowercase(Locale.US).orEmpty()
val host: String = parsedUri.host?.lowercase(Locale.US).orEmpty()
if (scheme.isBlank() || host.isBlank()) {
return null
}
val normalizedPort: String = if (parsedUri.port >= 0) ":${parsedUri.port}" else ""
return "$scheme://$host$normalizedPort"
}
private fun sendBackAction(controller: WebViewController) {
val message: BridgeMessage.Request = BridgeMessage.createAction(
WebViewAction.BACK,
BridgeMessage.EMPTY_PAYLOAD
)
sendActionInternal(controller, message) { error ->
mindboxLogW("Failed to send back action to WebView: $error")
inAppCallback.onInAppDismissed(wrapper.inAppType.inAppId)
inAppController.close()
}
}
internal fun checkEvaluateJavaScript(response: String?): Boolean {
return when (response) {
JS_RETURN -> true
else -> {
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_PRESENTATION_FAILED,
errorDescription = "evaluateJavaScript return unexpected response: $response"
)
inAppController.close()
false
}
}
}
private fun handleRequest(message: BridgeMessage.Request, controller: WebViewController, handlers: WebViewActionHandlers) {
if (handlers.hasSuspendHandler(message.action)) {
Mindbox.mindboxScope.launch {
val responsePayload: String = handlers.handleRequestSuspend(message)
.getOrElse { error ->
sendErrorResponse(message = message, error = error, controller = controller)
return@launch
}
sendSuccessResponse(message = message, responsePayload = responsePayload, controller = controller)
}
return
}
val responsePayload: String = handlers.handleRequest(message)
.getOrElse { error ->
sendErrorResponse(message = message, error = error, controller = controller)
return
}
sendSuccessResponse(message = message, responsePayload = responsePayload, controller = controller)
}
private fun sendSuccessResponse(
message: BridgeMessage.Request,
responsePayload: String?,
controller: WebViewController,
) {
val responseMessage: BridgeMessage.Response = BridgeMessage.createResponseAction(message, responsePayload)
sendActionInternal(controller, responseMessage)
}
private fun sendErrorResponse(
message: BridgeMessage.Request,
error: Throwable,
controller: WebViewController,
) {
val json: String = runCatching {
val payload = ErrorPayload(error = requireNotNull(error.message))
gson.toJson(payload)
}.getOrDefault(BridgeMessage.UNKNOWN_ERROR_PAYLOAD)
val errorMessage: BridgeMessage.Error = BridgeMessage.createErrorAction(message, json)
mindboxLogE("WebView send error response for ${message.action} with payload ${errorMessage.payload}")
sendActionInternal(controller, errorMessage)
}
private fun handleResponse(message: BridgeMessage.Response) {
val responseDeferred: CompletableDeferred<BridgeMessage.Response>? = pendingResponsesById.remove(message.id)
if (responseDeferred == null) {
mindboxLogW("No pending response for id $message.id")
return
}
if (!responseDeferred.isCompleted) {
responseDeferred.complete(message)
}
}
private fun handleError(message: BridgeMessage.Error) {
mindboxLogW("WebView error: ${message.payload}")
val responseDeferred: CompletableDeferred<BridgeMessage.Response>? = pendingResponsesById.remove(message.id)
responseDeferred?.cancel("WebView error: ${message.payload}")
inAppController.close()
}
private fun cancelPendingResponses(reason: String) {
val error: CancellationException = CancellationException(reason)
pendingResponsesById.values.forEach { deferred ->
if (!deferred.isCompleted) {
deferred.cancel(error)
}
}
pendingResponsesById.clear()
}
private fun renderLayer(layer: Layer.WebViewLayer) {
if (webViewController == null) {
val controller: WebViewController = createWebViewController(layer)
webViewController = controller
Mindbox.mindboxScope.launch {
val configuration: Configuration = DbManager.listenConfigurations().first()
val handlers: WebViewActionHandlers = createWebViewActionHandlers(controller, layer, configuration)
controller.setVisibility(false)
controller.setJsBridge(bridge = { json ->
mindboxLogI("SDK <- receive message $json")
val message = gson.fromJson<BridgeMessage>(json).getOrNull()
if (!messageValidator.isValid(message)) {
return@setJsBridge
}
controller.executeOnViewThread {
when (message) {
is BridgeMessage.Request -> handleRequest(message, controller, handlers)
is BridgeMessage.Response -> handleResponse(message)
is BridgeMessage.Error -> handleError(message)
else -> mindboxLogW("Unknown message type: $message")
}
}
})
controller.setUserAgentSuffix(configuration.getShortUserAgent())
layer.contentUrl?.let { contentUrl ->
runCatching {
gatewayManager.fetchWebViewContent(contentUrl)
}.onSuccess { response: String ->
currentWebViewOrigin = resolveOrigin(layer.baseUrl)
onContentPageLoaded(
content = WebViewHtmlContent(
baseUrl = layer.baseUrl ?: "",
html = response
)
)
}.onFailure { e ->
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_LOAD_FAILED,
errorDescription = "Failed to fetch HTML content for In-App",
throwable = e
)
controller.executeOnViewThread { inAppController.close() }
}
} ?: run {
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_LOAD_FAILED,
errorDescription = "WebView content URL is null"
)
controller.executeOnViewThread { inAppController.close() }
}
}
}
webViewController?.let { controller ->
inAppFailureTracker.executeWithFailureTracking(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.PRESENTATION_FAILED,
errorDescription = "Error when trying WebView layout",
) {
val view: WebViewPlatformView = controller.view
if (view.parent !== inAppLayout) {
view.parent.safeAs<ViewGroup>()?.removeView(view)
inAppLayout.addView(view)
}
}
} ?: run {
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_PRESENTATION_FAILED,
errorDescription = "WebView controller is null when trying show inapp"
)
inAppController.close()
}
}
private fun onContentPageLoaded(content: WebViewHtmlContent) {
webViewController?.let { controller ->
controller.executeOnViewThread {
controller.loadContent(content)
}
startTimer {
inAppFailureTracker.sendFailureWithContext(
inAppId = wrapper.inAppType.inAppId,
failureReason = FailureReason.WEBVIEW_LOAD_FAILED,
errorDescription = "WebView initialization timed out after ${Stopwatch.stop(TIMER)}."
)
controller.executeOnViewThread {
inAppController.close()
}
}
} ?: run {
mindboxLogW("WebView controller is null when loading content, skipping")
}
}
private fun stopTimer() {
closeInappTimer?.let { timer ->
mindboxLogI("WebView initialization completed " + Stopwatch.stop(TIMER))
timer.cancel()
}
closeInappTimer = null
}
private fun startTimer(onTimeOut: () -> Unit) {
Stopwatch.start(TIMER)
closeInappTimer = timer(
initialDelay = INIT_TIMEOUT_MS,
period = INIT_TIMEOUT_MS,
action = { onTimeOut() }
)
}
override fun show(currentRoot: MindboxView) {
currentMindboxView = currentRoot
super.show(currentRoot)
mindboxLogI("Try to show in-app with id ${wrapper.inAppType.inAppId}")
wrapper.inAppType.layers.forEach { layer ->
when (layer) {
is Layer.WebViewLayer -> renderLayer(layer)
else -> mindboxLogW("Layer is not supported")
}
}
mindboxLogI("Show In-App ${wrapper.inAppType.inAppId} in holder ${this.hashCode()}")
}
override fun reattach(currentRoot: MindboxView) {
currentMindboxView = currentRoot
super.reattach(currentRoot)
wrapper.inAppType.layers.forEach { layer ->
when (layer) {
is Layer.WebViewLayer -> renderLayer(layer)
else -> mindboxLogW("Layer is not supported")
}
}
inAppLayout.requestFocus()
webViewController?.let { controller -> bindWebViewBackAction(currentRoot, controller) }
}
override fun canReuseOnRestore(inAppId: String): Boolean = wrapper.inAppType.inAppId == inAppId
override fun onStop() {
// do nothing
}
override fun onClose() {
hapticFeedbackExecutor.cancel()
motionService?.stopMonitoring()
stopTimer()
cancelPendingResponses("WebView In-App is closed")
webViewController?.let { controller ->
val view: WebViewPlatformView = controller.view
view.parent.safeAs<ViewGroup>()?.removeView(view)
controller.destroy()
}
currentWebViewOrigin = null
webViewController?.destroy()
webViewController = null
currentMindboxView = null
super.onClose()
}
private fun getOrCreateMotionService(): MotionServiceProtocol =
motionService ?: MotionService(
context = appContext,
lifecycle = ProcessLifecycleOwner.get().lifecycle,
timeProvider = timeProvider,
).also { service ->
service.onGestureDetected = { gesture, data ->
sendMotionEvent(gesture = gesture, data = data)
}
motionService = service
}
private data class NavigationInterceptedPayload(
@SerializedName("url")
val url: String
)
private data class ErrorPayload(
@SerializedName("error")
val error: String
)
private data class MotionStartPayload(
@SerializedName("success")
val success: Boolean = true,
@SerializedName("unavailable")
val unavailable: List<String>? = null,
)
private data class SettingsOpenRequest(
@SerializedName("target")
val target: String,
@SerializedName("channelId")
val channelId: String?
)
private enum class SettingsOpenTargetType {
NOTIFICATIONS,
APPLICATION
}
}