Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.ionic.libs</groupId>
<artifactId>ioninappbrowser-android</artifactId>
<version>1.6.1</version>
<version>1.7.0</version>
</project>
9 changes: 9 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
<activity
android:name=".views.OSIABWebViewActivity"
android:exported="false"
android:process=":OSInAppBrowser"
Comment thread
OS-pedrogustavobilro marked this conversation as resolved.
android:configChanges="orientation|screenSize|uiMode"
android:label="OSIABWebViewActivity"
android:theme="@style/AppTheme.WebView"
android:enableOnBackInvokedCallback="true"
tools:targetApi="33" />
<activity
android:name=".views.OSIABWebViewActivitySharing"
android:exported="false"
android:configChanges="orientation|screenSize|uiMode"
android:label="OSIABWebViewActivity"
android:theme="@style/AppTheme.WebView"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,124 @@
package com.outsystems.plugins.inappbrowser.osinappbrowserlib

import android.content.BroadcastReceiver
import android.content.Context
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABWebViewActivity
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.io.Serializable

sealed class OSIABEvents {
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "This API requires a prior call to OSIABEvents.registerReceiver(context) to work correctly with process isolation on Android 9+."
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class RequiresEventBridgeRegistration

sealed class OSIABEvents : Serializable {
Comment thread
OS-pedrogustavobilro marked this conversation as resolved.
abstract val browserId: String

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
override val browserId: String
) : 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"

private val _events = MutableSharedFlow<OSIABEvents>()
// Buffer capacity is required because BroadcastReceiver.onReceive() is synchronous.
// We must use tryEmit() which would drop events without buffer space.
private val _events = MutableSharedFlow<OSIABEvents>(extraBufferCapacity = 64)
Comment thread
ItsChaceD marked this conversation as resolved.
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) {
Comment thread
ItsChaceD marked this conversation as resolved.
Comment thread
ItsChaceD marked this conversation as resolved.
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)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class)

package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers

import android.content.ComponentName
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ data class OSIABWebViewOptions(
@SerializedName("allowZoom") val allowZoom: Boolean = true,
@SerializedName("hardwareBack") val hardwareBack: Boolean = true,
@SerializedName("pauseMedia") val pauseMedia: Boolean = true,
@SerializedName("customUserAgent") val customUserAgent: String? = null
@SerializedName("customUserAgent") val customUserAgent: String? = null,
@SerializedName("isIsolated") val isIsolated: Boolean = true
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm here: By default the plugin will set "isIsolated" to true, meaning this will be a breaking change in the plugin as discussed some time ago?

The reason why we are using default of true is because there are potential security concerns implicated? Because otherwise if we think there aren't security concerns, we can leave it at false to maintain backwards compatibility.

) : OSIABOptions, Serializable
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(com.outsystems.plugins.inappbrowser.osinappbrowserlib.RequiresEventBridgeRegistration::class)

package com.outsystems.plugins.inappbrowser.osinappbrowserlib.routeradapters

import android.content.Context
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down
Loading
Loading