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>
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"
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should make sure plugins (OutSystems especially) document this change, wouldn't be surprised if it ends up breaking existing Android apps in some unexpected way, due to the many different ways people use InAppBrowser.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, doesn't this mean that existing apps that have cookies or local storage data for a certain webpage in InAppBrowser, will no longer have that data when they update the app to this version where the data directory changes?

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,11 +1,25 @@
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 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Feels confusing for this class to be Serializable, subclasses have Context and Activity which are not serializable. The serialization is only relevant for the broadcast receiver, which is only called for webview, but from what I see in the PR you don't set OSIABWebViewActivity anymore for OSIABWebViewEvent.

Can you explain why you implemented it that way?

abstract val browserId: String

data class BrowserPageLoaded(override val browserId: String) : OSIABEvents()
Expand All @@ -19,18 +33,59 @@ sealed class OSIABEvents {
) : OSIABEvents()
data class OSIABWebViewEvent(
override val browserId: String,
val activity: OSIABWebViewActivity
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 EXTRA_EVENT_DATA = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_EVENT_DATA"

private val _events = MutableSharedFlow<OSIABEvents>()
private val _events = MutableSharedFlow<OSIABEvents>(extraBufferCapacity = 64)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why was this added?

val events = _events.asSharedFlow()

private var receiver: BroadcastReceiver? = null

@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun registerReceiver(context: Context) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

So this is necessary because the WebView activity now runs in a separate process?

Wouldn't this be necessary for CustomTabs as well before-hand?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wondering if we should have a way to unregister the receiver, e.g. after Close events are submitted, can we get rid of the receiver, rather than it living throughout the entire app's lifecycle?

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
}
Comment on lines +58 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: We can probably replace this with a one-liner for IntentCompat#getSerializableExtra (assuming the library already has an AndroidX dependency with the method available)

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)
}
Comment on lines +71 to +75
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: Can maybe replace this with a one-liner ContextCompat#registerReceiver (assuming the library already has an AndroidX dependency that comes with this)

}

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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,7 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface {
flowHelper: OSIABFlowHelperInterface,
customTabsSessionCallback: (CustomTabsSession?) -> Unit
) {
OSIABEvents.registerReceiver(context)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is being called here but broadcastEvent isn't called here, so wondering about its usefulness.

Same for OSIABCustomTabsRouterAdapter

CustomTabsClient.bindCustomTabsService(
context,
packageName,
Expand All @@ -54,6 +56,7 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface {
)
}

@OptIn(RequiresEventBridgeRegistration::class)
private inner class CustomTabsCallbackImpl(
private val browserId: String,
private val lifecycleScope: 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
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 @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand All @@ -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()

Expand Down Expand Up @@ -917,6 +926,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
private fun sendWebViewEvent(event: OSIABEvents) {
lifecycleScope.launch {
OSIABEvents.postEvent(event)
OSIABEvents.broadcastEvent(this@OSIABWebViewActivity, event)
Comment on lines 928 to +929
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we be calling both methods here?

}
}

Expand Down
Loading