From 65c89c6c65f95e24818628df09ea0f61d63e13a0 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 28 Apr 2026 16:27:54 +0200 Subject: [PATCH] bring pwa to front when already running instead of starting a new instance --- .../activities/ExternalAppBrowserActivity.kt | 10 ++ .../activities/IntentReceiverActivity.kt | 130 +++++++++++++++++- 2 files changed, 135 insertions(+), 5 deletions(-) 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 ad16057e..d85667bd 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 @@ -45,11 +45,21 @@ open class ExternalAppBrowserActivity : AppCompatActivity() { context: Context, customTabSessionId: String, webAppManifestUrl: String? = null, + pwaProfileUuid: String? = null, + pwaContextId: String? = null, + pwaToken: String? = null, + pwaInstallStartUrl: String? = null, ): Intent { return Intent(context, ExternalAppBrowserActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, customTabSessionId) webAppManifestUrl?.let { putExtra(EXTRA_WEB_APP_MANIFEST_URL, it) } + pwaProfileUuid?.let { putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, it) } + pwaContextId?.let { putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, it) } + pwaToken?.let { putExtra(PwaConstants.EXTRA_PWA_TOKEN, it) } + pwaInstallStartUrl?.let { + putExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL, it) + } } } } 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 0bae820a..839a9c38 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 @@ -7,7 +7,7 @@ package eu.weblibre.flutter_mozilla_components.activities import android.app.Activity -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import android.app.ActivityManager import android.content.Context import android.content.Intent import android.content.pm.ShortcutManager @@ -15,6 +15,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Log +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 @@ -26,6 +27,7 @@ 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 import mozilla.components.feature.pwa.intent.WebAppIntentProcessor @@ -168,7 +170,7 @@ class IntentReceiverActivity : Activity() { } Log.d(TAG, "Trusted PWA intent with profile metadata: profileUuid=$profileUuid, contextId=$contextId") - handlePwaIntent(intent, profileUuid, contextId) + handlePwaIntent(intent, profileUuid, contextId, token) return } @@ -345,6 +347,7 @@ class IntentReceiverActivity : Activity() { intent: Intent, profileUuid: String, contextId: String?, + token: String?, ) { val url = intent.dataString if (url == null) { @@ -353,18 +356,36 @@ class IntentReceiverActivity : Activity() { return } + val installStartUrl = intent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL) + val currentProfileUuid = getCurrentProfileUuid() if (currentProfileUuid != null && currentProfileUuid != profileUuid) { Log.d(TAG, "Profile mismatch: current=$currentProfileUuid, expected=$profileUuid") showProfileMismatchDialog( intent = intent, - onProceed = { launchPwaWithContext(url, contextId) }, + onProceed = { + launchPwaWithContext( + url = url, + contextId = contextId, + profileUuid = profileUuid, + token = token, + installStartUrl = installStartUrl, + allowTaskReuse = false, + ) + }, isPwa = true, ) } else { Log.d(TAG, "Profile match or indeterminate, launching PWA with contextId=$contextId") - launchPwaWithContext(url, contextId) + launchPwaWithContext( + url = url, + contextId = contextId, + profileUuid = profileUuid, + token = token, + installStartUrl = installStartUrl, + allowTaskReuse = true, + ) } } @@ -441,7 +462,19 @@ class IntentReceiverActivity : Activity() { /** * Launches the PWA with the specified context ID for storage isolation. */ - private fun launchPwaWithContext(url: String, contextId: String?) { + private fun launchPwaWithContext( + url: String, + contextId: String?, + profileUuid: String, + token: String?, + installStartUrl: String?, + allowTaskReuse: Boolean, + ) { + if (allowTaskReuse && bringPwaTaskToFront(url, profileUuid, contextId, token, installStartUrl)) { + finish() + return + } + val components = GlobalComponents.components ?: run { Log.e(TAG, "Components not available for PWA launch") @@ -467,6 +500,10 @@ class IntentReceiverActivity : Activity() { context = this@IntentReceiverActivity, customTabSessionId = sessionId, webAppManifestUrl = url, + pwaProfileUuid = profileUuid, + pwaContextId = contextId, + pwaToken = token, + pwaInstallStartUrl = installStartUrl ?: url, ) startActivity(externalIntent) finish() @@ -477,6 +514,89 @@ class IntentReceiverActivity : Activity() { } } + private fun bringPwaTaskToFront( + url: String, + profileUuid: String, + contextId: String?, + token: String?, + installStartUrl: String?, + ): Boolean { + if (token.isNullOrEmpty()) { + return false + } + + val activityManager = getSystemService(ActivityManager::class.java) ?: return false + val components = GlobalComponents.components + + for (appTask in activityManager.appTasks) { + val baseIntent = runCatching { appTask.taskInfo.baseIntent } + .getOrElse { error -> + Log.w(TAG, "Failed to inspect app task", error) + return@getOrElse null + } ?: continue + + if (baseIntent.component?.className != ExternalAppBrowserActivity::class.java.name) { + continue + } + + if (!matchesPwaTaskIntent(baseIntent, url, profileUuid, contextId, token, installStartUrl, components)) { + continue + } + + return try { + appTask.moveToFront() + Log.d(TAG, "Resumed existing PWA task for $url") + true + } catch (e: Exception) { + Log.e(TAG, "Failed to move existing PWA task to front", e) + false + } + } + + return false + } + + private fun matchesPwaTaskIntent( + baseIntent: Intent, + url: String, + profileUuid: String, + contextId: String?, + token: String, + installStartUrl: String?, + components: Components?, + ): Boolean { + val requestedContextId = contextId.orEmpty() + val requestedInstallStartUrl = installStartUrl ?: url + + val taskToken = baseIntent.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN) + if (!taskToken.isNullOrEmpty()) { + return taskToken == token && + baseIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == profileUuid && + baseIntent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID).orEmpty() == requestedContextId && + baseIntent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL) == requestedInstallStartUrl + } + + val taskManifestUrl = baseIntent.getStringExtra(ExternalAppBrowserActivity.EXTRA_WEB_APP_MANIFEST_URL) + if (taskManifestUrl != requestedInstallStartUrl && taskManifestUrl != url) { + return false + } + + val sessionId = baseIntent.getStringExtra(ExternalAppBrowserActivity.EXTRA_CUSTOM_TAB_SESSION_ID) + ?: return false + val customTab = components?.core?.store?.state?.findCustomTab(sessionId) ?: return false + val appType = customTab.config.externalAppType + val isPwa = appType == ExternalAppType.PROGRESSIVE_WEB_APP || + appType == ExternalAppType.TRUSTED_WEB_ACTIVITY + if (!isPwa) { + return false + } + + val manifestStartUrl = customTab.content.webAppManifest?.startUrl + val launchUrl = manifestStartUrl ?: taskManifestUrl ?: customTab.content.url + return customTab.contextId.orEmpty() == requestedContextId && + (launchUrl == requestedInstallStartUrl || launchUrl == url) + } + /** * Creates a custom tab session for a PWA with the specified context ID. */