improved pwa lifecycle

This commit is contained in:
Fabian Freund
2026-04-30 06:16:06 +02:00
parent 66160500e1
commit 28dc4e93f1
4 changed files with 124 additions and 54 deletions
@@ -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()
@@ -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
}
}
}
@@ -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")
@@ -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.