From f9a3b3c6d0e6554d84b27e22077452e1246aedb6 Mon Sep 17 00:00:00 2001 From: Chace Daniels Date: Mon, 19 Jan 2026 14:55:08 -0600 Subject: [PATCH 1/3] fix: separate local storage between WebView processes --- src/main/AndroidManifest.xml | 1 + .../osinappbrowserlib/OSIABEvents.kt | 61 ++++++++++++++++++- .../helpers/OSIABCustomTabsSessionHelper.kt | 3 + .../helpers/OSIABFlowHelper.kt | 5 ++ .../helpers/OSIABFlowHelperInterface.kt | 5 ++ .../OSIABCustomTabsRouterAdapter.kt | 5 ++ .../OSIABWebViewRouterAdapter.kt | 3 + .../views/OSIABWebViewActivity.kt | 12 +++- 8 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 4def0b9..bf36993 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ () + private val _events = MutableSharedFlow(extraBufferCapacity = 64) val events = _events.asSharedFlow() + private var receiver: BroadcastReceiver? = null + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + fun registerReceiver(context: Context) { + if (receiver != null) return + + val appContext = context.applicationContext + receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_IAB_EVENT) { + @Suppress("DEPRECATION") + val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(EXTRA_EVENT_DATA, OSIABEvents::class.java) + } else { + intent.getSerializableExtra(EXTRA_EVENT_DATA) as? OSIABEvents + } + event?.let { + _events.tryEmit(it) + } + } + } + } + + val filter = IntentFilter(ACTION_IAB_EVENT) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + appContext.registerReceiver(receiver, filter) + } + } + suspend fun postEvent(event: OSIABEvents) { _events.emit(event) } + + fun broadcastEvent(context: Context, event: OSIABEvents) { + val intent = Intent(ACTION_IAB_EVENT).apply { + setPackage(context.packageName) + putExtra(EXTRA_EVENT_DATA, event) + } + context.sendBroadcast(intent) + } } } diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt index a26ca53..a5e5862 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt @@ -11,6 +11,7 @@ import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABCustomTabsControllerActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -35,6 +36,7 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface { flowHelper: OSIABFlowHelperInterface, customTabsSessionCallback: (CustomTabsSession?) -> Unit ) { + OSIABEvents.registerReceiver(context) CustomTabsClient.bindCustomTabsService( context, packageName, @@ -54,6 +56,7 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface { ) } + @OptIn(RequiresEventBridgeRegistration::class) private inner class CustomTabsCallbackImpl( private val browserId: String, private val lifecycleScope: CoroutineScope, diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelper.kt index 3ae711b..f312132 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelper.kt @@ -1,6 +1,7 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.transformWhile @@ -14,7 +15,11 @@ class OSIABFlowHelper: OSIABFlowHelperInterface { * @param browserId Identifier for the browser instance to emit events to * @param scope CoroutineScope to launch * @param onEventReceived callback to send the collected event in + * + * @note For Android API 28+, you must call [OSIABEvents.registerReceiver] once during your application + * or activity lifecycle to ensure events from the isolated browser process are correctly received and bridged. */ + @RequiresEventBridgeRegistration override fun listenToEvents( browserId: String, scope: CoroutineScope, diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelperInterface.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelperInterface.kt index 7c0c329..0ff2bbc 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelperInterface.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelperInterface.kt @@ -1,6 +1,7 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -12,7 +13,11 @@ interface OSIABFlowHelperInterface { * @param browserId Identifier for the browser instance to emit events to * @param scope CoroutineScope to launch * @param onEventReceived callback to send the collected event in + * + * @note For Android API 28+, you must call [OSIABEvents.registerReceiver] once during your application + * or activity lifecycle to ensure events from the isolated browser process are correctly received and bridged. */ + @RequiresEventBridgeRegistration fun listenToEvents( browserId: String, scope: CoroutineScope, diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt index 0c4e69a..08df9d1 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt @@ -6,6 +6,7 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsSession import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelperInterface import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFlowHelperInterface @@ -40,7 +41,9 @@ class OSIABCustomTabsRouterAdapter( // for the browserPageLoaded event, which we only want to trigger on the first URL loaded in the CustomTabs instance private var isFirstLoad = true + @OptIn(RequiresEventBridgeRegistration::class) override fun close(completionHandler: (Boolean) -> Unit) { + OSIABEvents.registerReceiver(context) var closeEventJob: Job? = null closeEventJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> @@ -141,6 +144,7 @@ class OSIABCustomTabsRouterAdapter( } override fun handleOpen(url: String, completionHandler: (Boolean) -> Unit) { + OSIABEvents.registerReceiver(context) lifecycleScope.launch { try { val uri = Uri.parse(url) @@ -165,6 +169,7 @@ class OSIABCustomTabsRouterAdapter( } } + @OptIn(RequiresEventBridgeRegistration::class) private fun openCustomTabsIntent(session: CustomTabsSession, uri: Uri, completionHandler: (Boolean) -> Unit) { val customTabsIntent = buildCustomTabsIntent(session) var eventsJob: Job? = null diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt index 6f5b741..951ffc5 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFlowHelperInterface import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABWebViewOptions import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABWebViewActivity @@ -71,10 +72,12 @@ class OSIABWebViewRouterAdapter( * @param url URL to be opened. * @param completionHandler The callback with the result of opening the url. */ + @OptIn(RequiresEventBridgeRegistration::class) override fun handleOpen(url: String, completionHandler: (Boolean) -> Unit) { lifecycleScope.launch { try { // Collect the browser events + OSIABEvents.registerReceiver(context) var eventsJob: Job? = null eventsJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> when (event) { diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt index 59ec090..0b42254 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt @@ -148,6 +148,15 @@ class OSIABWebViewActivity : AppCompatActivity() { return File.createTempFile("${prefix}${timeStamp}_", suffix, storageDir) } + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + WebView.setDataDirectorySuffix("OSInAppBrowser") + } catch (e: Exception) { + Log.d(LOG_TAG, "Suffix already set or error: ${e.message}") + } + } + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -164,7 +173,7 @@ class OSIABWebViewActivity : AppCompatActivity() { browserId = intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID) ?: "" - sendWebViewEvent(OSIABWebViewEvent(browserId, this@OSIABWebViewActivity)) + sendWebViewEvent(OSIABEvents.OSIABWebViewEvent(browserId)) appName = applicationInfo.loadLabel(packageManager).toString() @@ -917,6 +926,7 @@ class OSIABWebViewActivity : AppCompatActivity() { private fun sendWebViewEvent(event: OSIABEvents) { lifecycleScope.launch { OSIABEvents.postEvent(event) + OSIABEvents.broadcastEvent(this@OSIABWebViewActivity, event) } } From 60c07587e1dca8b0f3836fea8a34ca13e43b48ae Mon Sep 17 00:00:00 2001 From: Chace Daniels Date: Mon, 19 Jan 2026 15:01:20 -0600 Subject: [PATCH 2/3] chore: update pom.xml version and changelog --- CHANGELOG.md | 6 ++++++ pom.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcac34..61b6e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.0] + +### Fixes + +- Fix issue with local storage isolation between WebView and main app on Android 28+ [RMET-4918](https://outsystemsrd.atlassian.net/browse/RMET-4918) + ## [1.6.1] ### Fixes diff --git a/pom.xml b/pom.xml index 3ab5e59..c704fb1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ioninappbrowser-android - 1.6.1 + 1.7.0 From 0d1632ee030f5cb2339202fc727ff24dfbad7306 Mon Sep 17 00:00:00 2001 From: Chace Daniels Date: Wed, 28 Jan 2026 09:23:43 -0600 Subject: [PATCH 3/3] fix: broadcast events correctly across processes --- .../osinappbrowserlib/OSIABEvents.kt | 67 ++++++++++++++----- .../helpers/OSIABCustomTabsSessionHelper.kt | 5 +- .../OSIABCustomTabsRouterAdapter.kt | 20 ++++-- .../OSIABWebViewRouterAdapter.kt | 53 +++++++++------ .../views/OSIABWebViewActivity.kt | 33 ++++++++- 5 files changed, 127 insertions(+), 51 deletions(-) diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABEvents.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABEvents.kt index 76b6e45..3a5ff48 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABEvents.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABEvents.kt @@ -1,11 +1,11 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib -import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build +import androidx.core.content.ContextCompat +import androidx.core.content.IntentCompat import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABWebViewActivity import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -25,41 +25,49 @@ sealed class OSIABEvents : Serializable { data class BrowserPageLoaded(override val browserId: String) : OSIABEvents() data class BrowserFinished(override val browserId: String) : OSIABEvents() data class BrowserPageNavigationCompleted(override val browserId: String, val url: String?) : OSIABEvents() - data class OSIABCustomTabsEvent( override val browserId: String, val action: String, - val context: Context + @Transient val context: Context? = null ) : OSIABEvents() + data class OSIABWebViewEvent( override val browserId: String, - val activity: OSIABWebViewActivity? = null + @Transient val activity: OSIABWebViewActivity? = null ) : OSIABEvents() companion object { const val EXTRA_BROWSER_ID = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_BROWSER_ID" const val ACTION_IAB_EVENT = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.ACTION_IAB_EVENT" + const val ACTION_CLOSE_WEBVIEW = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.ACTION_CLOSE_WEBVIEW" const val EXTRA_EVENT_DATA = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_EVENT_DATA" + // Buffer capacity is required because BroadcastReceiver.onReceive() is synchronous. + // We must use tryEmit() which would drop events without buffer space. private val _events = MutableSharedFlow(extraBufferCapacity = 64) val events = _events.asSharedFlow() private var receiver: BroadcastReceiver? = null + private var receiverRefCount = 0 - @SuppressLint("UnspecifiedRegisterReceiverFlag") + /** + * Registers a BroadcastReceiver to listen for events from the isolated WebView process. + * This must be called before opening a WebView on Android 9+ to ensure events are received. + */ + @Synchronized fun registerReceiver(context: Context) { + receiverRefCount++ if (receiver != null) return val appContext = context.applicationContext receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == ACTION_IAB_EVENT) { - @Suppress("DEPRECATION") - val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getSerializableExtra(EXTRA_EVENT_DATA, OSIABEvents::class.java) - } else { - intent.getSerializableExtra(EXTRA_EVENT_DATA) as? OSIABEvents - } + val event = IntentCompat.getSerializableExtra( + intent, + EXTRA_EVENT_DATA, + OSIABEvents::class.java + ) event?.let { _events.tryEmit(it) } @@ -68,10 +76,33 @@ sealed class OSIABEvents : Serializable { } val filter = IntentFilter(ACTION_IAB_EVENT) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - appContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) - } else { - appContext.registerReceiver(receiver, filter) + ContextCompat.registerReceiver( + appContext, + receiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED + ) + } + + /** + * Unregisters the BroadcastReceiver. Should be called when the browser is closed. + * The receiver is only truly unregistered when all registered 'users' have unregistered. + */ + @Synchronized + fun unregisterReceiver(context: Context) { + if (receiverRefCount > 0) { + receiverRefCount-- + } + + if (receiverRefCount == 0) { + receiver?.let { + try { + context.applicationContext.unregisterReceiver(it) + } catch (e: Exception) { + // Receiver may not be registered, ignore + } + receiver = null + } } } @@ -79,6 +110,10 @@ sealed class OSIABEvents : Serializable { _events.emit(event) } + /** + * Broadcasts an event from the isolated WebView process to the main process. + * Only data-only events should be broadcast (BrowserPageLoaded, BrowserFinished, etc.). + */ fun broadcastEvent(context: Context, event: OSIABEvents) { val intent = Intent(ACTION_IAB_EVENT).apply { setPackage(context.packageName) diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt index a5e5862..f45ac20 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt @@ -1,3 +1,5 @@ +@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class) + package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers import android.content.ComponentName @@ -11,7 +13,6 @@ import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents -import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABCustomTabsControllerActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -36,7 +37,6 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface { flowHelper: OSIABFlowHelperInterface, customTabsSessionCallback: (CustomTabsSession?) -> Unit ) { - OSIABEvents.registerReceiver(context) CustomTabsClient.bindCustomTabsService( context, packageName, @@ -56,7 +56,6 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface { ) } - @OptIn(RequiresEventBridgeRegistration::class) private inner class CustomTabsCallbackImpl( private val browserId: String, private val lifecycleScope: CoroutineScope, diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt index 08df9d1..d6d1cb5 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt @@ -1,3 +1,5 @@ +@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class) + package com.outsystems.plugins.inappbrowser.osinappbrowserlib.routeradapters import android.content.Context @@ -6,7 +8,6 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsSession import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents -import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelperInterface import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFlowHelperInterface @@ -40,10 +41,13 @@ class OSIABCustomTabsRouterAdapter( // for the browserPageLoaded event, which we only want to trigger on the first URL loaded in the CustomTabs instance private var isFirstLoad = true + private var isFinished = false - @OptIn(RequiresEventBridgeRegistration::class) override fun close(completionHandler: (Boolean) -> Unit) { - OSIABEvents.registerReceiver(context) + if (isFinished) { + completionHandler(true) + return + } var closeEventJob: Job? = null closeEventJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> @@ -144,7 +148,6 @@ class OSIABCustomTabsRouterAdapter( } override fun handleOpen(url: String, completionHandler: (Boolean) -> Unit) { - OSIABEvents.registerReceiver(context) lifecycleScope.launch { try { val uri = Uri.parse(url) @@ -169,7 +172,6 @@ class OSIABCustomTabsRouterAdapter( } } - @OptIn(RequiresEventBridgeRegistration::class) private fun openCustomTabsIntent(session: CustomTabsSession, uri: Uri, completionHandler: (Boolean) -> Unit) { val customTabsIntent = buildCustomTabsIntent(session) var eventsJob: Job? = null @@ -178,13 +180,16 @@ class OSIABCustomTabsRouterAdapter( is OSIABEvents.OSIABCustomTabsEvent -> { if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_READY) { try { - customTabsIntent.launchUrl(event.context, uri) - completionHandler(true) + event.context?.let { ctx -> + customTabsIntent.launchUrl(ctx, uri) + completionHandler(true) + } ?: completionHandler(false) } catch (e: Exception) { completionHandler(false) } } else if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) { + isFinished = true onBrowserFinished() eventsJob?.cancel() } @@ -198,6 +203,7 @@ class OSIABCustomTabsRouterAdapter( is OSIABEvents.BrowserFinished -> { // Ensure that custom tabs controller activity is fully destroyed startCustomTabsControllerActivity(true) + isFinished = true onBrowserFinished() eventsJob?.cancel() } diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt index 951ffc5..1a98a87 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABWebViewRouterAdapter.kt @@ -11,7 +11,6 @@ import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABWebViewA import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import java.lang.ref.WeakReference import java.util.UUID class OSIABWebViewRouterAdapter( @@ -39,30 +38,41 @@ class OSIABWebViewRouterAdapter( const val CUSTOM_HEADERS_EXTRA = "CUSTOM_HEADERS_EXTRA" } - private var webViewActivityRef: WeakReference? = null + private var isFinished = false - private fun setWebViewActivity(activity: OSIABWebViewActivity?) { - webViewActivityRef = if (activity == null) { - null - } else { - WeakReference(activity) + private fun finalizeBrowser() { + if (!isFinished) { + isFinished = true + onBrowserFinished() + OSIABEvents.unregisterReceiver(context) } } - private fun getWebViewActivity(): OSIABWebViewActivity? { - return webViewActivityRef?.get() - } - + /** + * Closes the WebView by sending a broadcast to the separate process. + * The WebView activity will receive this and call finish() on itself. + */ + @OptIn(RequiresEventBridgeRegistration::class) override fun close(completionHandler: (Boolean) -> Unit) { - getWebViewActivity().let { activity -> - if(activity == null) { - completionHandler(false) - } - else { - activity.finish() - setWebViewActivity(null) - onBrowserFinished() + if (isFinished) { + completionHandler(true) + return + } + + // Send close broadcast to the WebView process + val closeIntent = Intent(OSIABEvents.ACTION_CLOSE_WEBVIEW).apply { + setPackage(context.packageName) + putExtra(OSIABEvents.EXTRA_BROWSER_ID, browserId) + } + context.sendBroadcast(closeIntent) + + // Listen for the BrowserFinished event to confirm close + var closeJob: Job? = null + closeJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> + if (event is OSIABEvents.BrowserFinished) { + finalizeBrowser() completionHandler(true) + closeJob?.cancel() } } } @@ -82,15 +92,13 @@ class OSIABWebViewRouterAdapter( eventsJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> when (event) { is OSIABEvents.OSIABWebViewEvent -> { - setWebViewActivity(event.activity) completionHandler(true) } is OSIABEvents.BrowserPageLoaded -> { onBrowserPageLoaded() } is OSIABEvents.BrowserFinished -> { - setWebViewActivity(null) - onBrowserFinished() + finalizeBrowser() eventsJob?.cancel() } is OSIABEvents.BrowserPageNavigationCompleted -> { @@ -116,6 +124,7 @@ class OSIABWebViewRouterAdapter( ) } catch (e: Exception) { + finalizeBrowser() completionHandler(false) } } diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt index 0b42254..d75999e 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt @@ -2,16 +2,18 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.views import android.Manifest import android.app.Activity +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager +import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.util.Log import android.view.Gravity -import android.graphics.Bitmap import android.view.View import android.webkit.CookieManager import android.webkit.GeolocationPermissions @@ -39,7 +41,6 @@ import androidx.core.content.FileProvider import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents -import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents.OSIABWebViewEvent import com.outsystems.plugins.inappbrowser.osinappbrowserlib.R import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABPdfHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABToolbarPosition @@ -69,6 +70,8 @@ class OSIABWebViewActivity : AppCompatActivity() { private lateinit var appName: String private lateinit var browserId: String + private var closeReceiver: BroadcastReceiver? = null + // for the browserPageLoaded event, which we only want to trigger on the first URL loaded in the WebView private var isFirstLoad = true @@ -173,6 +176,23 @@ class OSIABWebViewActivity : AppCompatActivity() { browserId = intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID) ?: "" + // Register receiver for close commands from main process + closeReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val targetBrowserId = intent?.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID) + if (targetBrowserId == browserId) { + finish() + } + } + } + val filter = IntentFilter(OSIABEvents.ACTION_CLOSE_WEBVIEW) + ContextCompat.registerReceiver( + this, + closeReceiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED + ) + sendWebViewEvent(OSIABEvents.OSIABWebViewEvent(browserId)) appName = applicationInfo.loadLabel(packageManager).toString() @@ -250,6 +270,14 @@ class OSIABWebViewActivity : AppCompatActivity() { } override fun onDestroy() { + closeReceiver?.let { + try { + unregisterReceiver(it) + } catch (e: Exception) { + // Receiver may not be registered, ignore + } + closeReceiver = null + } webView.destroy() super.onDestroy() } @@ -925,7 +953,6 @@ class OSIABWebViewActivity : AppCompatActivity() { */ private fun sendWebViewEvent(event: OSIABEvents) { lifecycleScope.launch { - OSIABEvents.postEvent(event) OSIABEvents.broadcastEvent(this@OSIABWebViewActivity, event) } }