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
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 @@
()
+ // 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
+
+ /**
+ * 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) {
+ val event = IntentCompat.getSerializableExtra(
+ intent,
+ EXTRA_EVENT_DATA,
+ OSIABEvents::class.java
+ )
+ event?.let {
+ _events.tryEmit(it)
+ }
+ }
+ }
+ }
+
+ val filter = IntentFilter(ACTION_IAB_EVENT)
+ 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
+ }
+ }
+ }
+
suspend fun postEvent(event: OSIABEvents) {
_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)
+ 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..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
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..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
@@ -39,8 +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
override fun close(completionHandler: (Boolean) -> Unit) {
+ if (isFinished) {
+ completionHandler(true)
+ return
+ }
var closeEventJob: Job? = null
closeEventJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event ->
@@ -173,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()
}
@@ -193,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 6f5b741..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
@@ -4,13 +4,13 @@ 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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import java.lang.ref.WeakReference
import java.util.UUID
class OSIABWebViewRouterAdapter(
@@ -38,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()
}
}
}
@@ -71,23 +82,23 @@ 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) {
is OSIABEvents.OSIABWebViewEvent -> {
- setWebViewActivity(event.activity)
completionHandler(true)
}
is OSIABEvents.BrowserPageLoaded -> {
onBrowserPageLoaded()
}
is OSIABEvents.BrowserFinished -> {
- setWebViewActivity(null)
- onBrowserFinished()
+ finalizeBrowser()
eventsJob?.cancel()
}
is OSIABEvents.BrowserPageNavigationCompleted -> {
@@ -113,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 59ec090..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
@@ -148,6 +151,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 +176,24 @@ class OSIABWebViewActivity : AppCompatActivity() {
browserId = intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID) ?: ""
- sendWebViewEvent(OSIABWebViewEvent(browserId, this@OSIABWebViewActivity))
+ // 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()
@@ -241,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()
}
@@ -916,7 +953,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
*/
private fun sendWebViewEvent(event: OSIABEvents) {
lifecycleScope.launch {
- OSIABEvents.postEvent(event)
+ OSIABEvents.broadcastEvent(this@OSIABWebViewActivity, event)
}
}