Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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>
1 change: 1 addition & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<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"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,126 @@
package com.outsystems.plugins.inappbrowser.osinappbrowserlib

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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
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
@Transient val activity: OSIABWebViewActivity? = null
Comment thread
ItsChaceD marked this conversation as resolved.
Outdated
) : 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
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -38,30 +38,41 @@ class OSIABWebViewRouterAdapter(
const val CUSTOM_HEADERS_EXTRA = "CUSTOM_HEADERS_EXTRA"
}

private var webViewActivityRef: WeakReference<OSIABWebViewActivity>? = 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()
}
Comment thread
ItsChaceD marked this conversation as resolved.
Outdated
}
}
Expand All @@ -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 -> {
Expand All @@ -113,6 +124,7 @@ class OSIABWebViewRouterAdapter(
)

} catch (e: Exception) {
finalizeBrowser()
Comment thread
ItsChaceD marked this conversation as resolved.
Outdated
completionHandler(false)
}
}
Expand Down
Loading
Loading