Skip to content

Commit 96d7d49

Browse files
authored
fix: separate local storage between WebView processes with isIsolated
1 parent 910e674 commit 96d7d49

15 files changed

Lines changed: 323 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.0.0]
8+
9+
### Fixes
10+
11+
- Isolate WebView local storage and cookies from the main app by default on Android 28+ [RMET-4918](https://outsystemsrd.atlassian.net/browse/RMET-4918)
12+
13+
### BREAKING CHANGES
14+
15+
- WebView storage is now isolated by default on Android 28+. Apps that need to share the main app WebView's `localStorage` or cookies must set `isIsolated` to `false`.
16+
717
## [1.6.2]
818

919
### Fixes
1020

11-
- Replace HEAD / GET request for checking if file is PDF, with WebKit's [DownloadListener](https://developer.android.com/reference/android/webkit/DownloadListener). This makes sure that for non-PDF urls, no extra request is done [RMET-5141](https://outsystemsrd.atlassian.net/browse/RMET-5141) / [RPM-6744](https://outsystemsrd.atlassian.net/browse/RPM-6744)
21+
- Replace HEAD / GET request for checking if file is PDF, with WebKit's [DownloadListener](https://developer.android.com/reference/android/webkit/DownloadListener). This makes sure that for non-PDF urls, no extra request is done [RMET-5141](https://outsystemsrd.atlassian.net/browse/RMET-5141) / [RPM-6744](https://outsystemsrd.atlassian.net/browse/RPM-6744)
1222

1323
## [1.6.1]
1424

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
<modelVersion>4.0.0</modelVersion>
77
<groupId>io.ionic.libs</groupId>
88
<artifactId>ioninappbrowser-android</artifactId>
9-
<version>1.6.2</version>
9+
<version>2.0.0</version>
1010
</project>

src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
<activity
1818
android:name=".views.OSIABWebViewActivity"
1919
android:exported="false"
20+
android:process=":OSInAppBrowser"
21+
android:configChanges="orientation|screenSize|uiMode"
22+
android:label="OSIABWebViewActivity"
23+
android:theme="@style/AppTheme.WebView"
24+
android:enableOnBackInvokedCallback="true"
25+
tools:targetApi="33" />
26+
<activity
27+
android:name=".views.OSIABWebViewActivitySharing"
28+
android:exported="false"
2029
android:configChanges="orientation|screenSize|uiMode"
2130
android:label="OSIABWebViewActivity"
2231
android:theme="@style/AppTheme.WebView"
Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,124 @@
11
package com.outsystems.plugins.inappbrowser.osinappbrowserlib
22

3+
import android.content.BroadcastReceiver
34
import android.content.Context
4-
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABWebViewActivity
5+
import android.content.Intent
6+
import android.content.IntentFilter
7+
import androidx.core.content.ContextCompat
8+
import androidx.core.content.IntentCompat
59
import kotlinx.coroutines.flow.MutableSharedFlow
610
import kotlinx.coroutines.flow.asSharedFlow
11+
import java.io.Serializable
712

8-
sealed class OSIABEvents {
13+
@RequiresOptIn(
14+
level = RequiresOptIn.Level.WARNING,
15+
message = "This API requires a prior call to OSIABEvents.registerReceiver(context) to work correctly with process isolation on Android 9+."
16+
)
17+
@Retention(AnnotationRetention.BINARY)
18+
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
19+
annotation class RequiresEventBridgeRegistration
20+
21+
sealed class OSIABEvents : Serializable {
922
abstract val browserId: String
1023

1124
data class BrowserPageLoaded(override val browserId: String) : OSIABEvents()
1225
data class BrowserFinished(override val browserId: String) : OSIABEvents()
1326
data class BrowserPageNavigationCompleted(override val browserId: String, val url: String?) : OSIABEvents()
14-
1527
data class OSIABCustomTabsEvent(
1628
override val browserId: String,
1729
val action: String,
18-
val context: Context
30+
@Transient val context: Context? = null
1931
) : OSIABEvents()
32+
2033
data class OSIABWebViewEvent(
21-
override val browserId: String,
22-
val activity: OSIABWebViewActivity
34+
override val browserId: String
2335
) : OSIABEvents()
2436

2537
companion object {
2638
const val EXTRA_BROWSER_ID = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_BROWSER_ID"
39+
const val ACTION_IAB_EVENT = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.ACTION_IAB_EVENT"
40+
const val ACTION_CLOSE_WEBVIEW = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.ACTION_CLOSE_WEBVIEW"
41+
const val EXTRA_EVENT_DATA = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_EVENT_DATA"
2742

28-
private val _events = MutableSharedFlow<OSIABEvents>()
43+
// Buffer capacity is required because BroadcastReceiver.onReceive() is synchronous.
44+
// We must use tryEmit() which would drop events without buffer space.
45+
private val _events = MutableSharedFlow<OSIABEvents>(extraBufferCapacity = 64)
2946
val events = _events.asSharedFlow()
3047

48+
private var receiver: BroadcastReceiver? = null
49+
private var receiverRefCount = 0
50+
51+
/**
52+
* Registers a BroadcastReceiver to listen for events from the isolated WebView process.
53+
* This must be called before opening a WebView on Android 9+ to ensure events are received.
54+
*/
55+
@Synchronized
56+
fun registerReceiver(context: Context) {
57+
receiverRefCount++
58+
if (receiver != null) return
59+
60+
val appContext = context.applicationContext
61+
receiver = object : BroadcastReceiver() {
62+
override fun onReceive(context: Context?, intent: Intent?) {
63+
if (intent?.action == ACTION_IAB_EVENT) {
64+
val event = IntentCompat.getSerializableExtra(
65+
intent,
66+
EXTRA_EVENT_DATA,
67+
OSIABEvents::class.java
68+
)
69+
event?.let {
70+
_events.tryEmit(it)
71+
}
72+
}
73+
}
74+
}
75+
76+
val filter = IntentFilter(ACTION_IAB_EVENT)
77+
ContextCompat.registerReceiver(
78+
appContext,
79+
receiver,
80+
filter,
81+
ContextCompat.RECEIVER_NOT_EXPORTED
82+
)
83+
}
84+
85+
/**
86+
* Unregisters the BroadcastReceiver. Should be called when the browser is closed.
87+
* The receiver is only truly unregistered when all registered 'users' have unregistered.
88+
*/
89+
@Synchronized
90+
fun unregisterReceiver(context: Context) {
91+
if (receiverRefCount > 0) {
92+
receiverRefCount--
93+
}
94+
95+
if (receiverRefCount == 0) {
96+
receiver?.let {
97+
try {
98+
context.applicationContext.unregisterReceiver(it)
99+
} catch (e: Exception) {
100+
// Receiver may not be registered, ignore
101+
}
102+
receiver = null
103+
}
104+
}
105+
}
106+
31107
suspend fun postEvent(event: OSIABEvents) {
32108
_events.emit(event)
33109
}
110+
111+
/**
112+
* Broadcasts an event from the isolated WebView process to the main process.
113+
* Only data-only events should be broadcast (BrowserPageLoaded, BrowserFinished, etc.).
114+
*/
115+
fun broadcastEvent(context: Context, event: OSIABEvents) {
116+
val intent = Intent(ACTION_IAB_EVENT).apply {
117+
setPackage(context.packageName)
118+
putExtra(EXTRA_EVENT_DATA, event)
119+
}
120+
context.sendBroadcast(intent)
121+
}
34122
}
35123

36124
}

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class)
2+
13
package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers
24

35
import android.content.ComponentName

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelper.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers
22

33
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents
4+
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration
45
import kotlinx.coroutines.CoroutineScope
56
import kotlinx.coroutines.Job
67
import kotlinx.coroutines.flow.transformWhile
@@ -14,7 +15,11 @@ class OSIABFlowHelper: OSIABFlowHelperInterface {
1415
* @param browserId Identifier for the browser instance to emit events to
1516
* @param scope CoroutineScope to launch
1617
* @param onEventReceived callback to send the collected event in
18+
*
19+
* @note For Android API 28+, you must call [OSIABEvents.registerReceiver] once during your application
20+
* or activity lifecycle to ensure events from the isolated browser process are correctly received and bridged.
1721
*/
22+
@RequiresEventBridgeRegistration
1823
override fun listenToEvents(
1924
browserId: String,
2025
scope: CoroutineScope,

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFlowHelperInterface.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers
22

33
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents
4+
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration
45
import kotlinx.coroutines.CoroutineScope
56
import kotlinx.coroutines.Job
67

@@ -12,7 +13,11 @@ interface OSIABFlowHelperInterface {
1213
* @param browserId Identifier for the browser instance to emit events to
1314
* @param scope CoroutineScope to launch
1415
* @param onEventReceived callback to send the collected event in
16+
*
17+
* @note For Android API 28+, you must call [OSIABEvents.registerReceiver] once during your application
18+
* or activity lifecycle to ensure events from the isolated browser process are correctly received and bridged.
1519
*/
20+
@RequiresEventBridgeRegistration
1621
fun listenToEvents(
1722
browserId: String,
1823
scope: CoroutineScope,

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/models/OSIABWebViewOptions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ data class OSIABWebViewOptions(
1616
@SerializedName("allowZoom") val allowZoom: Boolean = true,
1717
@SerializedName("hardwareBack") val hardwareBack: Boolean = true,
1818
@SerializedName("pauseMedia") val pauseMedia: Boolean = true,
19-
@SerializedName("customUserAgent") val customUserAgent: String? = null
19+
@SerializedName("customUserAgent") val customUserAgent: String? = null,
20+
@SerializedName("isIsolated") val isIsolated: Boolean = true
2021
) : OSIABOptions, Serializable

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class)
2+
13
package com.outsystems.plugins.inappbrowser.osinappbrowserlib.routeradapters
24

35
import android.content.Context
@@ -39,8 +41,13 @@ class OSIABCustomTabsRouterAdapter(
3941

4042
// for the browserPageLoaded event, which we only want to trigger on the first URL loaded in the CustomTabs instance
4143
private var isFirstLoad = true
44+
private var isFinished = false
4245

4346
override fun close(completionHandler: (Boolean) -> Unit) {
47+
if (isFinished) {
48+
completionHandler(true)
49+
return
50+
}
4451
var closeEventJob: Job? = null
4552

4653
closeEventJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event ->
@@ -173,13 +180,16 @@ class OSIABCustomTabsRouterAdapter(
173180
is OSIABEvents.OSIABCustomTabsEvent -> {
174181
if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_READY) {
175182
try {
176-
customTabsIntent.launchUrl(event.context, uri)
177-
completionHandler(true)
183+
event.context?.let { ctx ->
184+
customTabsIntent.launchUrl(ctx, uri)
185+
completionHandler(true)
186+
} ?: completionHandler(false)
178187
} catch (e: Exception) {
179188
completionHandler(false)
180189
}
181190
}
182191
else if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) {
192+
isFinished = true
183193
onBrowserFinished()
184194
eventsJob?.cancel()
185195
}
@@ -193,6 +203,7 @@ class OSIABCustomTabsRouterAdapter(
193203
is OSIABEvents.BrowserFinished -> {
194204
// Ensure that custom tabs controller activity is fully destroyed
195205
startCustomTabsControllerActivity(true)
206+
isFinished = true
196207
onBrowserFinished()
197208
eventsJob?.cancel()
198209
}

0 commit comments

Comments
 (0)