diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt index 30f46e99..ce8b66c2 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt @@ -163,7 +163,9 @@ object GlobalComponents { val previousComponents = _components val previousMode = currentMode - val previousCustomTabs = if (previousMode == ComponentsMode.EXTERNAL) { + val isSameProfile = previousComponents?.profileApplicationContext?.relativePath == + applicationContext.relativePath + val previousCustomTabs = if (isSameProfile) { previousComponents?.core?.store?.state?.customTabs.orEmpty() } else { emptyList() diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSessionCreator.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSessionCreator.kt new file mode 100644 index 00000000..e3ca0b02 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSessionCreator.kt @@ -0,0 +1,51 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package eu.weblibre.flutter_mozilla_components + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.ExternalAppType +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.concept.engine.EngineSession + +object PwaSessionCreator { + suspend fun create(url: String, contextId: String?): String { + val components = GlobalComponents.components + ?: throw IllegalStateException("Components not initialized") + + val manifest = withContext(Dispatchers.IO) { + components.core.webAppManifestStorage.loadManifest(url) + } + + return withContext(Dispatchers.Main) { + val customTabConfig = CustomTabConfig( + externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP, + ) + + val tab = createCustomTab( + url = url, + contextId = contextId, + config = customTabConfig, + webAppManifest = manifest, + source = SessionState.Source.Internal.CustomTab, + private = false, + ) + + components.core.store.dispatch( + CustomTabListAction.AddCustomTabAction(tab), + ) + + val loadUrlFlags = EngineSession.LoadUrlFlags.external() + components.useCases.sessionUseCases.loadUrl(url, tab.id, loadUrlFlags) + + tab.id + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt index d85667bd..90906c44 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt @@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.PwaConstants +import eu.weblibre.flutter_mozilla_components.PwaSessionCreator import eu.weblibre.flutter_mozilla_components.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -66,6 +67,7 @@ open class ExternalAppBrowserActivity : AppCompatActivity() { private val logger = Logger("ExternalAppBrowserActivity") private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private var isRecoveringPwaSession = false private val customTabSessionId: String? get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID) @@ -85,17 +87,19 @@ open class ExternalAppBrowserActivity : AppCompatActivity() { finishAndRemoveTask() } + WindowCompat.setDecorFitsSystemWindows(window, false) + setContentView(R.layout.activity_external_app_browser) + val sessionId = customTabSessionId if (sessionId == null) { Log.e(TAG, "No custom tab session ID provided") logger.error("No custom tab session ID provided, finishing.") - fallbackToMainActivity() + if (!recoverPwaSession(null, "missing session ID")) { + fallbackToMainActivity() + } return } - WindowCompat.setDecorFitsSystemWindows(window, false) - setContentView(R.layout.activity_external_app_browser) - val components = GlobalComponents.components if (components == null) { if (GlobalComponents.ensureExternalComponents(applicationContext)) { @@ -145,7 +149,9 @@ open class ExternalAppBrowserActivity : AppCompatActivity() { if (components.core.store.state.findCustomTab(sessionId) == null) { Log.e(TAG, "Custom tab session $sessionId not found in store") logger.error("Custom tab session $sessionId not found in store, finishing.") - fallbackToMainActivity() + if (!recoverPwaSession(sessionId, "session not found in store")) { + fallbackToMainActivity() + } return } @@ -168,10 +174,61 @@ open class ExternalAppBrowserActivity : AppCompatActivity() { if (components.core.store.state.findCustomTab(sessionId) == null) { Log.w(TAG, "Custom tab session $sessionId gone on resume") logger.debug("Custom tab session $sessionId gone, finishing activity.") - fallbackToMainActivity() + if (!recoverPwaSession(sessionId, "session gone on resume")) { + fallbackToMainActivity() + } } } + private fun recoverPwaSession(missingSessionId: String?, reason: String): Boolean { + if (isRecoveringPwaSession) { + return true + } + + val launchUrl = webAppManifestUrl + ?: intent?.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL) + ?: return false + + val isPwaTask = webAppManifestUrl != null || + intent?.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == true + if (!isPwaTask) { + return false + } + + val contextId = intent?.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID) + isRecoveringPwaSession = true + Log.w(TAG, "Recovering PWA session for $launchUrl: $reason") + + coroutineScope.launch { + try { + val sessionId = PwaSessionCreator.create(launchUrl, contextId) + Log.d( + TAG, + "Recovered PWA session: old=$missingSessionId, new=$sessionId, url=$launchUrl", + ) + + intent.putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, sessionId) + if (webAppManifestUrl == null) { + intent.putExtra(EXTRA_WEB_APP_MANIFEST_URL, launchUrl) + } + + if (!isFinishing && !isDestroyed) { + showFragment(sessionId) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to recover PWA session", e) + logger.error("Failed to recover PWA session, removing stale task.", e) + if (!isFinishing && !isDestroyed) { + finishAndRemoveTask() + } + } finally { + isRecoveringPwaSession = false + } + } + + return true + } + private fun fallbackToMainActivity() { val mainIntent = Intent().apply { setClassName(this@ExternalAppBrowserActivity, "eu.weblibre.gecko.MainActivity") diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt index 839a9c38..6b46e03c 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt @@ -19,6 +19,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.weblibre.flutter_mozilla_components.Components import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.PwaConstants +import eu.weblibre.flutter_mozilla_components.PwaSessionCreator import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentBlockNotifier import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences import kotlinx.coroutines.CoroutineScope @@ -26,7 +27,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.intent.ext.getSessionId @@ -475,23 +475,17 @@ class IntentReceiverActivity : Activity() { return } - val components = GlobalComponents.components - ?: run { - Log.e(TAG, "Components not available for PWA launch") - handleRegularIntent(intent) - return - } + if (GlobalComponents.components == null) { + Log.e(TAG, "Components not available for PWA launch") + handleRegularIntent(intent) + return + } coroutineScope.launch { try { - val manifest = withContext(Dispatchers.IO) { - components.core.webAppManifestStorage.loadManifest(url) - } - - val sessionId = createPwaSession( + val sessionId = PwaSessionCreator.create( url = url, contextId = contextId, - manifest = manifest ) Log.d(TAG, "Created PWA session: contextId=$contextId, sessionId=$sessionId") @@ -597,40 +591,6 @@ class IntentReceiverActivity : Activity() { (launchUrl == requestedInstallStartUrl || launchUrl == url) } - /** - * Creates a custom tab session for a PWA with the specified context ID. - */ - private fun createPwaSession( - url: String, - contextId: String?, - manifest: mozilla.components.concept.engine.manifest.WebAppManifest? - ): String { - val components = GlobalComponents.components - ?: throw IllegalStateException("Components not initialized") - - val customTabConfig = mozilla.components.browser.state.state.CustomTabConfig( - externalAppType = mozilla.components.browser.state.state.ExternalAppType.PROGRESSIVE_WEB_APP - ) - - val tab = mozilla.components.browser.state.state.createCustomTab( - url = url, - contextId = contextId, - config = customTabConfig, - webAppManifest = manifest, - source = mozilla.components.browser.state.state.SessionState.Source.Internal.CustomTab, - private = false - ) - - components.core.store.dispatch( - mozilla.components.browser.state.action.CustomTabListAction.AddCustomTabAction(tab) - ) - - val loadUrlFlags = mozilla.components.concept.engine.EngineSession.LoadUrlFlags.external() - components.useCases.sessionUseCases.loadUrl(url, tab.id, loadUrlFlags) - - return tab.id - } - /** * Extracts a URL from a SEND intent. * First checks EXTRA_TEXT for a URL, then EXTRA_STREAM.