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 previousComponents = _components
val previousMode = currentMode 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() previousComponents?.core?.store?.state?.customTabs.orEmpty()
} else { } else {
emptyList() 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.ExternalAppBrowserFragment
import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.PwaConstants import eu.weblibre.flutter_mozilla_components.PwaConstants
import eu.weblibre.flutter_mozilla_components.PwaSessionCreator
import eu.weblibre.flutter_mozilla_components.R import eu.weblibre.flutter_mozilla_components.R
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -66,6 +67,7 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
private val logger = Logger("ExternalAppBrowserActivity") private val logger = Logger("ExternalAppBrowserActivity")
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var isRecoveringPwaSession = false
private val customTabSessionId: String? private val customTabSessionId: String?
get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID) get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID)
@@ -85,17 +87,19 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
finishAndRemoveTask() finishAndRemoveTask()
} }
WindowCompat.setDecorFitsSystemWindows(window, false)
setContentView(R.layout.activity_external_app_browser)
val sessionId = customTabSessionId val sessionId = customTabSessionId
if (sessionId == null) { if (sessionId == null) {
Log.e(TAG, "No custom tab session ID provided") Log.e(TAG, "No custom tab session ID provided")
logger.error("No custom tab session ID provided, finishing.") logger.error("No custom tab session ID provided, finishing.")
fallbackToMainActivity() if (!recoverPwaSession(null, "missing session ID")) {
fallbackToMainActivity()
}
return return
} }
WindowCompat.setDecorFitsSystemWindows(window, false)
setContentView(R.layout.activity_external_app_browser)
val components = GlobalComponents.components val components = GlobalComponents.components
if (components == null) { if (components == null) {
if (GlobalComponents.ensureExternalComponents(applicationContext)) { if (GlobalComponents.ensureExternalComponents(applicationContext)) {
@@ -145,7 +149,9 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
if (components.core.store.state.findCustomTab(sessionId) == null) { if (components.core.store.state.findCustomTab(sessionId) == null) {
Log.e(TAG, "Custom tab session $sessionId not found in store") Log.e(TAG, "Custom tab session $sessionId not found in store")
logger.error("Custom tab session $sessionId not found in store, finishing.") logger.error("Custom tab session $sessionId not found in store, finishing.")
fallbackToMainActivity() if (!recoverPwaSession(sessionId, "session not found in store")) {
fallbackToMainActivity()
}
return return
} }
@@ -168,10 +174,61 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
if (components.core.store.state.findCustomTab(sessionId) == null) { if (components.core.store.state.findCustomTab(sessionId) == null) {
Log.w(TAG, "Custom tab session $sessionId gone on resume") Log.w(TAG, "Custom tab session $sessionId gone on resume")
logger.debug("Custom tab session $sessionId gone, finishing activity.") 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() { private fun fallbackToMainActivity() {
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
setClassName(this@ExternalAppBrowserActivity, "eu.weblibre.gecko.MainActivity") 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.Components
import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.PwaConstants 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.IntentBlockNotifier
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -26,7 +27,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.intent.ext.getSessionId
@@ -475,23 +475,17 @@ class IntentReceiverActivity : Activity() {
return return
} }
val components = GlobalComponents.components if (GlobalComponents.components == null) {
?: run { Log.e(TAG, "Components not available for PWA launch")
Log.e(TAG, "Components not available for PWA launch") handleRegularIntent(intent)
handleRegularIntent(intent) return
return }
}
coroutineScope.launch { coroutineScope.launch {
try { try {
val manifest = withContext(Dispatchers.IO) { val sessionId = PwaSessionCreator.create(
components.core.webAppManifestStorage.loadManifest(url)
}
val sessionId = createPwaSession(
url = url, url = url,
contextId = contextId, contextId = contextId,
manifest = manifest
) )
Log.d(TAG, "Created PWA session: contextId=$contextId, sessionId=$sessionId") Log.d(TAG, "Created PWA session: contextId=$contextId, sessionId=$sessionId")
@@ -597,40 +591,6 @@ class IntentReceiverActivity : Activity() {
(launchUrl == requestedInstallStartUrl || launchUrl == url) (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. * Extracts a URL from a SEND intent.
* First checks EXTRA_TEXT for a URL, then EXTRA_STREAM. * First checks EXTRA_TEXT for a URL, then EXTRA_STREAM.