From 8fe34158ff6325172943f5534b3ac04f8faa53ce Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 28 Apr 2026 12:46:11 +0200 Subject: [PATCH] improve intent handling --- .../android/app/src/main/AndroidManifest.xml | 23 ++--- .../BaseBrowserFragment.kt | 3 +- .../ExternalAppBrowserFragment.kt | 3 + .../activities/IntentReceiverActivity.kt | 96 ++++++++++++++++++- .../api/GeckoAppLinksApiImpl.kt | 8 +- 5 files changed, 116 insertions(+), 17 deletions(-) diff --git a/apps/weblibre/android/app/src/main/AndroidManifest.xml b/apps/weblibre/android/app/src/main/AndroidManifest.xml index 4eebc3fc..6accd9bf 100644 --- a/apps/weblibre/android/app/src/main/AndroidManifest.xml +++ b/apps/weblibre/android/app/src/main/AndroidManifest.xml @@ -108,17 +108,7 @@ - - - - - - - - - - - + @@ -170,6 +160,17 @@ + + + + + + + + + + + diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt index 6e38968c..bd87ddfd 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt @@ -344,7 +344,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit val appLinksUseCases = components.useCases.appLinksUseCases val getRedirect = appLinksUseCases.appLinkRedirect val redirect = getRedirect.invoke(fallbackUrl) - redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK + redirect.appIntent?.flags = + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK appLinksUseCases.openAppLink.invoke(redirect.appIntent) } }, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ExternalAppBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ExternalAppBrowserFragment.kt index 14c30095..7f9bc368 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ExternalAppBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ExternalAppBrowserFragment.kt @@ -295,6 +295,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler type = "text/plain" putExtra(Intent.EXTRA_TEXT, tab.content.url) putExtra(Intent.EXTRA_SUBJECT, tab.content.title) + // Use NEW_DOCUMENT + MULTIPLE_TASK so the receiving app opens + // in its own task rather than inside WebLibre's recents entry. + flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } startActivity(Intent.createChooser(shareIntent, null)) } 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 dcc2427c..c8b73b71 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 @@ -33,11 +33,12 @@ import mozilla.components.browser.state.state.ExternalAppType import java.io.File /** - * Lightweight transparent activity that receives all ACTION_VIEW intents and routes them + * Lightweight transparent activity that receives all external intents and routes them * to the appropriate activity: * - Custom Tab intents → [ExternalAppBrowserActivity] * - PWA launch intents → [ExternalAppBrowserActivity] (with profile/context tracking) - * - Regular VIEW intents → MainActivity (Flutter) + * - SHARE intents with a URL → [ExternalAppBrowserActivity] (separate recents entry) + * - Regular VIEW / SEND / PROCESS_TEXT / WEB_SEARCH intents → MainActivity (Flutter) * * For PWA intents created by our custom installer, checks profile match and shows dialog * if the current profile differs from the installation profile. @@ -135,6 +136,23 @@ class IntentReceiverActivity : Activity() { val privateBrowsingMode = intent.getBooleanExtra(PRIVATE_BROWSING_MODE, false) intent.putExtra(PRIVATE_BROWSING_MODE, privateBrowsingMode) + // SHARE intents with a URL are routed to ExternalAppBrowserActivity so they + // appear as a separate recents entry ("share as new task"), matching the + // behaviour users expect when sharing content into the browser. SEND intents + // without a URL (plain text, files) fall through to MainActivity for the + // Flutter-side to handle (search, PDF display, etc.). + if (intent.action == Intent.ACTION_SEND) { + val shareUrl = extractShareUrl(intent) + if (shareUrl != null) { + Log.d(TAG, "SHARE intent with URL, routing to custom tab: $shareUrl") + handleShareUrlAsCustomTab(intent, shareUrl, privateBrowsingMode) + return + } + Log.d(TAG, "SHARE intent without URL, routing to MainActivity") + handleRegularIntent(intent) + return + } + // Check if this is our custom PWA intent with profile metadata val profileUuid = intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID) @@ -493,6 +511,80 @@ class IntentReceiverActivity : Activity() { return tab.id } + /** + * Extracts a URL from a SEND intent. + * First checks EXTRA_TEXT for a URL, then EXTRA_STREAM. + * Returns null if no valid URL is found. + */ + private fun extractShareUrl(intent: Intent): String? { + fun String?.toHttpUrl(): String? { + val candidate = this?.trim().orEmpty() + return candidate.takeIf { + it.startsWith("http://") || it.startsWith("https://") + } + } + + intent.getStringExtra(Intent.EXTRA_TEXT)?.toHttpUrl()?.let { return it } + + @Suppress("DEPRECATION") + val streamUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_STREAM) + streamUri?.toString().toHttpUrl()?.let { return it } + + intent.dataString.toHttpUrl()?.let { return it } + + return null + } + + /** + * Creates a custom tab session for the shared URL and launches + * [ExternalAppBrowserActivity], which appears as a separate recents entry + * because it uses taskAffinity="" and documentLaunchMode="always". + */ + private fun handleShareUrlAsCustomTab( + sourceIntent: Intent, + url: String, + privateBrowsingMode: Boolean, + ) { + var components = GlobalComponents.components + if (components == null) { + if (!GlobalComponents.ensureExternalComponents(applicationContext)) { + Log.w(TAG, "Components not initialized, falling back to MainActivity for SHARE") + handleRegularIntent(sourceIntent) + return + } + components = GlobalComponents.components + } + + if (components == null) { + Log.e(TAG, "Components still null after init, falling back to MainActivity for SHARE") + handleRegularIntent(sourceIntent) + return + } + + val customTabConfig = mozilla.components.browser.state.state.CustomTabConfig() + + val tab = mozilla.components.browser.state.state.createCustomTab( + url = url, + config = customTabConfig, + source = mozilla.components.browser.state.state.SessionState.Source.Internal.CustomTab, + private = privateBrowsingMode, + ) + + 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) + + val externalIntent = ExternalAppBrowserActivity.createIntent( + context = this, + customTabSessionId = tab.id, + ) + startActivity(externalIntent) + finish() + } + private fun handleRegularIntent(intent: Intent) { val mainActivityIntent = Intent(intent).apply { setClassName(this@IntentReceiverActivity, "eu.weblibre.gecko.MainActivity") diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAppLinksApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAppLinksApiImpl.kt index 08d92af9..d7b47053 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAppLinksApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAppLinksApiImpl.kt @@ -54,9 +54,11 @@ class GeckoAppLinksApiImpl( return@launch } - // Set FLAG_ACTIVITY_NEW_TASK to launch in new task - // This prevents issues with app task stacks - redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK + // Use NEW_DOCUMENT + MULTIPLE_TASK so the target app opens in its own + // task and doesn't get absorbed into WebLibre's recents entry. + // This matches Fenix's ShareController behaviour. + redirect.appIntent?.flags = + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK components.useCases.appLinksUseCases.openAppLink.invoke(redirect.appIntent) callback(Result.success(true))