diff --git a/CHANGELOG.md b/CHANGELOG.md index 6284d6d..d5640f9 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). +## [2.0.3] + +### Fixes + +- Keep the main app process active while the WebView is in the foreground, by binding it to the WebView's process. Without this, Android can freeze the main process during a browser session, so browser events stop being processed and the app never reacts to the page finishing ([RMET-5394](https://outsystemsrd.atlassian.net/browse/RMET-5394)). + ## [2.0.2] ### Fixes diff --git a/pom.xml b/pom.xml index 259c045..41e4cf8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ioninappbrowser-android - 2.0.2 + 2.0.3 diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 2218bcf..10c7dc4 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -49,6 +49,9 @@ android:resource="@xml/file_paths" /> + diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABKeepAliveService.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABKeepAliveService.kt new file mode 100644 index 0000000..bbbcced --- /dev/null +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABKeepAliveService.kt @@ -0,0 +1,15 @@ +package com.outsystems.plugins.inappbrowser.osinappbrowserlib + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.IBinder + +/** + * Runs in the main app process. While the isolated WebView activity is bound to it, + * the main process is not eligible for OS app freezing, so it keeps processing + * browser events while the WebView is in the foreground. + */ +class OSIABKeepAliveService : Service() { + override fun onBind(intent: Intent?): IBinder = Binder() +} 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 6665832..b8a5d5a 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 @@ -4,14 +4,17 @@ import android.Manifest import android.app.Application import android.app.Activity import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.ServiceConnection import android.content.pm.PackageManager import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.IBinder import android.provider.MediaStore import android.util.Log import android.view.Gravity @@ -42,6 +45,7 @@ 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.OSIABKeepAliveService import com.outsystems.plugins.inappbrowser.osinappbrowserlib.R import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABPdfHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABToolbarPosition @@ -73,6 +77,8 @@ open class OSIABWebViewActivity : AppCompatActivity() { private var closeReceiver: BroadcastReceiver? = null + private var keepAliveConnection: ServiceConnection? = null + // for the browserPageLoaded event, which we only want to trigger on the first URL loaded in the WebView private var isFirstLoad = true @@ -139,6 +145,11 @@ open class OSIABWebViewActivity : AppCompatActivity() { const val REQUEST_LOCATION_PERMISSION = 623 const val REQUEST_CAMERA_PERMISSION = 624 const val LOG_TAG = "OSIABWebViewActivity" + const val ISOLATED_PROCESS_SUFFIX = ":OSInAppBrowser" + + private fun isIsolatedProcess(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + Application.getProcessName().endsWith(ISOLATED_PROCESS_SUFFIX) val errorsToHandle = listOf( WebViewClient.ERROR_HOST_LOOKUP, WebViewClient.ERROR_UNSUPPORTED_SCHEME, @@ -153,15 +164,12 @@ open class OSIABWebViewActivity : AppCompatActivity() { } init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { - val processName = Application.getProcessName() - if (processName.endsWith(":OSInAppBrowser")) { - WebView.setDataDirectorySuffix("OSInAppBrowser") - } - } catch (e: Exception) { - Log.d(LOG_TAG, "Suffix already set or error: ${e.message}") + try { + if (isIsolatedProcess()) { + WebView.setDataDirectorySuffix("OSInAppBrowser") } + } catch (e: Exception) { + Log.d(LOG_TAG, "Suffix already set or error: ${e.message}") } } } @@ -180,6 +188,17 @@ open class OSIABWebViewActivity : AppCompatActivity() { browserId = intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID) ?: "" + // keep the main process out of the freezable state while the browser is in front, + // otherwise events queue and the app's close flow stalls until it unfreezes + if (isIsolatedProcess()) { + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) {} + override fun onServiceDisconnected(name: ComponentName?) {} + } + bindService(Intent(this, OSIABKeepAliveService::class.java), connection, Context.BIND_AUTO_CREATE) + keepAliveConnection = connection + } + // Register receiver for close commands from main process closeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -280,6 +299,14 @@ open class OSIABWebViewActivity : AppCompatActivity() { } closeReceiver = null } + keepAliveConnection?.let { + try { + unbindService(it) + } catch (e: Exception) { + // Service may not be bound, ignore + } + keepAliveConnection = null + } webView.destroy() super.onDestroy() }