diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt index a910960e..2be1187f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt @@ -14,6 +14,7 @@ object PwaConstants { const val EXTRA_PWA_PROFILE_UUID = "pwa_profile_uuid" const val EXTRA_PWA_CONTEXT_ID = "pwa_context_id" const val EXTRA_PWA_TOKEN = "pwa_token" + const val EXTRA_PWA_INSTALL_START_URL = "pwa_install_start_url" // Profile and file paths const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile" 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 c7f879f8..fec20043 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 @@ -10,6 +10,8 @@ import android.app.Activity import android.app.AlertDialog import android.content.Context import android.content.Intent +import android.content.pm.ShortcutManager +import android.os.Build import android.os.Bundle import android.util.Log import eu.weblibre.flutter_mozilla_components.Components @@ -147,7 +149,8 @@ class IntentReceiverActivity : Activity() { } private fun isTrustedPwaLaunch(intent: Intent, profileUuid: String, token: String?): Boolean { - val url = intent.dataString ?: return false + val intentUrl = intent.dataString ?: return false + val installStartUrl = intent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL) val action = intent.action val hasTrustedAction = action == Intent.ACTION_VIEW || action == "mozilla.components.feature.pwa.VIEW_PWA" if (!hasTrustedAction || token.isNullOrEmpty()) { @@ -158,14 +161,53 @@ class IntentReceiverActivity : Activity() { PwaConstants.PROFILE_MAPPING_PREFS, Context.MODE_PRIVATE, ) - val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${url}::${profileUuid}" - val storedToken = prefs.getString(tokenKey, null) - if (storedToken == null || storedToken != token) { - Log.w(TAG, "PWA token mismatch for $url") + val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${intentUrl}::${profileUuid}" + if (prefs.getString(tokenKey, null) == token) { + return true + } + + if (!installStartUrl.isNullOrEmpty() && installStartUrl != intentUrl) { + val installTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${installStartUrl}::${profileUuid}" + if (prefs.getString(installTokenKey, null) == token) { + return true + } + } + + if (isPinnedShortcutTokenMatch(intentUrl, profileUuid, token, installStartUrl)) { + return true + } + + Log.w(TAG, "PWA token mismatch for $intentUrl") + return false + } + + private fun isPinnedShortcutTokenMatch( + intentUrl: String, + profileUuid: String, + token: String, + installStartUrl: String?, + ): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return false } - return true + val shortcutManager = getSystemService(ShortcutManager::class.java) ?: return false + return shortcutManager.pinnedShortcuts.any { shortcut -> + val shortcutIntent = shortcut.intent ?: return@any false + if (shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) != profileUuid) { + return@any false + } + + if (shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN) != token) { + return@any false + } + + val shortcutUrl = shortcutIntent.dataString + val shortcutInstallUrl = shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL) + shortcutUrl == intentUrl || + (shortcutInstallUrl != null && shortcutInstallUrl == intentUrl) || + (!installStartUrl.isNullOrEmpty() && shortcutInstallUrl == installStartUrl && (shortcutUrl == intentUrl || shortcutUrl == installStartUrl)) + } } private fun resolveSessionIdFromStore( diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt index 5805c1c4..9b280ec0 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt @@ -145,7 +145,12 @@ class GeckoPwaApiImpl( val (iconBitmap, isMaskable) = loadPwaIcon(manifest) val shortcutId = generateShortcutId(manifest.startUrl) - val launchToken = generateAndStoreLaunchToken(manifest.startUrl, profileUuid) + val launchToken = resolveLaunchToken( + shortcutManager = shortcutManager, + shortcutId = shortcutId, + startUrl = manifest.startUrl, + profileUuid = profileUuid, + ) val appName = manifest.shortName ?: manifest.name ?: "Web App" @@ -155,6 +160,7 @@ class GeckoPwaApiImpl( putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, profileUuid) putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId) putExtra(PwaConstants.EXTRA_PWA_TOKEN, launchToken) + putExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL, manifest.startUrl) } val shortcut = ShortcutInfo.Builder(context, shortcutId).apply { @@ -261,16 +267,55 @@ class GeckoPwaApiImpl( .apply() } - private fun generateAndStoreLaunchToken(startUrl: String, profileUuid: String): String { - val token = UUID.randomUUID().toString() - val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}" - val committed = appPrefs.edit() - .putString(tokenKey, token) - .commit() + private fun resolveLaunchToken( + shortcutManager: ShortcutManager, + shortcutId: String, + startUrl: String, + profileUuid: String, + ): String { + val storedToken = getStoredLaunchToken(startUrl, profileUuid) + val existingShortcutToken = shortcutManager.pinnedShortcuts + .firstOrNull { shortcut -> shortcut.id == shortcutId } + ?.intent + ?.takeIf { shortcutIntent -> + shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == profileUuid + } + ?.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN) + + if (!existingShortcutToken.isNullOrEmpty()) { + val committed = storeLaunchToken(startUrl, profileUuid, existingShortcutToken) + if (!committed) { + logger.warn("Failed to persist pinned shortcut PWA token for $startUrl") + } + return existingShortcutToken + } + + if (!storedToken.isNullOrEmpty()) { + val committed = storeLaunchToken(startUrl, profileUuid, storedToken) + if (!committed) { + logger.warn("Failed to refresh stored PWA launch token index for $startUrl") + } + return storedToken + } + + val generatedToken = UUID.randomUUID().toString() + val committed = storeLaunchToken(startUrl, profileUuid, generatedToken) if (!committed) { logger.warn("Failed to persist PWA launch token for $startUrl") } - return token + return generatedToken + } + + private fun getStoredLaunchToken(startUrl: String, profileUuid: String): String? { + val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}" + return appPrefs.getString(tokenKey, null) + } + + private fun storeLaunchToken(startUrl: String, profileUuid: String, token: String): Boolean { + val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}" + return appPrefs.edit() + .putString(tokenKey, token) + .commit() } private fun getProfileMapping(startUrl: String): String? {