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))