summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/app-links
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
commitda4c7e7ed675c3bf405668739c3012d140856109 (patch)
treecdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/feature/app-links
parentAdding upstream version 125.0.3. (diff)
downloadfirefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz
firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/feature/app-links')
-rw-r--r--mobile/android/android-components/components/feature/app-links/README.md40
-rw-r--r--mobile/android/android-components/components/feature/app-links/build.gradle60
-rw-r--r--mobile/android/android-components/components/feature/app-links/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinkRedirect.kt41
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt184
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksInterceptor.kt237
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt318
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt34
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt109
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-am/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-an/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ar/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ast/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-az/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-azb/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-be/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-bg/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-bn/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-br/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-bs/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ca/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-cak/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ceb/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ckb/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-co/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-cs/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-cy/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-da/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-de/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-dsb/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-el/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rCA/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rGB/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-eo/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rAR/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rCL/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rES/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rMX/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-es/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-et/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-eu/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-fa/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ff/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-fi/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-fr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-fur/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-fy-rNL/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ga-rIE/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-gd/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-gl/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-gn/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-gu-rIN/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hi-rIN/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hil/strings.xml9
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hsb/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hu/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-hy-rAM/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ia/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-in/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-is/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-it/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-iw/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ja/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ka/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-kaa/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-kab/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-kk/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-kmr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-kn/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ko/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-lij/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-lo/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-lt/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-mix/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ml/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-mr/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-my/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-nb-rNO/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ne-rNP/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-nl/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-nn-rNO/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-oc/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-or/strings.xml9
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rIN/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rPK/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-pl/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rBR/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rPT/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-rm/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ro/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ru/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sat/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sc/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-si/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sk/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-skr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sl/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sq/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-su/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-sv-rSE/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ta/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-te/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-tg/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-th/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-tl/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-tr/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-trs/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-tt/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-tzm/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ug/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-uk/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-ur/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-uz/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-vec/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-vi/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-yo/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rCN/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rTW/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/main/res/values/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinkRedirectTest.kt77
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt330
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksInterceptorTest.kt679
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt671
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragmentTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/app-links/src/test/resources/robolectric.properties1
129 files changed, 4576 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/app-links/README.md b/mobile/android/android-components/components/feature/app-links/README.md
new file mode 100644
index 0000000000..a51afe251e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/README.md
@@ -0,0 +1,40 @@
+# [Android Components](../../../README.md) > Feature > App-Links
+
+A session component to support opening non-browser apps and `intent://` style URLs.
+
+## Usage
+
+From a `BrowserFragment`:
+```kotlin
+// Start listening to the intercepted and offer to open app banners
+AppLinksFeature(
+ context = context,
+ sessionManager = sessionManager,
+ sessionId = customSessionId,
+ fragmentManager = fragmentManager
+)
+```
+
+From elsewhere in the app:
+```kotlin
+val redirect = AppLinksUseCases.appLinkRedirect.invoke(redirect)
+
+if (redirect.isExternalApp()) {
+ AppLinkUseCases.openAppLink(redirect)
+}
+```
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/)
+ ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:feature-app-links:{latest-version}"
+```
+
+## License
+
+ 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 http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/feature/app-links/build.gradle b/mobile/android/android-components/components/feature/app-links/build.gradle
new file mode 100644
index 0000000000..71f94f3bc8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/build.gradle
@@ -0,0 +1,60 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.google.devtools.ksp'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ resources {
+ excludes += ['META-INF/proguard/androidx-annotations.pro']
+ }
+ }
+
+ namespace 'mozilla.components.feature.app.links'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':feature-session')
+ implementation project(':ui-widgets')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/feature/app-links/proguard-rules.pro b/mobile/android/android-components/components/feature/app-links/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/app-links/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinkRedirect.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinkRedirect.kt
new file mode 100644
index 0000000000..e0f6f7f76b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinkRedirect.kt
@@ -0,0 +1,41 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.Intent
+
+/**
+ * Data class for the external Intent or fallback URL a given URL encodes for.
+ */
+data class AppLinkRedirect(
+ val appIntent: Intent?,
+ val fallbackUrl: String?,
+ val marketplaceIntent: Intent?,
+) {
+ /**
+ * If there is a third-party app intent.
+ */
+ fun hasExternalApp() = appIntent != null
+
+ /**
+ * If there is a fallback URL (should the intent fails).
+ */
+ fun hasFallback() = fallbackUrl != null
+
+ /**
+ * If there is a marketplace intent (should the external app is not installed).
+ */
+ fun hasMarketplaceIntent() = marketplaceIntent != null
+
+ /**
+ * If the app link is a redirect (to an app or URL).
+ */
+ fun isRedirect() = hasExternalApp() || hasFallback() || hasMarketplaceIntent()
+
+ /**
+ * Is the app link one that can be installed from a store.
+ */
+ fun isInstallable() = appIntent?.data?.scheme == "market"
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt
new file mode 100644
index 0000000000..3d02265a3a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt
@@ -0,0 +1,184 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.FragmentManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.EXTERNAL
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE
+import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ENGINE_SUPPORTED_SCHEMES
+import mozilla.components.feature.app.links.RedirectDialogFragment.Companion.FRAGMENT_TAG
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.content.appName
+
+/**
+ * This feature implements observer for handling redirects to external apps. The users are asked to
+ * confirm their intention before leaving the app if in private session. These include the Android
+ * Intents, custom schemes and support for [Intent.CATEGORY_BROWSABLE] `http(s)` URLs.
+ *
+ * It requires: a [Context], and a [FragmentManager].
+ *
+ * @param context Context the feature is associated with.
+ * @param store Reference to the application's [BrowserStore].
+ * @param sessionId The session ID to observe.
+ * @param fragmentManager FragmentManager for interacting with fragments.
+ * @param dialog The dialog for redirect.
+ * @param launchInApp If {true} then launch app links in third party app(s). Default to false because
+ * of security concerns.
+ * @param useCases These use cases allow for the detection of, and opening of links that other apps
+ * have registered to open.
+ * @param failedToLaunchAction Action to perform when failing to launch in third party app.
+ * @param loadUrlUseCase Used to load URL if user decides not to launch in third party app.
+ **/
+class AppLinksFeature(
+ private val context: Context,
+ private val store: BrowserStore,
+ private val sessionId: String? = null,
+ private val fragmentManager: FragmentManager? = null,
+ private val dialog: RedirectDialogFragment? = null,
+ private val launchInApp: () -> Boolean = { false },
+ private val useCases: AppLinksUseCases = AppLinksUseCases(context, launchInApp),
+ private val failedToLaunchAction: (fallbackUrl: String?) -> Unit = {},
+ private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase? = null,
+ private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
+ private val shouldPrompt: () -> Boolean = { true },
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Starts observing app links on the selected session.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(sessionId) }
+ .distinctUntilChangedBy {
+ it.content.appIntent
+ }
+ .collect { tab ->
+ tab.content.appIntent?.let {
+ handleAppIntent(tab, it.url, it.appIntent)
+ store.dispatch(ContentAction.ConsumeAppIntentAction(tab.id))
+ }
+ }
+ }
+
+ findPreviousDialogFragment()?.let {
+ fragmentManager?.beginTransaction()?.remove(it)?.commit()
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun handleAppIntent(tab: SessionState, url: String, appIntent: Intent?) {
+ if (appIntent == null) {
+ return
+ }
+
+ val doNotOpenApp = {
+ AppLinksInterceptor.addUserDoNotIntercept(url, appIntent)
+
+ loadUrlIfSchemeSupported(tab, url)
+ }
+
+ val doOpenApp = {
+ useCases.openAppLink(
+ appIntent,
+ failedToLaunchAction = failedToLaunchAction,
+ )
+ }
+
+ @Suppress("ComplexCondition")
+ if (isSameCallerAndApp(tab, appIntent) || (!tab.content.private && !shouldPrompt()) ||
+ fragmentManager == null
+ ) {
+ doOpenApp()
+ return
+ }
+
+ val dialog = getOrCreateDialog(tab.content.private, url)
+ dialog.onConfirmRedirect = doOpenApp
+ dialog.onCancelRedirect = doNotOpenApp
+
+ if (!isAlreadyADialogCreated()) {
+ dialog.showNow(fragmentManager, FRAGMENT_TAG)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun getOrCreateDialog(isPrivate: Boolean, url: String): RedirectDialogFragment {
+ if (dialog != null) {
+ return dialog
+ }
+
+ val message = context.getString(
+ R.string.mozac_feature_applinks_normal_confirm_dialog_message,
+ context.appName,
+ )
+
+ return SimpleRedirectDialogFragment.newInstance(
+ dialogTitleText = if (isPrivate) {
+ R.string.mozac_feature_applinks_confirm_dialog_title
+ } else {
+ R.string.mozac_feature_applinks_normal_confirm_dialog_title
+ },
+ dialogMessageString = if (isPrivate) {
+ url
+ } else {
+ message
+ },
+ positiveButtonText = R.string.mozac_feature_applinks_confirm_dialog_confirm,
+ negativeButtonText = R.string.mozac_feature_applinks_confirm_dialog_deny,
+ )
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun loadUrlIfSchemeSupported(tab: SessionState, url: String) {
+ val schemeSupported = engineSupportedSchemes.contains(Uri.parse(url).scheme)
+ if (schemeSupported) {
+ loadUrlUseCase?.invoke(
+ url = url,
+ sessionId = tab.id,
+ flags = EngineSession.LoadUrlFlags.select(EXTERNAL, LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE),
+ )
+ }
+ }
+
+ private fun isAlreadyADialogCreated(): Boolean {
+ return findPreviousDialogFragment() != null
+ }
+
+ private fun findPreviousDialogFragment(): RedirectDialogFragment? {
+ return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? RedirectDialogFragment
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun isSameCallerAndApp(tab: SessionState, appIntent: Intent): Boolean {
+ return (tab.source as? SessionState.Source.External)?.let { externalSource ->
+ when (externalSource.caller?.packageId) {
+ null -> false
+ appIntent.component?.packageName -> true
+ else -> false
+ }
+ } ?: false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksInterceptor.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksInterceptor.kt
new file mode 100644
index 0000000000..66880a5935
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksInterceptor.kt
@@ -0,0 +1,237 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.SystemClock
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ALWAYS_DENY_SCHEMES
+import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ENGINE_SUPPORTED_SCHEMES
+import mozilla.components.support.ktx.android.net.isHttpOrHttps
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+
+private const val WWW = "www."
+private const val M = "m."
+private const val MOBILE = "mobile."
+private const val MAPS = "maps."
+
+/**
+ * This feature implements use cases for detecting and handling redirects to external apps. The user
+ * is asked to confirm her intention before leaving the app. These include the Android Intents,
+ * custom schemes and support for [Intent.CATEGORY_BROWSABLE] `http(s)` URLs.
+ *
+ * In the case of Android Intents that are not installed, and with no fallback, the user is prompted
+ * to search the installed market place.
+ *
+ * It provides use cases to detect and open links openable in third party non-browser apps.
+ *
+ * It requires: a [Context].
+ *
+ * A [Boolean] flag is provided at construction to allow the feature and use cases to be landed without
+ * adjoining UI. The UI will be activated in https://github.com/mozilla-mobile/android-components/issues/2974
+ * and https://github.com/mozilla-mobile/android-components/issues/2975.
+ *
+ * @param context Context the feature is associated with.
+ * @param interceptLinkClicks If {true} then intercept link clicks.
+ * @param engineSupportedSchemes List of schemes that the engine supports.
+ * @param alwaysDeniedSchemes List of schemes that will never be opened in a third-party app even if
+ * [interceptLinkClicks] is `true`.
+ * @param launchInApp If {true} then launch app links in third party app(s). Default to false because
+ * of security concerns.
+ * @param useCases These use cases allow for the detection of, and opening of links that other apps
+ * have registered to open.
+ * @param launchFromInterceptor If {true} then the interceptor will launch the link in third-party apps if available.
+ */
+class AppLinksInterceptor(
+ private val context: Context,
+ private val interceptLinkClicks: Boolean = false,
+ private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
+ private val alwaysDeniedSchemes: Set<String> = ALWAYS_DENY_SCHEMES,
+ private var launchInApp: () -> Boolean = { false },
+ private val useCases: AppLinksUseCases = AppLinksUseCases(
+ context,
+ launchInApp,
+ alwaysDeniedSchemes = alwaysDeniedSchemes,
+ ),
+ private val launchFromInterceptor: Boolean = false,
+) : RequestInterceptor {
+
+ /**
+ * Update launchInApp for this instance of AppLinksInterceptor
+ * @param launchInApp the new value of launchInApp
+ */
+ fun updateLaunchInApp(launchInApp: () -> Boolean) {
+ this.launchInApp = launchInApp
+ useCases.updateLaunchInApp(launchInApp)
+ }
+
+ @Suppress("ComplexMethod", "ReturnCount")
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ val encodedUri = Uri.parse(uri)
+ val uriScheme = encodedUri.scheme
+ val engineSupportsScheme = engineSupportedSchemes.contains(uriScheme)
+ val isAllowedRedirect = (isRedirect && !isSubframeRequest)
+
+ val doNotIntercept = when {
+ uriScheme == null -> true
+ // A subframe request not triggered by the user should not go to an external app.
+ (!hasUserGesture && isSubframeRequest) -> true
+ // If request not from an user gesture, allowed redirect and direct navigation
+ // or if we're already on the site then let's not go to an external app.
+ (
+ (!hasUserGesture && !isAllowedRedirect && !isDirectNavigation) ||
+ isSameDomain(lastUri, uri)
+ ) && engineSupportsScheme -> true
+ // If scheme not in safelist then follow user preference
+ (!interceptLinkClicks || !launchInApp()) && engineSupportsScheme -> true
+ // Never go to an external app when scheme is in blocklist
+ alwaysDeniedSchemes.contains(uriScheme) -> true
+ else -> false
+ }
+
+ if (doNotIntercept) {
+ return null
+ }
+
+ val redirect = useCases.interceptedAppLinkRedirect(uri)
+ val result = handleRedirect(redirect, uri, engineSupportedSchemes.contains(uriScheme))
+
+ if (redirect.hasExternalApp()) {
+ val packageName = redirect.appIntent?.component?.packageName
+
+ if (
+ lastApplinksPackageWithTimestamp.first == packageName && lastApplinksPackageWithTimestamp.second +
+ APP_LINKS_DO_NOT_INTERCEPT_INTERVAL > SystemClock.elapsedRealtime()
+ ) {
+ return null
+ }
+
+ lastApplinksPackageWithTimestamp = Pair(packageName, SystemClock.elapsedRealtime())
+ }
+
+ if (redirect.isRedirect()) {
+ if (launchFromInterceptor && result is RequestInterceptor.InterceptionResponse.AppIntent) {
+ result.appIntent.flags = result.appIntent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
+ useCases.openAppLink(result.appIntent)
+ }
+
+ return result
+ }
+
+ return null
+ }
+
+ @SuppressWarnings("ReturnCount")
+ @SuppressLint("MissingPermission")
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun handleRedirect(
+ redirect: AppLinkRedirect,
+ uri: String,
+ schemeSupported: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ if (!launchInApp() || inUserDoNotIntercept(uri, redirect.appIntent)) {
+ redirect.fallbackUrl?.let {
+ return RequestInterceptor.InterceptionResponse.Url(it)
+ }
+ }
+
+ if (schemeSupported && inUserDoNotIntercept(uri, redirect.appIntent)) {
+ return null
+ }
+
+ if (!redirect.hasExternalApp()) {
+ redirect.marketplaceIntent?.let {
+ return RequestInterceptor.InterceptionResponse.AppIntent(it, uri)
+ }
+
+ redirect.fallbackUrl?.let {
+ return RequestInterceptor.InterceptionResponse.Url(it)
+ }
+
+ return null
+ }
+
+ redirect.appIntent?.let {
+ return RequestInterceptor.InterceptionResponse.AppIntent(it, uri)
+ }
+
+ return null
+ }
+
+ // Determines if the transition between the two URLs is related. If the two URLs
+ // are from the same website then the app links interceptor will not try to find an application to open it.
+ @VisibleForTesting
+ internal fun isSameDomain(url1: String?, url2: String?): Boolean {
+ return stripCommonSubDomains(url1?.tryGetHostFromUrl()) == stripCommonSubDomains(url2?.tryGetHostFromUrl())
+ }
+
+ // Remove subdomains that are ignored when determining if two URLs are from the same website.
+ private fun stripCommonSubDomains(url: String?): String? {
+ return when {
+ url == null -> return null
+ url.startsWith(WWW) -> url.replaceFirst(WWW, "")
+ url.startsWith(M) -> url.replaceFirst(M, "")
+ url.startsWith(MOBILE) -> url.replaceFirst(MOBILE, "")
+ url.startsWith(MAPS) -> url.replaceFirst(MAPS, "")
+ else -> url
+ }
+ }
+
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var userDoNotInterceptCache: MutableMap<Int, Long> = mutableMapOf()
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var lastApplinksPackageWithTimestamp: Pair<String?, Long> = Pair(null, 0L)
+
+ @VisibleForTesting
+ internal fun getCacheKey(url: String, appIntent: Intent?): Int? {
+ return Uri.parse(url)?.let { uri ->
+ when {
+ appIntent?.component?.packageName != null -> appIntent.component?.packageName
+ !uri.isHttpOrHttps -> uri.scheme
+ else -> uri.host // worst case we do not prompt again on this host
+ }.hashCode()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun inUserDoNotIntercept(url: String, appIntent: Intent?): Boolean {
+ val cacheKey = getCacheKey(url, appIntent)
+ val cacheTimeStamp = userDoNotInterceptCache[cacheKey]
+ val currentTimeStamp = SystemClock.elapsedRealtime()
+
+ return cacheTimeStamp != null &&
+ currentTimeStamp <= (cacheTimeStamp + APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL)
+ }
+
+ internal fun addUserDoNotIntercept(url: String, appIntent: Intent?) {
+ val cacheKey = getCacheKey(url, appIntent)
+ cacheKey?.let {
+ userDoNotInterceptCache[it] = SystemClock.elapsedRealtime()
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL = 60 * 60 * 1000L // 1 hour
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val APP_LINKS_DO_NOT_INTERCEPT_INTERVAL = 2000L // 2 second
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
new file mode 100644
index 0000000000..8d45426909
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
@@ -0,0 +1,318 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.ActivityNotFoundException
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.os.SystemClock
+import android.provider.Browser.EXTRA_APPLICATION_ID
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.pm.isPackageInstalled
+import mozilla.components.support.ktx.android.net.isHttpOrHttps
+import mozilla.components.support.utils.Browsers
+import mozilla.components.support.utils.BrowsersCache
+import mozilla.components.support.utils.ext.queryIntentActivitiesCompat
+import mozilla.components.support.utils.ext.resolveActivityCompat
+import java.lang.Exception
+import java.lang.NullPointerException
+import java.lang.NumberFormatException
+import java.net.URISyntaxException
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url"
+private const val MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id="
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val APP_LINKS_CACHE_INTERVAL = 30 * 1000L // 30 seconds
+private const val ANDROID_RESOLVER_PACKAGE_NAME = "android"
+
+/**
+ * These use cases allow for the detection of, and opening of links that other apps have registered
+ * an [IntentFilter]s to open.
+ *
+ * Care is taken to:
+ * * resolve [intent://] links, including [S.browser_fallback_url]
+ * * provide a fallback to the installed marketplace app (e.g. on Google Android, the Play Store).
+ * * open HTTP(S) links with an external app.
+ *
+ * Since browsers are able to open HTTPS pages, existing browser apps are excluded from the list of
+ * apps that trigger a redirect to an external app.
+ *
+ * @param context Context the feature is associated with.
+ * @param launchInApp If {true} then launch app links in third party app(s). Default to false because
+ * of security concerns.
+ * @param alwaysDeniedSchemes List of schemes that will never be opened in a third-party app.
+ * @param installedBrowsers List of all installed browsers on the device.
+ */
+class AppLinksUseCases(
+ private val context: Context,
+ private var launchInApp: () -> Boolean = { false },
+ private val alwaysDeniedSchemes: Set<String> = ALWAYS_DENY_SCHEMES,
+ private val installedBrowsers: Browsers = BrowsersCache.all(context),
+) {
+ @Suppress(
+ "QueryPermissionsNeeded", // We expect our browsers to have the QUERY_ALL_PACKAGES permission
+ "TooGenericExceptionCaught",
+ )
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun findActivities(intent: Intent): List<ResolveInfo> {
+ return try {
+ context.packageManager
+ .queryIntentActivitiesCompat(intent, PackageManager.GET_RESOLVED_FILTER)
+ } catch (e: RuntimeException) {
+ Logger("AppLinksUseCases").error("failed to query activities", e)
+ emptyList()
+ }
+ }
+
+ /**
+ * Update launchInApp for this instance of AppLinksUseCases
+ * @param launchInApp the new value of launchInApp
+ */
+ fun updateLaunchInApp(launchInApp: () -> Boolean) {
+ this.launchInApp = launchInApp
+ }
+
+ private fun findDefaultActivity(intent: Intent): ResolveInfo? {
+ return context.packageManager.resolveActivityCompat(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ }
+
+ /**
+ * Parse a URL and check if it can be handled by an app elsewhere on the Android device.
+ * If that app is not available, then a market place intent is also provided.
+ *
+ * It will also provide a fallback.
+ *
+ * @param includeHttpAppLinks If {true} then test URLs that start with {http} and {https}.
+ * @param includeInstallAppFallback If {true} then offer an app-link to the installed market app
+ * if no web fallback is available.
+ */
+ @Suppress("ComplexMethod")
+ inner class GetAppLinkRedirect internal constructor(
+ private val includeHttpAppLinks: Boolean = false,
+ private val includeInstallAppFallback: Boolean = false,
+ ) {
+ operator fun invoke(url: String): AppLinkRedirect {
+ val urlHash = (url + includeHttpAppLinks + includeHttpAppLinks).hashCode()
+ val currentTimeStamp = SystemClock.elapsedRealtime()
+ // since redirectCache is mutable, get the latest
+ val cache = redirectCache
+ if (cache != null && urlHash == cache.cachedUrlHash &&
+ currentTimeStamp <= cache.cacheTimeStamp + APP_LINKS_CACHE_INTERVAL
+ ) {
+ return cache.cachedAppLinkRedirect
+ }
+
+ val redirectData = createBrowsableIntents(url)
+ val isAppIntentHttpOrHttps = redirectData.appIntent?.data?.isHttpOrHttps ?: false
+ val isEngineSupportedScheme = ENGINE_SUPPORTED_SCHEMES.contains(Uri.parse(url).scheme)
+ val isBrowserRedirect = redirectData.resolveInfo?.activityInfo?.packageName?.let { packageName ->
+ installedBrowsers.isInstalled(packageName)
+ } ?: false
+
+ val fallbackUrl = when {
+ redirectData.fallbackIntent?.data?.isHttpOrHttps == true ->
+ redirectData.fallbackIntent.dataString
+ else -> null
+ }
+
+ val appIntent = when {
+ redirectData.resolveInfo == null -> null
+ isBrowserRedirect && isEngineSupportedScheme -> null
+ includeHttpAppLinks && isAppIntentHttpOrHttps -> redirectData.appIntent
+ !launchInApp() && (isEngineSupportedScheme || fallbackUrl != null) -> null
+ else -> redirectData.appIntent
+ }
+
+ // no need to check marketplace intent since it is only set if a package is set in the intent
+ val appLinkRedirect = AppLinkRedirect(appIntent, fallbackUrl, redirectData.marketplaceIntent)
+ redirectCache = AppLinkRedirectCache(currentTimeStamp, urlHash, appLinkRedirect)
+ return appLinkRedirect
+ }
+
+ private fun createBrowsableIntents(url: String): RedirectData {
+ val intent = safeParseUri(url, Intent.URI_INTENT_SCHEME)
+ val fallbackIntent = intent?.getStringExtra(EXTRA_BROWSER_FALLBACK_URL)?.let {
+ safeParseUri(it, 0)
+ }
+
+ val marketplaceIntent = intent?.`package`?.let {
+ if (includeInstallAppFallback &&
+ !context.packageManager.isPackageInstalled(it)
+ ) {
+ safeParseUri(MARKET_INTENT_URI_PACKAGE_PREFIX + it, 0)
+ } else {
+ null
+ }
+ }
+
+ if (marketplaceIntent != null) {
+ marketplaceIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+
+ val appIntent = when {
+ intent?.data == null -> null
+ alwaysDeniedSchemes.contains(intent.data?.scheme) -> null
+ else -> intent
+ }
+
+ appIntent?.let {
+ it.addCategory(Intent.CATEGORY_BROWSABLE)
+ it.component = null
+ it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ it.selector?.addCategory(Intent.CATEGORY_BROWSABLE)
+ it.selector?.component = null
+ it.putExtra(EXTRA_APPLICATION_ID, context.packageName)
+ }
+
+ val resolveInfo = appIntent?.let {
+ findDefaultActivity(it)
+ }?.let { resolveInfo ->
+ when (resolveInfo.activityInfo?.packageName) {
+ // don't self target when it is an app link
+ context.packageName -> null
+ // no default app found but Android resolver shows there are multiple applications
+ // that can open this app link
+ ANDROID_RESOLVER_PACKAGE_NAME, null -> {
+ findActivities(appIntent).firstOrNull {
+ it.filter != null
+ }
+ }
+ // use default app
+ else -> {
+ appIntent.component =
+ ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
+ resolveInfo
+ }
+ }
+ }
+
+ return RedirectData(appIntent, fallbackIntent, marketplaceIntent, resolveInfo)
+ }
+ }
+
+ /**
+ * Open an external app with the redirect created by the [GetAppLinkRedirect].
+ *
+ * This does not do any additional UI other than the chooser that Android may provide the user.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ inner class OpenAppLinkRedirect internal constructor(
+ private val context: Context,
+ ) {
+ /**
+ * Tries to open an external app for the provided [appIntent]. Invokes [failedToLaunchAction]
+ * in case an exception is thrown opening the app.
+ *
+ * @param appIntent the [Intent] to open the external app for.
+ * @param launchInNewTask whether or not the app should be launched in a new task.
+ * @param failedToLaunchAction callback invoked in case opening the external app fails.
+ */
+ operator fun invoke(
+ appIntent: Intent?,
+ launchInNewTask: Boolean = true,
+ failedToLaunchAction: (fallbackUrl: String?) -> Unit = {},
+ ) {
+ appIntent?.let {
+ try {
+ val scheme = appIntent.data?.scheme
+ if (scheme != null && alwaysDeniedSchemes.contains(scheme)) {
+ return
+ }
+
+ if (launchInNewTask) {
+ it.flags = it.flags or Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(it)
+ } catch (e: Exception) {
+ when (e) {
+ is ActivityNotFoundException, is SecurityException, is NullPointerException -> {
+ failedToLaunchAction(it.getStringExtra(EXTRA_BROWSER_FALLBACK_URL))
+ Logger.error("failed to start third party app activity", e)
+ }
+ else -> throw e
+ }
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun safeParseUri(uri: String, flags: Int): Intent? {
+ return try {
+ val intent = Intent.parseUri(uri, flags)
+ if (context.packageName != null && context.packageName == intent?.`package`) {
+ // Ignore intents that would open in the browser itself
+ null
+ } else {
+ intent
+ }
+ } catch (e: URISyntaxException) {
+ Logger.error("failed to parse URI", e)
+ null
+ } catch (e: NumberFormatException) {
+ Logger.error("failed to parse URI", e)
+ null
+ }
+ }
+
+ val openAppLink: OpenAppLinkRedirect by lazy { OpenAppLinkRedirect(context) }
+ val interceptedAppLinkRedirect: GetAppLinkRedirect by lazy {
+ GetAppLinkRedirect(
+ includeHttpAppLinks = false,
+ includeInstallAppFallback = true,
+ )
+ }
+ val appLinkRedirect: GetAppLinkRedirect by lazy {
+ GetAppLinkRedirect(
+ includeHttpAppLinks = true,
+ includeInstallAppFallback = false,
+ )
+ }
+ val appLinkRedirectIncludeInstall: GetAppLinkRedirect by lazy {
+ GetAppLinkRedirect(
+ includeHttpAppLinks = true,
+ includeInstallAppFallback = true,
+ )
+ }
+ private data class RedirectData(
+ val appIntent: Intent? = null,
+ val fallbackIntent: Intent? = null,
+ val marketplaceIntent: Intent? = null,
+ val resolveInfo: ResolveInfo? = null,
+ )
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal data class AppLinkRedirectCache(
+ var cacheTimeStamp: Long,
+ var cachedUrlHash: Int,
+ var cachedAppLinkRedirect: AppLinkRedirect,
+ )
+
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var redirectCache: AppLinkRedirectCache? = null
+
+ @VisibleForTesting
+ internal fun clearRedirectCache() {
+ redirectCache = null
+ }
+
+ // list of scheme from https://searchfox.org/mozilla-central/source/netwerk/build/components.conf
+ internal val ENGINE_SUPPORTED_SCHEMES: Set<String> = setOf(
+ "about", "data", "file", "ftp", "http",
+ "https", "moz-extension", "moz-safe-about", "resource", "view-source", "ws", "wss", "blob",
+ )
+
+ internal val ALWAYS_DENY_SCHEMES: Set<String> = setOf("jar", "file", "javascript", "data", "about", "content")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt
new file mode 100644
index 0000000000..70a3ddab83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt
@@ -0,0 +1,34 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import androidx.fragment.app.DialogFragment
+
+/**
+ * This is a general representation of a dialog meant to be used in collaboration with [AppLinksInterceptor]
+ * to show a dialog before an external link is opened.
+ * If [SimpleRedirectDialogFragment] is not flexible enough for your use case you should inherit for this class.
+ * Be mindful to call [onConfirmRedirect] when you want to open the linked app.
+ */
+abstract class RedirectDialogFragment : DialogFragment() {
+
+ /**
+ * A callback to trigger a download, call it when you are ready to open the linked app. For instance,
+ * a valid use case can be in confirmation dialog, after the positive button is clicked,
+ * this callback must be called.
+ */
+ var onConfirmRedirect: () -> Unit = {}
+
+ /**
+ * A callback to trigger when user dismisses the dialog.
+ * For instance, a valid use case can be in confirmation dialog, after the negative button is clicked,
+ * this callback must be called.
+ */
+ var onCancelRedirect: () -> Unit? = {}
+
+ companion object {
+ const val FRAGMENT_TAG = "SHOULD_OPEN_APP_LINK_PROMPT_DIALOG"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt
new file mode 100644
index 0000000000..793c8c2ef6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt
@@ -0,0 +1,109 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.StringRes
+import androidx.annotation.StyleRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+/**
+ * This is the default implementation of the [RedirectDialogFragment].
+ *
+ * It provides an [AlertDialog] giving the user the choice to allow or deny the opening of a
+ * third party app.
+ *
+ * Intents passed are guaranteed to be openable by a non-browser app.
+ */
+class SimpleRedirectDialogFragment : RedirectDialogFragment() {
+
+ @VisibleForTesting
+ internal var testingContext: Context? = null
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ fun getBuilder(themeID: Int): AlertDialog.Builder {
+ val context = testingContext ?: requireContext()
+ return if (themeID == 0) AlertDialog.Builder(context) else AlertDialog.Builder(context, themeID)
+ }
+
+ return with(requireBundle()) {
+ val dialogTitleText = getInt(KEY_TITLE_TEXT, R.string.mozac_feature_applinks_normal_confirm_dialog_title)
+ val dialogMessageString = getString(KEY_MESSAGE_STRING, "")
+ val positiveButtonText = getInt(KEY_POSITIVE_TEXT, R.string.mozac_feature_applinks_confirm_dialog_confirm)
+ val negativeButtonText = getInt(KEY_NEGATIVE_TEXT, R.string.mozac_feature_applinks_confirm_dialog_deny)
+ val themeResId = getInt(KEY_THEME_ID, 0)
+ val cancelable = getBoolean(KEY_CANCELABLE, false)
+
+ getBuilder(themeResId)
+ .setTitle(dialogTitleText)
+ .setMessage(dialogMessageString)
+ .setPositiveButton(positiveButtonText) { _, _ ->
+ onConfirmRedirect()
+ }
+ .setNegativeButton(negativeButtonText) { _, _ ->
+ onCancelRedirect()
+ }
+ .setCancelable(cancelable)
+ .create()
+ .withCenterAlignedButtons()
+ }
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [SimpleRedirectDialogFragment]
+ */
+ fun newInstance(
+ @StringRes dialogTitleText: Int = R.string.mozac_feature_applinks_normal_confirm_dialog_title,
+ dialogMessageString: String = "",
+ @StringRes positiveButtonText: Int = R.string.mozac_feature_applinks_confirm_dialog_confirm,
+ @StringRes negativeButtonText: Int = R.string.mozac_feature_applinks_confirm_dialog_deny,
+ @StyleRes themeResId: Int = 0,
+ cancelable: Boolean = false,
+ ): RedirectDialogFragment {
+ val fragment = SimpleRedirectDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putInt(KEY_TITLE_TEXT, dialogTitleText)
+
+ putString(KEY_MESSAGE_STRING, dialogMessageString)
+
+ putInt(KEY_POSITIVE_TEXT, positiveButtonText)
+
+ putInt(KEY_NEGATIVE_TEXT, negativeButtonText)
+
+ putInt(KEY_THEME_ID, themeResId)
+
+ putBoolean(KEY_CANCELABLE, cancelable)
+ }
+
+ fragment.arguments = arguments
+ fragment.isCancelable = false
+
+ return fragment
+ }
+
+ const val KEY_POSITIVE_TEXT = "KEY_POSITIVE_TEXT"
+
+ const val KEY_NEGATIVE_TEXT = "KEY_NEGATIVE_TEXT"
+
+ const val KEY_TITLE_TEXT = "KEY_TITLE_TEXT"
+
+ const val KEY_MESSAGE_STRING = "KEY_MESSAGE_STRING"
+
+ const val KEY_THEME_ID = "KEY_THEME_ID"
+
+ const val KEY_CANCELABLE = "KEY_CANCELABLE"
+ }
+
+ private fun requireBundle(): Bundle {
+ return arguments ?: throw IllegalStateException("Fragment $this arguments is not set.")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..ff69fd0fc0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-am/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ክፈት በ…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">በመተግበሪያ ውስጥ ክፈት? እንቅስቃሴዎ ከአሁን በኋላ ግላዊ ላይሆን ይችላል።</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">በሌላ መተግበሪያ ውስጥ ክፈት</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">ይህን ይዘት ለማየት %sን መተው ይፈልጋሉ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ክፈት</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ተወው</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..780abd5674
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-an/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ubrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ubrir en aplicación? Ye posible que la tuya actividat deixe d’estar privada.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ubrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..61c7d51975
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ar/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">افتح في…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">أنفتحه في التطبيق؟ قد لا يكون نشاطك خاصا بعد الآن.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">افتح في تطبيق آخر</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">أتريد مغادرة %s لعرض هذا المحتوى؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">افتح</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ألغِ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..b0e7e03ca9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ast/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Quies abrir el conteníu na aplicación? La to actividá yá nun va ser privada.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Encaboxar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..532a7d8027
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-az/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Bununla aç…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Tətbiqdə açılsın? Aktivliyiniz artıq məxfi qalmaya bilər.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Aç</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Ləğv et</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..34639febb9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-azb/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">… دا آچین</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">اپ‌ده آچیلسین؟ فعالیتیز آرتیق گیزلی اولمایا بیلیر.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">آیری اپ‌ده آچین</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">بو موحتوایا باخماق اوچون %s -دن آیریلماق ایستییرسیز؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">آچ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">لغو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..9895f225e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-be/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Адкрыць у…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Адкрыць у праграме? Вашы дзеянні, магчыма, больш не будуць прыватнымі.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Адкрыць у іншай праграме</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Хочаце выйсці з %s, каб паглядзець гэтае змесціва?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Адкрыць</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Адмена</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..e9d4200619
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bg/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Отваряне в…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Отваряне в приложение? Вашите действия може вече да не са поверителни.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Отваряне в друго приложение</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Желаете ли да напуснете %s, за да прегледате съдържанието?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Отваряне</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Отказ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..73f07daddf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bn/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">খোলা…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">অ্যাপে খুলবেন? আপনার ক্রিয়াকলাপ আর ব্যক্তিগত নাও থাকতে পারে।</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">খুলুন</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">বাতিল</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..37583e59e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-br/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Digeriñ e…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Digeriñ en arload? Gallout a rafe ocʼh oberiantiz paouez da vezañ prevez.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Digeriñ en un arload all</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Fellout a ra deoc’h leuskel %s da welet an dra-mañ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Digeriñ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Nullañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..30083615a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-bs/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Otvori u…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Otvori u aplikaciji? Vaš rad možda više neće biti privatan.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Otvorite u drugoj aplikaciji</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Želite li napustiti %s da pogledate ovaj sadržaj?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Otvori</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Otkaži</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..cfd1c79b2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ca/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Obre amb…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Voleu obrir-ho en l’aplicació? És possible que la vostra activitat deixi de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Obre en una altra aplicació</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Voleu sortir del %s per a veure aquest contingut?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Obre</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancel·la</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..a2b6d94329
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cak/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Tijaq pa…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿La nijaq pa ri chokoy? Rik\'in jub\'a\' man xtichinäx ta chik ri asamaj.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Tijaq pa jun chik chokoy</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿La nawajo\' chi ri %s? nuk\'üt re rupam re\'?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Tijaq</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Tiq\'at</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..9ec6bca557
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">i-Open sa…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">i-Open sa app? Basin imong mga lihok dili na pribado.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Open</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..abc334425f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">کردنەوە لە …</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">کردنەوە لە بەرنامە؟ چالاکیەکانت لەوانەیە چیتر تایبەت و شاراوە نەبن.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">کردنەوە</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">پاشگەزبوونەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1a396ca554
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-co/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Apre cù…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Apre cù l’appiecazione ? A vostra attività puderia ùn esse più privata.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Apre in un’altra appiecazione</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vulete lascià %s per affissà stu cuntenutu ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Apre</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Abbandunà</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..375f1ef655
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cs/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Otevřít v…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Chcete odkaz otevřít v jiné aplikaci? Vaše prohlížení nemusí zůstat anonymní.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Otevřít v jiné aplikaci</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Chcete aplikaci %s dovolit zobrazit tento obsah?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Otevřít</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Zrušit</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ab9964cfac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-cy/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Agor yn…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Agor yn yr ap? Efallai na fydd eich gweithgaredd yn breifat mwyach.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Agor mewn ap arall</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Hoffech chi adael %s i weld y cynnwys hwn?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Agor</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Diddymu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..4a066f5e02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-da/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Åbn i…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Åbn i app? Din aktivitet er muligvis ikke længere privat.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Åbn i en anden app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vil du forlade %s for at se dette indhold?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Åbn</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annuller</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..4bf0ab2df2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-de/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Öffnen in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">In App öffnen? Ihre Aktivitäten sind dann möglicherweise nicht mehr privat.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">In einer anderen App öffnen</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Möchten Sie %s verlassen, um diesen Inhalt anzuzeigen?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Öffnen</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Abbrechen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..70ab0f28c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Wócyniś w…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">W nałoženju wócyniś? Waša aktiwita wěcej njamóžo priwatna byś.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">W drugem nałoženju wócyniś</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Cośo %s skóńcyś, aby se wopśimjeśe woglědał?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Wócyniś</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Pśetergnuś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..02ed2b1c91
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-el/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Άνοιγμα σε…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Άνοιγμα στην εφαρμογή; Η δραστηριότητά σας ενδέχεται να μην είναι πλέον ιδιωτική.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Άνοιγμα σε άλλη εφαρμογή</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Θέλετε να αποχωρήσετε από το %s για την προβολή αυτού του περιεχομένου;</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Άνοιγμα</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Ακύρωση</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..4e9466cac8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Open in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Open in app? Your activity may no longer be private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Open in another app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Would you like to leave %s to view this content?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Open</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..4e9466cac8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Open in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Open in app? Your activity may no longer be private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Open in another app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Would you like to leave %s to view this content?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Open</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..0a4ef491f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-eo/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Malfermi per…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ĉu malfermi en programo? Via retumo povus ne plu esti privata.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Malfermi per alia apo</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Ĉu vi ŝatus forlasi %s por vidi tiun ĉi enhavon?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Malfermi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Nuligi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..fdf53466fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Abrir en aplicación? Es posible que tu actividad deje de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir en otra aplicación</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Querés dejar que %s muestre este contenido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..1a749b1dcc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Abrir en la aplicación? Puede que tu actividad deje de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir en otra app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Te gustaría dejar %s para ver este contenido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..8a7fd96185
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Abrir en aplicación? Es posible que tu actividad deje de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir en otra aplicación</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Quiere dejar %s para ver este contenido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..21f4054bfe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Abrir en la aplicación? Puede que tu actividad deje de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir en otra aplicación</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Te gustaría dejar %s para ver este contenido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..8a7fd96185
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-es/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Abrir en aplicación? Es posible que tu actividad deje de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir en otra aplicación</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Quiere dejar %s para ver este contenido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..3f5389f6c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-et/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ava link äpiga…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Kas soovid avada äpis? Sinu tegevus ei pruugi siis enam privaatne olla.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Ava teises äpis</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Kas soovid selle sisu vaatamiseks %sist lahkuda?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ava</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Loobu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..321bc172f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-eu/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ireki honekin…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Aplikazioan ireki? Baliteke zure jarduera pribatua ez izatea hemendik aurrera.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Ireki beste aplikazio batean</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">%s aplikazioa utzi nahi duzu eduki hau ikusteko?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ireki</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Utzi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..d2f7f56bcc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fa/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">گشودن در…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">گشودن در کاره؟ فعالیت شما ممکن است دیگر خصوصی نباشد.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">باز کردن در برنامه دیگر</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">آیا مایلید %s را ترک کنید تا این محتوا را ببینید؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">گشودن</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">لغو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..fb2352635b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ff/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Uddit e…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Uddit-de e nder jaaɓnirgal? Golle maina mbaawi nattude wonde cuuriiɗe.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Uddit e jaaɓngal goɗngal</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Aɗa yiɗi yaltude %s ngam yiyde ndii loowdi?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Uddit</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Haaytu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..8cd90c26b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fi/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Avaa sovelluksella…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Avaa sovelluksella? Toimesi eivät välttämättä ole enää yksityisiä.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Avaa toisessa sovelluksessa</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Siirrytäänkö sovelluksesta %s tämän sisällön katseluun?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Avaa</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Peruuta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..64ad89e7e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ouvrir dans…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ouvrir dans l’application ? Votre activité pourrait ne plus être privée.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Ouvrir dans une autre application</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Souhaitez-vous quitter %s pour afficher ce contenu ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ouvrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annuler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..336dc6b276
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fur/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Vierç in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Vierzi te aplicazion? Al è pussibil che lis tôs ativitâts no restin plui privadis.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Vierç intune altre app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Desideristu lâ fûr di %s par visualizâ chest contignût?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Vierç</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anule</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..239bbb058d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Iepenje yn…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Iepenje yn in app? Jo aktiviteit is miskien net langer privee.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Iepenje yn oare app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Wolle jo %s ferlitte om dizze ynhâld te besjen?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Iepenje</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annulearje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..1e64cbab2d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Oscail i…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Oscail in aip? Seans nach mbeidh do chuid gníomhaíochtaí príobháideach a thuilleadh.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Oscail</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cealaigh</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..7f9b198a09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gd/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Fosgail an-seo…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">A bheil thu airson fhosgladh ann an aplacaid? Dh’fhaoidte nach bi na nì thu prìobhaideach tuilleadh.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Fosgail ann an aplacaid eile</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">A bheil thu airson %s fhàgail gus an t-susbaint seo a leughadh?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Fosgail</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Sguir dheth</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..eee2f00cb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gl/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Queres abrir na aplicación? É posible que a súa actividade xa non sexa privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir noutro aplicativo</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Quere deixar %s para ver este contido?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..f772e611dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gn/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Embojuruja amo…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Embojurujápa tembiporu’i. Ikatu ne rembiapo osẽ ñemihágui.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Embojuruja ambue tembiporu’ípe</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">¿Añetépa ehejase %s ehecha hag̃ua ko tetepy?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Mbojuruja</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Heja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..4a4cd19689
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">આમાં ખોલો…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">એપ્લિકેશનમાં ખોલો? તમારી પ્રવૃત્તિ હવે ખાનગી રહેશે નહીં.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ખોલો</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">રદ કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..58773f9f3a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">इसमें खोलें…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ऐप में खोलें? आपके गतिविधि शायद अब निजी नहीं रह सकते।</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">खोलें</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">रद्द करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..92009ec37e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hil/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Pagabuksan sa…</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Buksan</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Kanselahon</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..ddd2353142
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Otvori u …</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Otvoriti u aplikaciji? Tvoja aktivnost možda više neće biti privatna.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Otvori u drugoj aplikaciji</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Želite li napustiti %s da vidite ovaj sadržaj?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Otvori</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Odustani</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..f2200e0692
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Wočinić w…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">W nałoženju wočinić? Waša aktiwita hižo njemóže priwatna być.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">W druhim nałoženju wočinić</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Chceće %s skónčić, zo byšće sej wobsah wobhladał?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Wočinić</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Přetorhnyć</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..5118745c3e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hu/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Megnyitás a következővel…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Megnyitja az alkalmazásban? Lehet, hogy tevékenysége már nem lesz privát.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Megnyitás egy másik alkalmazásban</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Elhagyja a %sot a tartalom megtekintéséhez?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Megnyitás</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Mégse</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..f6adb07c3b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Բացել հետևյալում…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Բացե՞լ եք հավելվածում: Ձեր գործունեությունն այլևս չի կարող լինել մասնավոր:</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Բացել այլ հավելվածում</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Ցանկանու՞մ եք հեռանալ %s-ից՝ այս բովանդակությունը դիտելու համար:</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Բացել</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Չեղարկել</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..c27deca6a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ia/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Aperir in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Aperir in le app? Tu activitate poterea devenir non private</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Aperir in un altere application</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Desira tu permitter que %s vide iste contento?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Aperir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancellar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..30194e3a14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-in/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Buka di…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Buka di aplikasi? Aktivitas Anda mungkin tidak lagi pribadi.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Buka di aplikasi lainnya</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Ingin meninggalkan %s untuk melihat konten ini?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Buka</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Batal</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..86fbd7b19d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-is/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Opna með…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Opna í smáforriti? Vera má að athafnir þínar verði opinberar.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Opna með öðru forriti</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Viltu yfirgefa %s til að skoða þetta efni?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Opna</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Hætta við</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..59b022a2d0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-it/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Apri in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Aprire con questa app? Le tue attività potrebbero non rimanere private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Apri in un’altra app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Uscire da %s per visualizzare questo contenuto?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Apri</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..933d4fba76
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-iw/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">פתיחה ב…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">האם לפתוח ביישומון? ייתכן שהפעילות שלך כבר לא תהיה פרטית.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">פתיחה ביישומון אחר</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">האם ברצונך לעזוב את %s כדי לצפות בתוכן זה?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">פתיחה</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ביטול</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..e20cb72eaa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ja/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">外部アプリで開く…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">外部アプリで開く場合、その行動はプライベートにはなりません。</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">他のアプリで開く</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">%s を離れてこのコンテンツを表示しますか?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">開く</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">キャンセル</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..791e2e1586
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ka/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ბმულის გახსნა…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ხსნით პროგრამაში? თქვენი მოქმედებები შეიძლება გამჟღავნდეს.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">სხვა პროგრამით გახსნა</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">გსურთ დატოვოთ %s ამ შიგთავსის სანახავად?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">გახსნა</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">გაუქმება</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..89468a8e2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">…da ashıw</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Baǵdarlamada ashılsın ba? Endi háreketińiz jeke bolmawı múmkin.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Basqa baǵdarlamada ashıw</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Bul kontentti kóriw ushın %s dan shıǵıwdı qáleysiz be?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ashıw</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Biykarlaw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..6c96ab6468
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kab/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ldi deg…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ldi deg usnas? Armud-ik yezmer ur yettili ara d abaḍni.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Ldi deg usnas-nniḍen</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Tebɣiḍ ad teǧǧeḍ %s i uskan n ugbur-a?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ldi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Sefsex</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..a2a1495f9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kk/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Көмегімен ашу…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Қолданбада ашу керек пе? Әрекетіңіз енді жеке болмауы мүмкін.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Басқа қолданбада ашу</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Осы мазмұнды көру үшін %s қалдырғыңыз келе ме?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ашу</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Бас тарту</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..3f75f3c7b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Veke di…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Di sepanê de veke? Dibe ku çalakiyên te veşartî nemînin.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Di appeke din de veke</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Gelo tu dixwazî ji bo dîtina vê naverokê ji %sê derkevî?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Veke</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Betal bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..228fbba25e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-kn/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ಇದರಲ್ಲಿ ತೆರೆ…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ಅಪ್ಲಿಕೇಶನ್‌ನಲ್ಲಿ ತೆರೆಯುವುದೇ? ನಿಮ್ಮ ಚಟುವಟಿಕೆ ಇನ್ನು ಮುಂದೆ ಖಾಸಗಿಯಾಗಿರಬಾರದು.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ತೆರೆ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ರದ್ದು ಮಾಡು</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..6331322422
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ko/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">앱에서 열기…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">앱에서 여시겠습니까? 사용자의 활동이 더 이상 보호되지 않을 수 있습니다.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">다른 앱에서 열기</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">이 콘텐츠를 보기 위해 %s에서 나가시겠습니까?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">열기</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">취소</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..f5d32160c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lij/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Arvi in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Arvî con sta app? Dòppo e teu ativitæ porieivan no ese ciù privæ.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Arvi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..8a5b283d03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lo/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ເປີດໃນ…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ເປີດໃນແອັບນີ້ບໍ່? ການເຄື່ອນໄຫວຂອງທ່ານອາດຈະບໍ່ເປັນສ່ວນຕົວອີກຕໍ່ໄປ.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">ເປີດໃນແອັບອື່ນ</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">ທ່ານຕ້ອງການອອກຈາກ %s ເພື່ອເບິ່ງເນື້ອຫານີ້ບໍ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ເປີດ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ຍົກເລີກ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..7eccf4ceb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-lt/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Atverti per…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Atverti programoje? Jūs veikla galimai nebus privati.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Atverti</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Atsisakyti</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..4a5791ab9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-mix/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Kuna tsi…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">¿Kuna nu aplicación? ntyina ni ku kuntye^e ña sau.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Kuna</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Kunchatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..8b0c9711bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ml/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ഇതിൽ തുറക്കുക…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">അപ്ലിക്കേഷനിൽ തുറക്കണോ? നിങ്ങളുടെ പ്രവർത്തനം ഇനിമേൽ സ്വകാര്യമായിരിക്കില്ല.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">തുറക്കുക</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">റദ്ദാക്കുക</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c2973030a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-mr/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">मध्ये उघडा…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">अॅपमध्ये उघडायचे आहे का? आपली कृती यापुढे गोपनीय राहणार नाही.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">उघडा</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">रद्द करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..03a631f703
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-my/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">…တွင် ဖွင့်ရန်</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">အက်ပ်ဖွင့်ပါသလား။ သင်၏လုပ်ဆောင်မှုသည် မလျှို့ဝှက်ပါ။</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ဖွင့်ပါ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ပယ်​ဖျက်ပါ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..376b821089
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Åpne i…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Åpne i app? Aktiviteten din er muligens ikke lenger privat.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Åpne i en annen app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vil du forlate %s for å se dette innholdet?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Åpne</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..ba26f27e88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">… मा खोल्नुहोस्</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">एपमा खोल्न चाहानुहुन्छ ? तपाईका गतिबिधीहरु अब उप्रान्त गोप्य नहुन सक्छन्।</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">खोल्नुहोस्</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">रद्द गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..8ef9a0217c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nl/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Openen in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Openen in een app? Uw activiteit is mogelijk niet langer privé.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Openen in andere app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Wilt u %s verlaten om deze inhoud te bekijken?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Openen</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annuleren</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..2f167375f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Opne i…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Opne i app? Aktiviteten din er kanskje ikkje lenger privat.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Opne i ein annan app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vil du forlate %s for å sjå dette innhaldet?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Opne</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..c842e1490d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-oc/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Dobrir amb…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Dobrir dins l’aplicacion ? Vòstra activitat poiriá quitar d’èsser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Dobrir dins una autra aplicacion</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Volètz quitar %s per afichar aqueste contengut ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Dobrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anullar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..f050324b07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-or/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">…ରେ ଖୋଲନ୍ତୁ</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ଖୋଲନ୍ତୁ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ବାତିଲ କରନ୍ତୁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..4020752925
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">…ਨਾਲ ਖੋਲ੍ਹੋ</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ਐਪ ‘ਚ ਖੋਲ੍ਹਣਾ ਹੈ? ਤੁਹਾਡੀ ਸਰਗਰਮੀ ਨਿੱਜੀ ਨਹੀਂ ਵੀ ਰਹਿ ਸਕਦੀ ਹੈ।</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">ਹੋਰ ਐਪ ਵਿੱਚ ਖੋਲ੍ਹੋ</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">ਕੀ ਤੁਸੀਂ ਇਹ ਸਮੱਗਰੀ ਵੇਖਣ ਲਈ %s ਤੋਂ ਬਾਹਰ ਜਾਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ਖੋਲ੍ਹੋ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ਰੱਦ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..5582e5e082
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">کیہنوں کھولھو…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ایپ چ کھولھݨا اے؟ تہاڈی ورتوں نجی نہیں وی رہ سکدی اے۔</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">دوجی ایپ نال کھولھو</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">کیہہ تسیں %s توں باہر جاوݨ چاہندے او؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">کھولھو</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">رد کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..f761f3bc3f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pl/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Otwórz w…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Otworzyć w aplikacji? Twoje działania mogą nie być już prywatne.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Otwórz w innej aplikacji</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Czy opuścić aplikację %s, aby wyświetlić tę treść?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Otwórz</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anuluj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..894a5ffac6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir no…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Abrir em aplicativo? Sua atividade pode não ser mais privativa.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir em outro aplicativo</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Quer deixar %s ver este conteúdo?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..7e2e9c3692
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Abrir em…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Abrir na aplicação? A sua atividade pode deixar de ser privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Abrir noutra aplicação</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Gostaria de deixar %s para ver este conteúdo?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Abrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..4ca52de0bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-rm/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Avrir en…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Avrir en ina app? Tia activitad n\'è lura forsa betg pli privata.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Avrir en ina autra app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vuls ti bandunar %s per laschar mussar quest cuntegn?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Avrir</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Interrumper</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..b3359b3b12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ro/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Deschide în…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Deschizi în aplicație? Este posibil ca activitatea ta să nu mai fie privată.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Deschide</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anulează</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c78bc38057
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ru/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Открыть в…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Открыть в приложении? Возможно, ваши действия перестанут быть приватными.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Открыть в другом приложении</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Вы хотите покинуть %s для просмотра этого содержимого?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Открыть</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Отмена</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..ef1819dbaa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sat/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">… ᱨᱮ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ᱮᱯ ᱨᱮ ᱡᱷᱤᱡᱽ ᱢᱮ? ᱟᱢᱟᱜ ᱠᱟᱹᱢᱤ ᱟᱨ ᱡᱟᱹᱥᱛᱤ ᱜᱷᱟᱹᱬᱤᱡ ᱱᱤᱡᱮᱨᱟᱜ ᱵᱟᱝ ᱛᱟᱦᱮᱸᱱ-ᱟ ᱾</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">ᱮᱴᱟᱜ ᱮᱯ ᱨᱮ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">ᱟᱢ ᱫᱚ ᱱᱚᱶᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ %s ᱟᱲᱟᱜ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ ?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ᱵᱟᱹᱰᱨᱟᱹ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..ca74934899
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sc/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Aberi in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Boles abèrrere su cuntenutu in s’aplicatzione? Podet èssere chi s’atividade tua non siat prus privada.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Aberi in un’àtera aplicatzione</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Boles lassare %s pro bìdere custu cuntenutu?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Aberi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Annulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..251176ffaf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-si/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">මෙහි අරින්න…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">යෙදුමෙහි අරින්නද? ඔබගේ ක්‍රියාකාරකම් තවදුරටත් පෞද්. නොවීමට හැකිය.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">අන් යෙදුමකින් අරින්න</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">මෙම අන්තර්ගතය බැලීමට %s හැර යාමට කැමතිද?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">අරින්න</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">අවලංගු</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..0dbc181a50
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sk/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Otvoriť pomocou…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Chcete tento odkaz otvoriť v aplikácii? Môže sa tým znížiť úroveň vášho súkromia.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Otvoriť v inej aplikácii</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Chcete tento obsah zobraziť v aplikácii %s?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Otvoriť</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Zrušiť</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..78faaff48b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-skr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">۔۔۔ وچ کھولو</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ایپ وچ کھولوں؟ تہاݙی سرگرمی ہݨ نجی کائناں ہوسی۔</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">ہک ٻئی ایپ وچ کھولو</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">بھلا تساں ایہ مواد ݙیکھݨ کیتے %s کوں چھوڑݨ پسند کریسو؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">کھولو</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">منسوخ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..1e61c3da29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sl/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Odpri v …</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Odprem v aplikaciji? Vaša dejavnost morda ne bo več zasebna.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Odpiranje v drugi aplikaciji</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Ali želite za ogled te vsebine zapustiti %s?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Odpri</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Prekliči</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..38db6daf14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sq/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Hapeni në…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Të hapet në aplikacion? Veprimtaria juaj mund të mos jetë më private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Hape me një aplikacion tjetër</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Do të donit ta linit %s të shohë këtë lëndë?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Hape</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anuloje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..bda63de1f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Отвори у…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Отвори у апликацији? Ваше радње можда неће више бити приватне.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Отвори у другој апликацији</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Желите ли да напустите %s да видите овај садржај?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Отвори</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Откажи</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..1253240696
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-su/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Buka di…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Buka dina aplikasi? Réngkak anjeun bisa jadi henteu nyamuni.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Buka di séjén aplikasi</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Badé ninggalkeun %s pikeun muka ieu kontén?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Buka</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Bolay</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..3c39a5eb58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Öppna med…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Öppna i appen? Din aktivitet kanske inte längre är privat.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Öppna i en annan app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Vill du lämna %s för att se detta innehåll?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Öppna</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..2268adbf51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ta/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">இதில் திற…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">செயலியில் திறக்கவா? உங்கள் செயல்பாடு இனி தனிப்பட்டதாக இருக்காது.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">திற</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">இரத்து</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..5e5bb1e0f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-te/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">దీనిలో తెరువు…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">అనువర్తనంలో తెరవాలా? మీ కార్యాచరణ ఇకపై అంతరంగికంగా ఉండకపోవచ్చు.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">తెరువు</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">రద్దుచేయి</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..da0b24aecd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tg/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Кушодан дар…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Дар барнома кушода шавад? Фаъолияти шумо метавонад дигар хусусӣ набошад.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Кушодан дар барномаи дигар</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Барои дидани ин муҳтаво шумо мехоҳед, ки %s-ро тарк кунед?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Кушодан</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Бекор кардан</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..88f38d1f46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-th/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">เปิดใน…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ต้องการเปิดในแอปหรือไม่? กิจกรรมของคุณอาจไม่เป็นส่วนตัวอีกต่อไป</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">เปิดในแอปอื่น</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">คุณต้องการออกจาก %s เพื่อดูเนื้อหานี้หรือไม่?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">เปิด</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">ยกเลิก</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..669bc89bf0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tl/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Buksan sa…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Buksan sa app? Maaaring hindi na maging pribado ang iyong aktibidad.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Buksan</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Kanselahin</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..2d6239cfa8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Birlikte aç…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Uygulamada açılsın mı? İşleminiz gizli kalmayabilir.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Başka bir uygulamada aç</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Bu içeriği görüntülemek için %s tarayıcısından ayrılmak istiyor musunuz?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Aç</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">İptal</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..43958b8ee7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-trs/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Nā\'nīn riña…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Nā\'nīnt riña aplikasiûn nan anj. Ga\'ue gīni\'iāj a\'ngô nej si sa \'iát.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Nā’nïn riña a’ngô app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Ruhuât dūnâjt %s da’ gā’hue ni’hiājt sa mà riña nan anj.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Nā\'nīn</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Duyichin\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..92e461c2bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tt/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">… белән ачу</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Кушымтада ачу кирәкме? Гамәлләрегез бүтән хосусый булмаска да мөмкин.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Башка кушымтада ачу</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Бу эчтәлекне карау өчен %s программасыннан чыгарга телисезме?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ачу</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Баш тарту</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..bea48edbd7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Rẓem g…</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Rẓem</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..d763c0fff2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ug/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">ئېچىش…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ئەپتە ئاچامسىز؟ پائالىيىتىڭىز ئەمدى خۇپىيانە بولماسلىقى مۇمكىن.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">باشقا ئەپتە ئاچ</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">بۇ مەزمۇننى كۆرۈش ئۈچۈن %s دىن ئايرىلامسىز؟</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">ئېچىش</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">بىكار قىلىش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..49630c3a6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-uk/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Відкрити в…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Відкрити в програмі? Ваша діяльність може більше не бути приватною.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Відкрити в іншій програмі</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Бажаєте вийти з %s для перегляду цього вмісту?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Відкрити</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Скасувати</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..0a2932d39d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-ur/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">… میں کھولیں</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">ایپ میں کھولیں؟ آپکی سرگرمی اب ذاتی نہیں ہوگی۔</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">کھولیں</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">منسوخ کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..3ea8055ac7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-uz/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ochish:</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ilovada ochilsinmi? Faoliyatingizning maxfiyligi yoʻqoladi.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ochish</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Bekor qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..99cf7d769d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-vec/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Vèrxi in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Vèrxere co sta app? Ƚe to atività ƚe poderia no restare private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Verxi en on altra app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Nare fora da %s par vixualixare cuesto contenudo?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Vèrxi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Anuƚa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..04ca57ca9d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-vi/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Mở trong…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Mở trong ứng dụng? Hoạt động của bạn có thể không còn riêng tư.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Mở trong ứng dụng khác</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Bạn có muốn rời khỏi %s để xem nội dung này không?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Mở</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Hủy bỏ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..cee62dfb36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-yo/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Ṣi nínú…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Ṣí sílẹ̀ lórí áàpù? Ohun tí ò ń ṣe lè má jẹ́ ìkọ̀kọ̀ mọ́.</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Ṣi</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Fagile</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..7be862b73b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">打开于…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">要在应用中打开吗?您的上网行为可能不再保持私密。</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">其他应用打开</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">您想要离开 %s 来查看此内容吗?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">打开</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">取消</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..034c22b91c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">開啟於…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">要使用 App 開啟?您的上網行為可能不再能保持隱私。</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">用其他應用程式開啟</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">您想要離開 %s 來檢視此內容嗎?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">開啟</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">取消</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/app-links/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..04d45e5412
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 http://mozilla.org/MPL/2.0/.
+ -->
+
+<resources>
+ <!-- The tile for the list of external apps to open the link in -->
+ <string name="mozac_feature_applinks_open_in">Open in…</string>
+ <!-- The description to warn users that their private browsing session changes -->
+ <string name="mozac_feature_applinks_confirm_dialog_title">Open in app? Your activity may no longer be private.</string>
+ <!-- The title of the prompt that warns users their normal browsing session is trying to open another app -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_title">Open in another app</string>
+ <!-- The message of the prompt to confirm with users that they want to open the link in another app
+ %s is a placeholder that will be replaced by the app name -->
+ <string name="mozac_feature_applinks_normal_confirm_dialog_message">Would you like to leave %s to view this content?</string>
+ <!-- Opens the selected time -->
+ <string name="mozac_feature_applinks_confirm_dialog_confirm">Open</string>
+ <!-- Cancels the prompt -->
+ <string name="mozac_feature_applinks_confirm_dialog_deny">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinkRedirectTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinkRedirectTest.kt
new file mode 100644
index 0000000000..dcd40035fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinkRedirectTest.kt
@@ -0,0 +1,77 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.Intent
+import android.net.Uri
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+class AppLinkRedirectTest {
+
+ @Test
+ fun hasExternalApp() {
+ var appLink = AppLinkRedirect(appIntent = mock(), fallbackUrl = null, marketplaceIntent = null)
+ assertTrue(appLink.hasExternalApp())
+ assertTrue(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = null, fallbackUrl = null, marketplaceIntent = null)
+ assertFalse(appLink.hasExternalApp())
+ assertFalse(appLink.isRedirect())
+ }
+
+ @Test
+ fun hasFallback() {
+ var appLink = AppLinkRedirect(appIntent = mock(), fallbackUrl = null, marketplaceIntent = null)
+ assertFalse(appLink.hasFallback())
+ assertTrue(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = mock(), fallbackUrl = "https://example.com", marketplaceIntent = null)
+ assertTrue(appLink.hasFallback())
+ assertTrue(appLink.isRedirect())
+ }
+
+ @Test
+ fun isRedirect() {
+ var appLink = AppLinkRedirect(appIntent = null, fallbackUrl = null, marketplaceIntent = null)
+ assertFalse(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = mock(), fallbackUrl = null, marketplaceIntent = null)
+ assertTrue(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = null, fallbackUrl = "https://example.com", marketplaceIntent = null)
+ assertTrue(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = mock(), fallbackUrl = "https://example.com", marketplaceIntent = null)
+ assertTrue(appLink.isRedirect())
+ }
+
+ @Test
+ fun isInstallable() {
+ val intent: Intent = mock()
+ val uri: Uri = mock()
+ `when`(intent.data).thenReturn(uri)
+ `when`(uri.scheme).thenReturn("market")
+
+ var appLink = AppLinkRedirect(appIntent = null, fallbackUrl = "https://example.com", marketplaceIntent = null)
+ assertFalse(appLink.isInstallable())
+ assertTrue(appLink.isRedirect())
+
+ appLink = AppLinkRedirect(appIntent = intent, fallbackUrl = "https://example.com", marketplaceIntent = null)
+ assertTrue(appLink.isInstallable())
+ assertTrue(appLink.isRedirect())
+ }
+
+ @Test
+ fun hasMarketplaceIntent() {
+ var appLink = AppLinkRedirect(appIntent = null, fallbackUrl = null, marketplaceIntent = mock())
+ assertTrue(appLink.hasMarketplaceIntent())
+ assertTrue(appLink.isRedirect())
+ assertTrue(appLink.hasMarketplaceIntent())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt
new file mode 100644
index 0000000000..a48af2da58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt
@@ -0,0 +1,330 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.fragment.app.FragmentManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.AppIntentState
+import mozilla.components.browser.state.state.ExternalPackage
+import mozilla.components.browser.state.state.PackageCategory
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class AppLinksFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var mockContext: Context
+ private lateinit var mockFragmentManager: FragmentManager
+ private lateinit var mockUseCases: AppLinksUseCases
+ private lateinit var mockGetRedirect: AppLinksUseCases.GetAppLinkRedirect
+ private lateinit var mockOpenRedirect: AppLinksUseCases.OpenAppLinkRedirect
+ private lateinit var mockEngineSession: EngineSession
+ private lateinit var mockDialog: RedirectDialogFragment
+ private lateinit var mockLoadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase
+ private lateinit var feature: AppLinksFeature
+
+ private val webUrl = "https://example.com"
+ private val webUrlWithAppLink = "https://soundcloud.com"
+ private val intentUrl = "zxing://scan"
+ private val aboutUrl = "about://scan"
+
+ @Before
+ fun setup() {
+ store = BrowserStore()
+ mockContext = mock()
+
+ mockFragmentManager = mock()
+ `when`(mockFragmentManager.beginTransaction()).thenReturn(mock())
+ mockUseCases = mock()
+ mockEngineSession = mock()
+ mockDialog = mock()
+ mockLoadUrlUseCase = mock()
+
+ mockGetRedirect = mock()
+ mockOpenRedirect = mock()
+ `when`(mockUseCases.interceptedAppLinkRedirect).thenReturn(mockGetRedirect)
+ `when`(mockUseCases.openAppLink).thenReturn(mockOpenRedirect)
+
+ val webRedirect = AppLinkRedirect(null, webUrl, null)
+ val appRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), null, null)
+ val appRedirectFromWebUrl = AppLinkRedirect(Intent.parseUri(webUrlWithAppLink, 0), null, null)
+
+ `when`(mockGetRedirect.invoke(webUrl)).thenReturn(webRedirect)
+ `when`(mockGetRedirect.invoke(intentUrl)).thenReturn(appRedirect)
+ `when`(mockGetRedirect.invoke(webUrlWithAppLink)).thenReturn(appRedirectFromWebUrl)
+
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ ),
+ ).also {
+ it.start()
+ }
+ }
+
+ @After
+ fun teardown() {
+ feature.stop()
+ }
+
+ @Test
+ fun `feature observes app intents when started`() {
+ val tab = createTab(webUrl)
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ verify(feature, never()).handleAppIntent(any(), any(), any())
+
+ val intent: Intent = mock()
+ val appIntent = AppIntentState(intentUrl, intent)
+ store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
+
+ store.waitUntilIdle()
+ verify(feature).handleAppIntent(any(), any(), any())
+
+ val tabWithConsumedAppIntent = store.state.findTab(tab.id)!!
+ assertNull(tabWithConsumedAppIntent.content.appIntent)
+ }
+
+ @Test
+ fun `feature doesn't observes app intents when stopped`() {
+ val tab = createTab(webUrl)
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ verify(feature, never()).handleAppIntent(any(), any(), any())
+
+ feature.stop()
+
+ val intent: Intent = mock()
+ val appIntent = AppIntentState(intentUrl, intent)
+ store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
+
+ verify(feature, never()).handleAppIntent(any(), any(), any())
+ }
+
+ @Test
+ fun `WHEN should prompt AND in non-private mode THEN an external app dialog is shown`() {
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ shouldPrompt = { true },
+ ),
+ ).also {
+ it.start()
+ }
+
+ val tab = createTab(webUrl)
+ feature.handleAppIntent(tab, intentUrl, mock())
+
+ verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
+ verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `WHEN should not prompt AND in non-private mode THEN an external app dialog is not shown`() {
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ shouldPrompt = { false },
+ ),
+ ).also {
+ it.start()
+ }
+
+ val tab = createTab(webUrl)
+ feature.handleAppIntent(tab, intentUrl, mock())
+
+ verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
+ }
+
+ @Test
+ fun `WHEN custom tab and caller is the same as external app THEN an external app dialog is not shown`() {
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ shouldPrompt = { true },
+ ),
+ ).also {
+ it.start()
+ }
+
+ val tab =
+ createCustomTab(
+ id = "c",
+ url = webUrl,
+ source = SessionState.Source.External.CustomTab(
+ ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
+ ),
+ )
+
+ val appIntent: Intent = mock()
+ val componentName: ComponentName = mock()
+ doReturn(componentName).`when`(appIntent).component
+ doReturn("com.zxing.app").`when`(componentName).packageName
+
+ feature.handleAppIntent(tab, intentUrl, appIntent)
+
+ verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
+ }
+
+ @Test
+ fun `WHEN should prompt and in private mode THEN an external app dialog is shown`() {
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ shouldPrompt = { true },
+ ),
+ ).also {
+ it.start()
+ }
+
+ val tab = createTab(webUrl, private = true)
+ feature.handleAppIntent(tab, intentUrl, mock())
+
+ verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
+ verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `WHEN should not prompt and in private mode THEN an external app dialog is shown`() {
+ feature = spy(
+ AppLinksFeature(
+ context = mockContext,
+ store = store,
+ fragmentManager = mockFragmentManager,
+ useCases = mockUseCases,
+ dialog = mockDialog,
+ loadUrlUseCase = mockLoadUrlUseCase,
+ shouldPrompt = { false },
+ ),
+ ).also {
+ it.start()
+ }
+
+ val tab = createTab(webUrl, private = true)
+ feature.handleAppIntent(tab, intentUrl, mock())
+
+ verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
+ verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `redirect dialog is only added once`() {
+ val tab = createTab(webUrl, private = true)
+ feature.handleAppIntent(tab, intentUrl, mock())
+
+ verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
+
+ doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, "")
+ doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG)
+ feature.handleAppIntent(tab, intentUrl, mock())
+ verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `only loads URL if scheme is supported`() {
+ val tab = createTab(webUrl, private = true)
+
+ feature.loadUrlIfSchemeSupported(tab, intentUrl)
+ verify(mockLoadUrlUseCase, never()).invoke(anyString(), anyString(), any(), any())
+
+ feature.loadUrlIfSchemeSupported(tab, webUrl)
+ verify(mockLoadUrlUseCase, times(1)).invoke(anyString(), anyString(), any(), any())
+
+ feature.loadUrlIfSchemeSupported(tab, aboutUrl)
+ verify(mockLoadUrlUseCase, times(2)).invoke(anyString(), anyString(), any(), any())
+ }
+
+ @Test
+ fun `WHEN caller and intent have the same package name THEN return true`() {
+ val customTab =
+ createCustomTab(
+ id = "c",
+ url = webUrl,
+ source = SessionState.Source.External.CustomTab(
+ ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
+ ),
+ )
+ val appIntent: Intent = mock()
+ val componentName: ComponentName = mock()
+ doReturn(componentName).`when`(appIntent).component
+ doReturn("com.zxing.app").`when`(componentName).packageName
+ assertTrue(feature.isSameCallerAndApp(customTab, appIntent))
+
+ val tab = createTab(webUrl, private = true)
+ assertFalse(feature.isSameCallerAndApp(tab, appIntent))
+
+ val customTab2 =
+ createCustomTab(
+ id = "c",
+ url = webUrl,
+ source = SessionState.Source.External.CustomTab(
+ ExternalPackage("com.example.app", PackageCategory.PRODUCTIVITY),
+ ),
+ )
+ assertFalse(feature.isSameCallerAndApp(customTab2, appIntent))
+
+ doReturn(null).`when`(componentName).packageName
+ assertFalse(feature.isSameCallerAndApp(customTab, appIntent))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksInterceptorTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksInterceptorTest.kt
new file mode 100644
index 0000000000..ca3a101cdf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksInterceptorTest.kt
@@ -0,0 +1,679 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.APP_LINKS_DO_NOT_INTERCEPT_INTERVAL
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.addUserDoNotIntercept
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.inUserDoNotIntercept
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.lastApplinksPackageWithTimestamp
+import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.userDoNotInterceptCache
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AppLinksInterceptorTest {
+ private lateinit var mockContext: Context
+ private lateinit var mockUseCases: AppLinksUseCases
+ private lateinit var mockGetRedirect: AppLinksUseCases.GetAppLinkRedirect
+ private lateinit var mockEngineSession: EngineSession
+ private lateinit var mockOpenRedirect: AppLinksUseCases.OpenAppLinkRedirect
+
+ private lateinit var appLinksInterceptor: AppLinksInterceptor
+
+ private val webUrl = "https://example.com"
+ private val webUrlWithAppLink = "https://soundcloud.com"
+ private val intentUrl = "zxing://scan;S.browser_fallback_url=example.com"
+ private val fallbackUrl = "https://getpocket.com"
+ private val marketplaceUrl = "market://details?id=example.com"
+
+ @Before
+ fun setup() {
+ mockContext = mock()
+ mockUseCases = mock()
+ mockEngineSession = mock()
+ mockGetRedirect = mock()
+ mockOpenRedirect = mock()
+ whenever(mockUseCases.interceptedAppLinkRedirect).thenReturn(mockGetRedirect)
+ whenever(mockUseCases.openAppLink).thenReturn(mockOpenRedirect)
+ userDoNotInterceptCache.clear()
+ lastApplinksPackageWithTimestamp = Pair(null, -APP_LINKS_DO_NOT_INTERCEPT_INTERVAL)
+
+ val webRedirect = AppLinkRedirect(null, webUrl, null)
+ val appRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), null, null)
+ val appRedirectFromWebUrl = AppLinkRedirect(Intent.parseUri(webUrlWithAppLink, 0), null, null)
+ val fallbackRedirect = AppLinkRedirect(null, fallbackUrl, null)
+ val marketRedirect = AppLinkRedirect(null, null, Intent.parseUri(marketplaceUrl, 0))
+
+ whenever(mockGetRedirect.invoke(webUrl)).thenReturn(webRedirect)
+ whenever(mockGetRedirect.invoke(intentUrl)).thenReturn(appRedirect)
+ whenever(mockGetRedirect.invoke(webUrlWithAppLink)).thenReturn(appRedirectFromWebUrl)
+ whenever(mockGetRedirect.invoke(fallbackUrl)).thenReturn(fallbackRedirect)
+ whenever(mockGetRedirect.invoke(marketplaceUrl)).thenReturn(marketRedirect)
+
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+ }
+
+ @Test
+ fun `request is intercepted by user clicking on a link`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `request is intercepted by redirect`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, false, false, true, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `request is not intercepted by a subframe redirect`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrl, null, false, false, true, false, true)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is intercepted by direct navigation`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, false, false, false, true, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `request is not intercepted when interceptLinkClicks is false`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted when launchInApp preference is false`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted when launchInApp preference is updated to false`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertEquals(null, response)
+
+ appLinksInterceptor.updateLaunchInApp { true }
+ verify(mockUseCases).updateLaunchInApp(any())
+ val response2 = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assert(response2 is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `request is not intercepted when not user clicking on a link`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, false, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted if the current session is already on the same host`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, webUrlWithAppLink, true, true, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted by a redirect on same domain`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, webUrlWithAppLink, true, true, true, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `domain is stripped before checking`() {
+ var response = appLinksInterceptor.onLoadRequest(mockEngineSession, "http://example.com", "example.com", true, true, true, false, false)
+ assertEquals(null, response)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, "https://example.com", "http://example.com", true, true, true, false, false)
+ assertEquals(null, response)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, "https://www.example.com", "http://example.com", true, true, true, false, false)
+ assertEquals(null, response)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, "http://www.example.com", "https://www.example.com", true, true, true, false, false)
+ assertEquals(null, response)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, "http://m.example.com", "https://www.example.com", true, true, true, false, false)
+ assertEquals(null, response)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, "http://mobile.example.com", "http://m.example.com", true, true, true, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted if a subframe request and not triggered by user`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, false, false, false, true, true)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `request is not intercepted if not user gesture, not redirect and not direct navigation`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, false, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `block listed schemes request not intercepted when triggered by user clicking on a link`() {
+ val engineSession: EngineSession = mock()
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val blocklistedUrl = "$blocklistedScheme://example.com"
+ val blocklistedRedirect = AppLinkRedirect(Intent.parseUri(blocklistedUrl, 0), blocklistedUrl, null)
+ whenever(mockGetRedirect.invoke(blocklistedUrl)).thenReturn(blocklistedRedirect)
+ var response = feature.onLoadRequest(engineSession, blocklistedUrl, null, true, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `supported schemes request not launched if launchInApp is false`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ engineSupportedSchemes = setOf(supportedScheme),
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val supportedUrl = "$supportedScheme://example.com"
+ val supportedRedirect = AppLinkRedirect(Intent.parseUri(supportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(supportedUrl)).thenReturn(supportedRedirect)
+ val response = feature.onLoadRequest(engineSession, supportedUrl, null, true, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `supported schemes request not launched if interceptLinkClicks is false`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ engineSupportedSchemes = setOf(supportedScheme),
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val supportedUrl = "$supportedScheme://example.com"
+ val supportedRedirect = AppLinkRedirect(Intent.parseUri(supportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(supportedUrl)).thenReturn(supportedRedirect)
+ val response = feature.onLoadRequest(engineSession, supportedUrl, null, true, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `supported schemes request not launched if not triggered by user`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ engineSupportedSchemes = setOf(supportedScheme),
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val supportedUrl = "$supportedScheme://example.com"
+ val supportedRedirect = AppLinkRedirect(Intent.parseUri(supportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(supportedUrl)).thenReturn(supportedRedirect)
+ val response = feature.onLoadRequest(engineSession, supportedUrl, null, false, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `not supported schemes request always intercepted regardless of hasUserGesture, interceptLinkClicks or launchInApp`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, false, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `blocklisted schemes request always ignored even if the engine does not support it`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(notSupportedScheme),
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, false, false, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `not supported schemes request should not use fallback if user preference is launch in app`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), fallbackUrl, null)
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, false, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `not supported schemes request uses fallback URL if available and launchInApp is set to false`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ val fallbackUrl = "https://example.com"
+ val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), fallbackUrl, null)
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `not supported schemes request uses fallback URL not market intent if launchInApp is set to false`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ val fallbackUrl = "https://example.com"
+ val notSupportedRedirect = AppLinkRedirect(null, fallbackUrl, Intent.parseUri(marketplaceUrl, 0))
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `intent scheme launch intent if fallback URL is unavailable and launchInApp is set to false`() {
+ val engineSession: EngineSession = mock()
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val intentUrl = "intent://example.com"
+ val intentRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(intentUrl)).thenReturn(intentRedirect)
+ val response = feature.onLoadRequest(engineSession, intentUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `intent scheme uses fallback URL if available and launchInApp is set to false`() {
+ val engineSession: EngineSession = mock()
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ )
+
+ val intentUrl = "intent://example.com"
+ val fallbackUrl = "https://example.com"
+ val intentRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), fallbackUrl, null)
+ whenever(mockGetRedirect.invoke(intentUrl)).thenReturn(intentRedirect)
+ val response = feature.onLoadRequest(engineSession, intentUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `request is not intercepted for URLs with javascript scheme`() {
+ val javascriptUri = "javascript:;"
+
+ val appRedirect = AppLinkRedirect(Intent.parseUri(javascriptUri, 0), null, null)
+ whenever(mockGetRedirect.invoke(javascriptUri)).thenReturn(appRedirect)
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, javascriptUri, null, true, true, false, false, false)
+ assertEquals(null, response)
+ }
+
+ @Test
+ fun `Use the fallback URL when no non-browser app is installed`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, fallbackUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `use the market intent if target app is not installed`() {
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, marketplaceUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `external app is launched when launch in app is set to true and it is user triggered`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `try to use fallback url if user preference is not to launch in third party app`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), fallbackUrl, null)
+ val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `external app is launched when url scheme is not supported by the engine`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, intentUrl, null, false, true, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `do not use fallback url if trigger by user gesture and preference is to launch in app`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), fallbackUrl, null)
+ val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `launch marketplace intent if available and no external app`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val testRedirect = AppLinkRedirect(null, fallbackUrl, Intent.parseUri(marketplaceUrl, 0))
+ val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, true)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `use fallback url if available and no external app`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ val testRedirect = AppLinkRedirect(null, fallbackUrl, null)
+ val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, true)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `WHEN url have same domain THEN is same domain returns true ELSE false`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ assert(appLinksInterceptor.isSameDomain("maps.google.com", "www.google.com"))
+ assert(appLinksInterceptor.isSameDomain("mobile.mozilla.com", "www.mozilla.com"))
+ assert(appLinksInterceptor.isSameDomain("m.mozilla.com", "maps.mozilla.com"))
+
+ assertFalse(appLinksInterceptor.isSameDomain("www.google.ca", "www.google.com"))
+ assertFalse(appLinksInterceptor.isSameDomain("maps.google.ca", "m.google.com"))
+ assertFalse(appLinksInterceptor.isSameDomain("accounts.google.com", "www.google.com"))
+ }
+
+ @Test
+ fun `WHEN request is in user do not intercept cache THEN request is not intercepted`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ addUserDoNotIntercept("https://soundcloud.com", null)
+
+ val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertNull(response)
+ }
+
+ @Test
+ fun `WHEN request is in user do not intercept cache but there is a fallback THEN fallback is used`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { false },
+ useCases = mockUseCases,
+ launchFromInterceptor = true,
+ )
+
+ addUserDoNotIntercept(intentUrl, null)
+ val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), fallbackUrl, null)
+ val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+ }
+
+ @Test
+ fun `WHEN request is in user do not intercept cache but engine doesn't support scheme THEN request is intercepted`() {
+ val engineSession: EngineSession = mock()
+ val supportedScheme = "supported"
+ val notSupportedScheme = "not_supported"
+ val blocklistedScheme = "blocklisted"
+ val feature = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = false,
+ engineSupportedSchemes = setOf(supportedScheme),
+ alwaysDeniedSchemes = setOf(blocklistedScheme),
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ val notSupportedUrl = "$notSupportedScheme://example.com"
+ addUserDoNotIntercept(notSupportedUrl, null)
+ val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), null, null)
+ whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
+ val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, false, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `WHEN added to user do not open cache THEN return true if user do no intercept cache exists`() {
+ addUserDoNotIntercept("test://test.com", null)
+ assertTrue(inUserDoNotIntercept("test://test.com", null))
+ assertFalse(inUserDoNotIntercept("https://test.com", null))
+
+ addUserDoNotIntercept("http://test.com", null)
+ assertTrue(inUserDoNotIntercept("https://test.com", null))
+ assertFalse(inUserDoNotIntercept("https://example.com", null))
+
+ val testIntent: Intent = mock()
+ val componentName: ComponentName = mock()
+ doReturn(componentName).`when`(testIntent).component
+ doReturn("app.example.com").`when`(componentName).packageName
+
+ addUserDoNotIntercept("https://example.com", testIntent)
+ assertTrue(inUserDoNotIntercept("https://example.com", testIntent))
+ assertTrue(inUserDoNotIntercept("https://test.com", testIntent))
+
+ doReturn("app.test.com").`when`(componentName).packageName
+ assertFalse(inUserDoNotIntercept("https://test.com", testIntent))
+ assertFalse(inUserDoNotIntercept("https://mozilla.org", null))
+ }
+
+ @Test
+ fun `WHEN user do not open cache expires THEN return false`() {
+ val testIntent: Intent = mock()
+ val componentName: ComponentName = mock()
+ doReturn(componentName).`when`(testIntent).component
+ doReturn("app.example.com").`when`(componentName).packageName
+
+ addUserDoNotIntercept("https://example.com", testIntent)
+ assertTrue(inUserDoNotIntercept("https://example.com", testIntent))
+ assertTrue(inUserDoNotIntercept("https://test.com", testIntent))
+
+ userDoNotInterceptCache["app.example.com".hashCode()] = -APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL
+ assertFalse(inUserDoNotIntercept("https://example.com", testIntent))
+ assertFalse(inUserDoNotIntercept("https://test.com", testIntent))
+ }
+
+ @Test
+ fun `WHEN request is redirecting to external app quickly THEN request is not intercepted`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ var response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertTrue(response is RequestInterceptor.InterceptionResponse.AppIntent)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertNull(response)
+ }
+
+ @Test
+ fun `WHEN request is redirecting to different app quickly THEN request is intercepted`() {
+ appLinksInterceptor = AppLinksInterceptor(
+ context = mockContext,
+ interceptLinkClicks = true,
+ launchInApp = { true },
+ useCases = mockUseCases,
+ )
+
+ var response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrl, null, true, false, false, false, false)
+ assert(response is RequestInterceptor.InterceptionResponse.Url)
+
+ response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
+ assertTrue(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
new file mode 100644
index 0000000000..12348433c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
@@ -0,0 +1,671 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageInfo
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.Browsers
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class AppLinksUseCasesTest {
+
+ private val appUrl = "https://example.com"
+ private val appIntent = "intent://example.com"
+ private val appSchemeIntent = "example://example.com"
+ private val appPackage = "com.example.app"
+ private val browserSchemeUrl = "browser://test"
+ private val browserPackage = Browsers.KnownBrowser.ANDROID_STOCK_BROWSER.packageName
+ private val testBrowserPackage = "com.current.browser"
+ private val filePath = "file:///storage/abc/test.mp3"
+ private val dataUrl = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
+ private val aboutUrl = "about:config"
+ private val javascriptUrl = "javascript:'hello, world'"
+ private val jarUrl = "jar:file://some/path/test.html"
+ private val contentUrl = "content://media/external_primary/downloads/12345"
+ private val fileType = "audio/mpeg"
+ private val layerUrl = "https://example.com"
+ private val layerPackage = "com.example.app"
+ private val layerActivity = "com.example2.app.intentActivity"
+ private val appIntentWithPackageAndFallback =
+ "intent://com.example.app#Intent;package=com.example.com;S.browser_fallback_url=https://example.com;end"
+
+ @Before
+ fun setup() {
+ AppLinksUseCases.redirectCache = null
+ }
+
+ private fun createContext(
+ vararg urlToPackages: Triple<String, String, String>,
+ default: Boolean = false,
+ installedApps: List<String> = emptyList(),
+ ): Context {
+ val pm = testContext.packageManager
+ val packageManager = shadowOf(pm)
+
+ urlToPackages.forEach { (urlString, pkgName, className) ->
+ val intent = Intent.parseUri(urlString, 0).addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val info = ActivityInfo().apply {
+ packageName = pkgName
+ name = className
+ icon = android.R.drawable.btn_default
+ }
+
+ val resolveInfo = ResolveInfo().apply {
+ labelRes = android.R.string.ok
+ activityInfo = info
+ }
+ @Suppress("DEPRECATION") // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/11832
+ packageManager.addResolveInfoForIntent(intent, resolveInfo)
+ packageManager.addDrawableResolution(pkgName, android.R.drawable.btn_default, mock())
+ }
+
+ val context = mock<Context>()
+ `when`(context.packageManager).thenReturn(pm)
+ if (!default) {
+ `when`(context.packageName).thenReturn(testBrowserPackage)
+ }
+
+ installedApps.forEach { name ->
+ val packageInfo = PackageInfo().apply {
+ packageName = name
+ }
+ packageManager.addPackageNoDefaults(packageInfo)
+ }
+
+ return context
+ }
+
+ @Test
+ fun `WHEN receiving a malformed URL THEN will not cause a crash`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+ val redirect = subject.interceptedAppLinkRedirect("test://test#Intent;")
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A URL that matches app with activity is an app link with correct component`() {
+ val context = createContext(Triple(layerUrl, layerPackage, layerActivity))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(layerUrl)
+ assertTrue(redirect.isRedirect())
+ assertEquals(redirect.appIntent?.component?.packageName, layerPackage)
+ assertEquals(redirect.appIntent?.component?.className, layerActivity)
+ }
+
+ @Test
+ fun `A URL that matches zero apps is not an app link`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A web URL that matches more than zero apps is an app link`() {
+ val context = createContext(Triple(appUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ // We will redirect to it if browser option set to true.
+ val redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A intent that targets a specific package but installed will not uses market intent`() {
+ val context = createContext(installedApps = listOf("com.example.com"))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
+ assertFalse(redirect.hasMarketplaceIntent())
+ assertTrue(redirect.hasFallback())
+ }
+
+ @Test
+ fun `A intent that targets a specific package but not installed will uses market intent`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
+ assertFalse(redirect.hasExternalApp())
+ assertTrue(redirect.hasMarketplaceIntent())
+ assertTrue(redirect.hasFallback())
+ }
+
+ @Test
+ fun `A file is not an app link`() {
+ val context = createContext(Triple(filePath, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ // We will redirect to it if browser option set to true.
+ val redirect = subject.interceptedAppLinkRedirect(filePath)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A data url is not an app link`() {
+ val context = createContext(Triple(dataUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(dataUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A javascript url is not an app link`() {
+ val context = createContext(Triple(javascriptUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(javascriptUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `An about url is not an app link`() {
+ val context = createContext(Triple(aboutUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(aboutUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A jar url is not an app link`() {
+ val context = createContext(Triple(jarUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(jarUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `A content url is not an app link`() {
+ val context = createContext(Triple(contentUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(contentUrl)
+ assertFalse(redirect.isRedirect())
+ }
+
+ @Test
+ fun `Will not redirect app link if browser option set to false and scheme is supported`() {
+ val context = createContext(Triple(appUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { false })
+
+ val redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertFalse(redirect.isRedirect())
+
+ val menuRedirect = subject.appLinkRedirect(appUrl)
+ assertTrue(menuRedirect.isRedirect())
+ }
+
+ @Test
+ fun `Will redirect app link if browser option set to false and scheme is not supported`() {
+ val context = createContext(Triple(appIntent, appPackage, ""))
+ val subject = AppLinksUseCases(context, { false })
+
+ val redirect = subject.interceptedAppLinkRedirect(appIntent)
+ assertTrue(redirect.isRedirect())
+
+ val menuRedirect = subject.appLinkRedirect(appIntent)
+ assertTrue(menuRedirect.isRedirect())
+ }
+
+ @Test
+ fun `WHEN A URL that matches a browser AND the scheme is not supported THEN is an app link`() {
+ val context = createContext(Triple(browserSchemeUrl, browserPackage, ""))
+ val browsers: Browsers = mock()
+ whenever(browsers.isInstalled(browserPackage)).thenReturn(true)
+ val subject = AppLinksUseCases(context = context, launchInApp = { true }, installedBrowsers = browsers)
+
+ val redirect = subject.interceptedAppLinkRedirect(browserSchemeUrl)
+ assertTrue(redirect.isRedirect())
+
+ val menuRedirect = subject.appLinkRedirect(browserSchemeUrl)
+ assertTrue(menuRedirect.isRedirect())
+ }
+
+ @Test
+ fun `WHEN A URL that matches a browser AND the scheme is supported THEN is not an app link`() {
+ val context = createContext(Triple(appUrl, browserPackage, ""))
+ val browsers: Browsers = mock()
+ whenever(browsers.isInstalled(browserPackage)).thenReturn(true)
+ val subject = AppLinksUseCases(context = context, launchInApp = { true }, installedBrowsers = browsers)
+
+ val redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertFalse(redirect.isRedirect())
+
+ val menuRedirect = subject.appLinkRedirect(appUrl)
+ assertFalse(menuRedirect.isRedirect())
+ }
+
+ @Test
+ fun `A intent scheme uri with an installed app is an app link`() {
+ val uri = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"
+ val context = createContext(Triple(uri, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(uri)
+ assertTrue(redirect.hasExternalApp())
+ assertNotNull(redirect.appIntent)
+ assertNotNull(redirect.marketplaceIntent)
+
+ assertEquals("zxing://scan/", redirect.appIntent!!.dataString)
+ }
+
+ @Test
+ fun `A bad intent scheme uri should not cause a crash`() {
+ val uri = "intent://blank#Intent;package=com.twitter.android%23Intent%3B;end"
+ val context = createContext(Triple(uri, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.appLinkRedirectIncludeInstall.invoke(uri)
+
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.isInstallable())
+ }
+
+ @Test
+ fun `A market scheme uri with no installed app is an install link`() {
+ val uri = "intent://details/#Intent;scheme=market;package=com.google.play;end"
+ val context = createContext(Triple(uri, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect.invoke(uri)
+
+ assertTrue(redirect.hasExternalApp())
+ assertTrue(redirect.isInstallable())
+ assert(
+ redirect.marketplaceIntent!!.flags and Intent.FLAG_ACTIVITY_NEW_TASK
+ == Intent.FLAG_ACTIVITY_NEW_TASK,
+ )
+ }
+
+ @Test
+ fun `A intent scheme uri without an installed app is not an app link`() {
+ val uri = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(uri)
+ assertFalse(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.fallbackUrl)
+ assertFalse(redirect.isInstallable())
+ }
+
+ @Test
+ fun `A intent scheme uri with a fallback without an installed app is not an app link`() {
+ val uri =
+ "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+
+ val redirect = subject.interceptedAppLinkRedirect(uri)
+ assertFalse(redirect.hasExternalApp())
+ assertTrue(redirect.hasFallback())
+
+ assertEquals("http://zxing.org", redirect.fallbackUrl)
+ }
+
+ @Test
+ fun `A intent scheme denied should return no app intent`() {
+ val uri = "intent://details/#Intent"
+ val context = createContext(Triple(uri, appPackage, ""))
+ val subject = AppLinksUseCases(context, { true }, alwaysDeniedSchemes = setOf("intent"))
+
+ val redirect = subject.interceptedAppLinkRedirect.invoke(uri)
+
+ assertNull(redirect.appIntent)
+ assertFalse(redirect.hasExternalApp())
+ }
+
+ @Test
+ fun `An openAppLink use case starts an activity`() {
+ val context = createContext()
+ val appIntent = Intent()
+ val redirect = AppLinkRedirect(appIntent, appUrl, null)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(redirect.appIntent)
+
+ verify(context).startActivity(any())
+ }
+
+ @Test
+ fun `Start activity fails will perform failure action`() {
+ val context = createContext()
+ val appIntent = Intent()
+ appIntent.putExtra(EXTRA_BROWSER_FALLBACK_URL, appUrl)
+ val redirect = AppLinkRedirect(appIntent, appUrl, null)
+ val subject = AppLinksUseCases(context, { true })
+
+ var failedToLaunch: String? = null
+ val failedAction = { fallbackUrl: String? -> failedToLaunch = fallbackUrl }
+ `when`(context.startActivity(any())).thenThrow(ActivityNotFoundException("failed"))
+ subject.openAppLink(redirect.appIntent, failedToLaunchAction = failedAction)
+
+ verify(context).startActivity(any())
+ assertEquals(failedToLaunch, appUrl)
+ }
+
+ @Test
+ fun `Security exception perform failure action`() {
+ val context = createContext()
+ val appIntent = Intent()
+ appIntent.putExtra(EXTRA_BROWSER_FALLBACK_URL, appUrl)
+ val redirect = AppLinkRedirect(appIntent, appUrl, null)
+ val subject = AppLinksUseCases(context, { true })
+
+ var failedToLaunch: String? = null
+ val failedAction = { fallbackUrl: String? -> failedToLaunch = fallbackUrl }
+ `when`(context.startActivity(any())).thenThrow(SecurityException("failed"))
+ subject.openAppLink(redirect.appIntent, failedToLaunchAction = failedAction)
+
+ verify(context).startActivity(any())
+ assertEquals(failedToLaunch, appUrl)
+ }
+
+ @Test
+ fun `Null pointer exception perform failure action`() {
+ val context = createContext()
+ val appIntent = Intent()
+ appIntent.putExtra(EXTRA_BROWSER_FALLBACK_URL, appUrl)
+ val redirect = AppLinkRedirect(appIntent, appUrl, null)
+ val subject = AppLinksUseCases(context, { true })
+
+ var failedToLaunch: String? = null
+ val failedAction = { fallbackUrl: String? -> failedToLaunch = fallbackUrl }
+ `when`(context.startActivity(any())).thenThrow(NullPointerException("failed"))
+ subject.openAppLink(redirect.appIntent, failedToLaunchAction = failedAction)
+
+ verify(context).startActivity(any())
+ assertEquals(failedToLaunch, appUrl)
+ }
+
+ @Test
+ fun `AppLinksUsecases uses cache`() {
+ val context = createContext(Triple(appUrl, appPackage, ""))
+
+ var subject = AppLinksUseCases(context, { true })
+ var redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ val timestamp = AppLinksUseCases.redirectCache?.cacheTimeStamp
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ assert(timestamp == AppLinksUseCases.redirectCache?.cacheTimeStamp)
+
+ AppLinksUseCases.clearRedirectCache()
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ }
+
+ @Test
+ fun `OpenAppLinkRedirect should not try to open files`() {
+ val context = createContext()
+ val uri = Uri.fromFile(File(filePath))
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, fileType)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(intent)
+
+ verify(context, never()).startActivity(any())
+ }
+
+ @Test
+ fun `OpenAppLinkRedirect should not try to open data URIs`() {
+ val context = createContext()
+ val uri = Uri.parse(dataUrl)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, fileType)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(intent)
+
+ verify(context, never()).startActivity(any())
+ }
+
+ @Test
+ fun `OpenAppLinkRedirect should not try to open javascript URIs`() {
+ val context = createContext()
+ val uri = Uri.parse(javascriptUrl)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, fileType)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(intent)
+
+ verify(context, never()).startActivity(any())
+ }
+
+ @Test
+ fun `OpenAppLinkRedirect should not try to open about URIs`() {
+ val context = createContext()
+ val uri = Uri.parse(aboutUrl)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, fileType)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(intent)
+
+ verify(context, never()).startActivity(any())
+ }
+
+ @Test
+ fun `OpenAppLinkRedirect should not try to open jar URIs`() {
+ val context = createContext()
+ val uri = Uri.parse(jarUrl)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, fileType)
+ val subject = AppLinksUseCases(context, { true })
+
+ subject.openAppLink(intent)
+
+ verify(context, never()).startActivity(any())
+ }
+
+ @Test
+ fun `WHEN receiving a app scheme uri WITH target package THEN will have marketplace intent`() {
+ val context = createContext()
+ val uri = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect(uri)
+ assertFalse(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNotNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(uri)
+ assertFalse(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNotNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ }
+
+ @Test
+ fun `WHEN receiving a app scheme uri THEN should try to redirect`() {
+ val context = createContext(Triple(appSchemeIntent, appPackage, ""))
+
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect(appSchemeIntent)
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appSchemeIntent)
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+ }
+
+ @Test
+ fun `WHEN opening a app scheme uri WITH fallback URL THEN use fallback if needed`() {
+ val context = createContext(Triple(appIntentWithPackageAndFallback, appPackage, ""))
+
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
+ assertFalse(redirect.hasExternalApp())
+ assertTrue(redirect.hasFallback())
+ assertTrue(redirect.marketplaceIntent != null)
+ assertEquals(redirect.fallbackUrl, "https://example.com")
+
+ AppLinksUseCases.clearRedirectCache()
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
+ assertTrue(redirect.hasExternalApp())
+ assertTrue(redirect.hasFallback())
+ assertTrue(redirect.marketplaceIntent != null)
+ assertEquals(redirect.fallbackUrl, "https://example.com")
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+ }
+
+ @Test
+ fun `WHEN opening a app scheme uri THEN tries to redirect`() {
+ val context = createContext(Triple(appIntent, appPackage, ""))
+
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect(appIntent)
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appIntent)
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+ }
+
+ @Test
+ fun `WHEN opening a app scheme uri WITHOUT package installed THEN do not try to redirect`() {
+ val context = createContext()
+
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect(appIntent)
+ assertFalse(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appIntent)
+ assertFalse(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ }
+
+ @Test
+ fun `WHEN opening a app scheme uri without a host WITH package installed THEN try to redirect`() {
+ val context = createContext(urlToPackages = arrayOf(Triple("my.scheme", appPackage, "")), default = true, installedApps = listOf(appPackage))
+
+ var subject = AppLinksUseCases(context, { false })
+ var redirect = subject.interceptedAppLinkRedirect("my.scheme")
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect("my.scheme")
+ assertTrue(redirect.hasExternalApp())
+ assertFalse(redirect.hasFallback())
+ assertNull(redirect.marketplaceIntent)
+ assertNull(redirect.fallbackUrl)
+ assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
+ }
+
+ @Test
+ fun `Failed to parse uri should not cause a crash`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+ var uri = "intent://blank#Intent;package=test"
+ var result = subject.safeParseUri(uri, 0)
+
+ assertNull(result)
+
+ uri =
+ "intent://blank#Intent;package=test;i.android.support.customtabs.extra.TOOLBAR_COLOR=2239095040;end"
+ result = subject.safeParseUri(uri, 0)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `Intent targeting same package should return null`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+ val uri = "intent://blank#Intent;package=$testBrowserPackage;end"
+ val result = subject.safeParseUri(uri, 0)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `Intent targeting external package should not return null`() {
+ val context = createContext()
+ val subject = AppLinksUseCases(context, { true })
+ val uri = "intent://blank#Intent;package=org.mozilla.test;end"
+ val result = subject.safeParseUri(uri, 0)
+
+ assertNotNull(result)
+ assertEquals(result?.`package`, "org.mozilla.test")
+ }
+
+ @Test
+ fun `WHEN launch in app is updated to true THEN should redirect`() {
+ val context = createContext(Triple(appUrl, appPackage, ""))
+ val subject = AppLinksUseCases(context, { false })
+
+ var redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertFalse(redirect.isRedirect())
+
+ AppLinksUseCases.clearRedirectCache()
+ subject.updateLaunchInApp { true }
+ redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragmentTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragmentTest.kt
new file mode 100644
index 0000000000..268e4df4d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragmentTest.kt
@@ -0,0 +1,113 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.app.links
+
+import android.os.Looper.getMainLooper
+import android.widget.Button
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class SimpleRedirectDialogFragmentTest {
+ private val webUrl = "https://example.com"
+ private val themeResId = appcompatR.style.Theme_AppCompat_Light
+
+ @Test
+ fun `Dialog confirmed callback is called correctly`() {
+ var onConfirmCalled = false
+ var onCancelCalled = false
+
+ val onConfirm = { onConfirmCalled = true }
+ val onCancel = { onCancelCalled = true }
+
+ val fragment = spy(SimpleRedirectDialogFragment.newInstance(themeResId = themeResId))
+ doNothing().`when`(fragment).dismiss()
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ fragment.onConfirmRedirect = onConfirm
+ fragment.onCancelRedirect = onCancel
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val confirmButton = dialog.findViewById<Button>(android.R.id.button1)
+ confirmButton?.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onConfirmCalled)
+ assertFalse(onCancelCalled)
+ }
+
+ @Test
+ fun `Dialog cancel callback is called correctly`() {
+ var onConfirmCalled = false
+ var onCancelCalled = false
+
+ val onConfirm = { onConfirmCalled = true }
+ val onCancel = { onCancelCalled = true }
+
+ val fragment = spy(SimpleRedirectDialogFragment.newInstance(themeResId = themeResId))
+ doNothing().`when`(fragment).dismiss()
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ fragment.onConfirmRedirect = onConfirm
+ fragment.onCancelRedirect = onCancel
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val confirmButton = dialog.findViewById<Button>(android.R.id.button2)
+ confirmButton?.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onConfirmCalled)
+ assertTrue(onCancelCalled)
+ }
+
+ @Test
+ fun `Dialog confirm and cancel is not called when dismissed`() {
+ var onConfirmCalled = false
+ var onCancelCalled = false
+
+ val onConfirm = { onConfirmCalled = true }
+ val onCancel = { onCancelCalled = true }
+
+ val fragment = spy(SimpleRedirectDialogFragment.newInstance(themeResId = themeResId))
+ doNothing().`when`(fragment).dismiss()
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ fragment.onConfirmRedirect = onConfirm
+ fragment.onCancelRedirect = onCancel
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ dialog.dismiss()
+
+ assertFalse(onConfirmCalled)
+ assertFalse(onCancelCalled)
+ }
+
+ private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ return fragmentManager
+ }
+}
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/app-links/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/app-links/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/app-links/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28